const fs = require('fs'); const path = require('path'); const https = require('https'); const { execSync } = require('child_process'); const PROJECT_ID = process.env.CI_PROJECT_ID || 'vyla-entertainment%2Fgo'; const TOKEN = process.env.GITLAB_TOKEN || process.env.VYLA_GITLAB_TOKEN; if (!TOKEN) { console.error('Error: GITLAB_TOKEN or VYLA_GITLAB_TOKEN is required.'); process.exit(1); } function incrementVersion() { const pkgPath = path.join(__dirname, 'package.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); let [major, minor, patch] = (pkg.version || '1.0.0').split('.').map(Number); patch = (patch || 0) + 1; const newVersion = `${major}.${minor}.${patch}`; pkg.version = newVersion; fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8'); const tauriPath = path.join(__dirname, 'src-tauri', 'tauri.conf.json'); if (fs.existsSync(tauriPath)) { const tauriConf = JSON.parse(fs.readFileSync(tauriPath, 'utf8')); tauriConf.version = newVersion; fs.writeFileSync(tauriPath, JSON.stringify(tauriConf, null, 2) + '\n', 'utf8'); } return newVersion; } function gitCommitAndTag(version) { try { execSync('git add package.json src-tauri/tauri.conf.json', { stdio: 'inherit' }); try { execSync(`git commit -m "chore: release ${version}"`, { stdio: 'inherit' }); } catch (_) { console.log('Working tree clean, skipping commit.'); } try { execSync(`git tag -a ${version} -m "Release ${version}"`, { stdio: 'inherit' }); } catch (_) { console.log(`Tag ${version} already exists locally.`); } execSync('git push origin HEAD --tags', { stdio: 'inherit' }); } catch (e) { console.warn('Git push failed (will fallback to GitLab API ref):', e.message); } } function httpsRequest(options, body = null) { return new Promise((resolve, reject) => { const req = https.request(options, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { if (res.statusCode >= 200 && res.statusCode < 300) resolve({ status: res.statusCode, body: data }); else reject(new Error(`HTTP ${res.statusCode}: ${data}`)); }); }); req.on('error', reject); if (body) req.write(body); req.end(); }); } function uploadFile(filePath, packageName, version, fileName) { return new Promise((resolve, reject) => { const stat = fs.statSync(filePath); const timestamp = Date.now(); const uploadName = `${path.parse(fileName).name}-${timestamp}${path.parse(fileName).ext}`; const urlPath = `/api/v4/projects/${PROJECT_ID}/packages/generic/${packageName}/${version}/${encodeURIComponent(uploadName)}`; const req = https.request({ hostname: 'gitlab.com', path: urlPath, method: 'PUT', headers: { 'PRIVATE-TOKEN': TOKEN, 'Content-Type': 'application/octet-stream', 'Content-Length': stat.size } }, (res) => { let body = ''; res.on('data', chunk => body += chunk); res.on('end', () => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve({ uploadName, url: `https://gitlab.com/api/v4/projects/${PROJECT_ID}/packages/generic/${packageName}/${version}/${encodeURIComponent(uploadName)}` }); } else { reject(new Error(`Upload failed (${res.statusCode}): ${body}`)); } }); }); req.on('error', reject); fs.createReadStream(filePath).pipe(req); }); } async function createRelease(version, assets) { const tag = version; const apk = assets.apk; const payload = assets.payload; const manifest = assets.manifest; let branch = 'main'; try { branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim() || 'main'; } catch (_) { } const description = [ `## Automated Release ${version}`, '', '### Assets', '', `* [vyla-go-${version}.apk](${apk.url})`, '', '### Payload', '', `* [manifest.json](${manifest.url})`, `* [update-payload.zip](${payload.url})`, ].join('\n'); const body = JSON.stringify({ name: `Vyla Go Release ${version}`, tag_name: tag, ref: branch, description, assets: { links: [ { name: `vyla-go-${version}.apk`, url: apk.url, link_type: 'package' }, { name: 'update-payload.zip', url: payload.url, link_type: 'other' }, { name: 'manifest.json', url: manifest.url, link_type: 'other' }, ] } }); await httpsRequest({ hostname: 'gitlab.com', path: `/api/v4/projects/${PROJECT_ID}/releases`, method: 'POST', headers: { 'PRIVATE-TOKEN': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }, body); } async function main() { const rawVersion = incrementVersion(); const version = `v${rawVersion}`; console.log(`Releasing ${version}...`); const androidHome = process.env.ANDROID_HOME || `${process.env.LOCALAPPDATA}\\Android\\Sdk`; const ndkHome = process.env.NDK_HOME || `${androidHome}\\ndk\\29.0.13846066`; const buildEnv = { ...process.env, ANDROID_HOME: androidHome, NDK_HOME: ndkHome, GITLAB_TOKEN: TOKEN, VYLA_GITLAB_TOKEN: TOKEN, }; console.log('Building payload...'); execSync('npm run build:payload', { stdio: 'inherit', env: buildEnv }); console.log('Building APK...'); execSync('npm run build:apk', { stdio: 'inherit', env: buildEnv }); const apkSearchDirs = [ path.join(__dirname, 'src-tauri', 'gen', 'android', 'app', 'build', 'outputs', 'apk', 'universal', 'release'), path.join(__dirname, 'src-tauri', 'gen', 'android', 'app', 'build', 'outputs', 'apk', 'arm64', 'release'), path.join(__dirname, 'src-tauri', 'gen', 'android', 'app', 'build', 'outputs', 'apk', 'release'), ]; let apkPath = null; for (const dir of apkSearchDirs) { if (!fs.existsSync(dir)) continue; const found = fs.readdirSync(dir).find(f => f.endsWith('.apk') && !f.includes('unsigned')); if (found) { apkPath = path.join(dir, found); break; } } if (!apkPath) throw new Error('Signed release APK not found'); const payloadPath = path.join(__dirname, 'update-payload.zip'); const manifestPath = path.join(__dirname, 'manifest.json'); if (!fs.existsSync(payloadPath)) throw new Error('update-payload.zip not found — run build:payload first'); if (!fs.existsSync(manifestPath)) throw new Error('manifest.json not found — run build:payload first'); gitCommitAndTag(version); console.log('Uploading APK...'); const apkAsset = await uploadFile(apkPath, 'vyla-go', version, `vyla-go-${version}.apk`); console.log('Uploading payload...'); const payloadAsset = await uploadFile(payloadPath, 'vyla-go', version, 'update-payload.zip'); console.log('Uploading manifest...'); const manifestAsset = await uploadFile(manifestPath, 'vyla-go', version, 'manifest.json'); console.log('Creating GitLab release...'); await createRelease(version, { apk: apkAsset, payload: payloadAsset, manifest: manifestAsset }); console.log(`\nRelease ${version} published successfully.`); console.log(`APK: ${apkAsset.url}`); console.log(`Payload: ${payloadAsset.url}`); console.log(`Manifest: ${manifestAsset.url}`); } main().catch(err => { console.error(err); process.exit(1); });