const fs = require('fs'); const path = require('path'); const https = require('https'); const { execSync } = require('child_process'); const GITLAB_PROJECT_PATH = 'vyla-entertainment%2Fhome'; const PROJECT_ID = process.env.CI_PROJECT_ID || GITLAB_PROJECT_PATH; const TOKEN = process.env.GITLAB_TOKEN || process.env.VYLA_GITLAB_TOKEN; if (!TOKEN) { console.error('Error: GITLAB_TOKEN or VYLA_GITLAB_TOKEN environment variable 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, 4) + '\n', 'utf8'); } const cargoPath = path.join(__dirname, 'src-tauri', 'Cargo.toml'); if (fs.existsSync(cargoPath)) { let cargo = fs.readFileSync(cargoPath, 'utf8'); cargo = cargo.replace(/^version\s*=\s*".*"/m, `version = "${newVersion}"`); fs.writeFileSync(cargoPath, cargo, 'utf8'); } return newVersion; } function uploadFileToGitLabPackage(filePath, packageName, version, fileName) { return new Promise((resolve, reject) => { const stat = fs.statSync(filePath); const url = `https://gitlab.com/api/v4/projects/${PROJECT_ID}/packages/generic/${packageName}/${version}/${encodeURIComponent(fileName)}`; const urlObj = new URL(url); const req = https.request({ hostname: urlObj.hostname, port: 443, path: urlObj.pathname, 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(url); } else { reject(new Error(`Failed to upload ${fileName} (HTTP ${res.statusCode}): ${body}`)); } }); }); req.on('error', reject); const fileStream = fs.createReadStream(filePath); fileStream.on('error', reject); fileStream.pipe(req); }); } function createGitLabRelease(tag, releaseLinks) { const sendRequest = (method, path) => { return new Promise((resolve, reject) => { let currentRef = 'main'; try { currentRef = execSync('git rev-parse HEAD').toString().trim(); } catch (e) { } const postData = JSON.stringify({ name: `Vyla Home Release ${tag}`, tag_name: tag, ref: currentRef, description: `Automated local release ${tag}`, assets: { links: releaseLinks } }); const req = https.request({ hostname: 'gitlab.com', port: 443, path: path, method: method, headers: { 'PRIVATE-TOKEN': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) } }, (res) => { let body = ''; res.on('data', chunk => body += chunk); res.on('end', () => { if (res.statusCode === 200 || res.statusCode === 201) { resolve(JSON.parse(body)); } else if (res.statusCode === 409 && method === 'POST') { console.log(`Release ${tag} already exists, updating assets...`); sendRequest('PUT', `/api/v4/projects/${PROJECT_ID}/releases/${encodeURIComponent(tag)}`) .then(resolve) .catch(reject); } else { reject(new Error(`Failed to publish release (HTTP ${res.statusCode}): ${body}`)); } }); }); req.on('error', reject); req.write(postData); req.end(); }); }; return sendRequest('POST', `/api/v4/projects/${PROJECT_ID}/releases`); } async function main() { const isFullBuild = process.argv.includes('--installer'); const customTag = process.argv.find(arg => arg.startsWith('--tag='))?.split('=')[1]; const version = customTag || `v${incrementVersion()}`; console.log(`Building Vyla Home Release (${version})`); console.log('[1/4] Building update payload and manifest...'); execSync('node build-pipeline.js', { stdio: 'inherit' }); const payloadPath = path.join(__dirname, 'update-payload.zip'); const manifestPath = path.join(__dirname, 'manifest.json'); if (!fs.existsSync(payloadPath) || !fs.existsSync(manifestPath)) { throw new Error('update-payload.zip or manifest.json not found.'); } let installerPath = null; let installerUploadName = null; if (isFullBuild) { console.log('Ensuring bundled Node.js runtime...'); await ensureBundledNode(); await ensureWebView2Loader(); const bundleDir = path.join(__dirname, 'src-tauri', 'target', 'release', 'bundle'); if (fs.existsSync(bundleDir)) { fs.rmSync(bundleDir, { recursive: true, force: true }); } console.log('Compiling Windows Tauri installer locally...'); execSync('npm run build', { stdio: 'inherit' }); const nsisDir = path.join(__dirname, 'src-tauri/target/release/bundle/nsis'); if (fs.existsSync(nsisDir)) { const targetVersion = version.startsWith('v') ? version.substring(1) : version; const files = fs.readdirSync(nsisDir).filter(f => f.endsWith('.exe') && f.includes(targetVersion)); if (files.length > 0) { const originalName = files[0]; installerPath = path.join(nsisDir, originalName); installerUploadName = originalName.replace(/\s+/g, '-'); } } } console.log('[2/4] Uploading artifacts to GitLab Package Registry...'); console.log('Uploading update-payload.zip...'); const payloadUrl = await uploadFileToGitLabPackage(payloadPath, 'vyla-home', version, 'update-payload.zip'); console.log('Uploading manifest.json...'); const manifestUrl = await uploadFileToGitLabPackage(manifestPath, 'vyla-home', version, 'manifest.json'); const releaseLinks = [ { name: 'update-payload.zip', url: payloadUrl, link_type: 'other' }, { name: 'manifest.json', url: manifestUrl, link_type: 'other' } ]; if (installerPath && installerUploadName) { console.log(`Uploading installer ${installerUploadName}...`); const installerUrl = await uploadFileToGitLabPackage(installerPath, 'vyla-home', version, installerUploadName); releaseLinks.push({ name: installerUploadName, url: installerUrl, link_type: 'package' }); } console.log('[3/4] Publishing GitLab Release...'); const release = await createGitLabRelease(version, releaseLinks); console.log(`[4/4] Release successfully created: ${release.name} (${release.tag_name})`); } function ensureBundledNode() { const binDir = path.join(__dirname, 'bin'); const nodeExePath = path.join(binDir, 'node.exe'); const nodeVersion = 'v22.14.0'; const versionMarker = path.join(binDir, `node-${nodeVersion}.marker`); if (fs.existsSync(nodeExePath) && fs.existsSync(versionMarker)) { return Promise.resolve(); } fs.mkdirSync(binDir, { recursive: true }); console.log(`Downloading standalone Node.js ${nodeVersion} runtime for bundling...`); const nodeUrl = `https://nodejs.org/dist/${nodeVersion}/win-x64/node.exe`; return new Promise((resolve, reject) => { const fileStream = fs.createWriteStream(nodeExePath); https.get(nodeUrl, (res) => { if (res.statusCode === 302 || res.statusCode === 301) { https.get(res.headers.location, (redirectRes) => { redirectRes.pipe(fileStream); fileStream.on('finish', () => { fileStream.close(); fs.writeFileSync(versionMarker, nodeVersion, 'utf8'); resolve(); }); }).on('error', reject); return; } res.pipe(fileStream); fileStream.on('finish', () => { fileStream.close(); fs.writeFileSync(versionMarker, nodeVersion, 'utf8'); resolve(); }); }).on('error', (err) => { try { fs.unlinkSync(nodeExePath); } catch (e) { } reject(err); }); }); } function ensureWebView2Loader() { const releaseDir = path.join(__dirname, 'src-tauri', 'target', 'release'); const rootDllPath = path.join(__dirname, 'src-tauri', 'WebView2Loader.dll'); fs.mkdirSync(releaseDir, { recursive: true }); const targetDllPath = path.join(releaseDir, 'WebView2Loader.dll'); if (fs.existsSync(rootDllPath) && fs.existsSync(targetDllPath)) { return Promise.resolve(); } const AdmZip = require('adm-zip'); const tempNupkg = path.join(__dirname, 'temp-wv2.zip'); const nupkgUrl = 'https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/1.0.2903.40'; return new Promise((resolve, reject) => { const fileStream = fs.createWriteStream(tempNupkg); const request = (url) => { https.get(url, (res) => { if (res.statusCode === 301 || res.statusCode === 302) { return request(res.headers.location); } res.pipe(fileStream); fileStream.on('finish', () => { fileStream.close(); try { const zip = new AdmZip(tempNupkg); const zipEntries = zip.getEntries(); const dllEntry = zipEntries.find(e => e.entryName.includes('x64/WebView2Loader.dll') || e.entryName.includes('x64\\WebView2Loader.dll') ); if (dllEntry) { const buffer = dllEntry.getData(); fs.writeFileSync(targetDllPath, buffer); fs.writeFileSync(rootDllPath, buffer); } try { fs.unlinkSync(tempNupkg); } catch (_) { } resolve(); } catch (e) { try { fs.unlinkSync(tempNupkg); } catch (_) { } reject(e); } }); }).on('error', (err) => { try { fs.unlinkSync(tempNupkg); } catch (_) { } reject(err); }); }; request(nupkgUrl); }); } main().catch(err => { console.error('Release failed:', err.message); process.exit(1); });