#!/usr/bin/env node const fs = require('fs') const path = require('path') const yaml = require('js-yaml') const { spawnSync } = require('child_process') function usage() { console.log(` Usage: node scripts/publish-release.js --channel [options] Options: --channel Release channel. Required. --changelog Optional changelog file. Default: auto-resolve from version --config Config file path. Default: config.yaml --help Show this help message `) } function parseArgs(argv) { const args = {} for (let i = 0; i < argv.length; i += 1) { const token = argv[i] if (!token.startsWith('--')) continue const key = token.slice(2) const value = argv[i + 1] if (!value || value.startsWith('--')) { args[key] = true continue } args[key] = value i += 1 } return args } function assert(condition, message) { if (!condition) { throw new Error(message) } } function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf-8')) } function readYaml(filePath) { return yaml.load(fs.readFileSync(filePath, 'utf-8')) } function resolveChangelogPath(version, explicitPath) { if (explicitPath) { return path.resolve(process.cwd(), explicitPath) } const rebuildPath = path.resolve(process.cwd(), 'docs', 'releases', `${version}-rebuild.md`) if (fs.existsSync(rebuildPath)) { return rebuildPath } return path.resolve(process.cwd(), 'docs', 'releases', `${version}.md`) } function validateUpdateConfig(configPath) { assert(fs.existsSync(configPath), `Config file not found: ${configPath}`) const parsed = readYaml(configPath) const update = parsed && parsed.update assert(update, 'Missing update config in config file') assert(update.enabled, 'update.enabled is false') assert(update.endpoint, 'update.endpoint is required') assert(update.accessKey, 'update.accessKey is required') assert(update.secretKey, 'update.secretKey is required') assert(update.bucket, 'update.bucket is required') assert(update.basePrefix, 'update.basePrefix is required') return update } function hasConflictMarker(filePath) { const content = fs.readFileSync(filePath, 'utf-8') return content.includes('<<<<<<<') || content.includes('=======') || content.includes('>>>>>>>') } function validateVersionFiles(version) { const packageJsonPath = path.resolve(process.cwd(), 'package.json') const packageLockPath = path.resolve(process.cwd(), 'package-lock.json') assert(fs.existsSync(packageLockPath), `package-lock.json not found: ${packageLockPath}`) assert(!hasConflictMarker(packageJsonPath), 'package.json contains merge conflict markers') assert(!hasConflictMarker(packageLockPath), 'package-lock.json contains merge conflict markers') const packageLock = readJson(packageLockPath) assert(packageLock.version === version, 'package-lock.json version does not match package.json') const rootPackage = packageLock.packages && packageLock.packages[''] if (rootPackage && rootPackage.version) { assert( rootPackage.version === version, 'package-lock root package version does not match package.json' ) } } function runStep(name, command, args, envOverrides = {}) { console.log('') console.log(`==> ${name}`) console.log(`$ ${command} ${args.join(' ')}`) let result if (process.platform === 'win32') { const shellCommand = [command, ...args] .map((arg) => (/\s|"/.test(arg) ? `"${String(arg).replace(/"/g, '\\"')}"` : arg)) .join(' ') result = spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', shellCommand], { cwd: process.cwd(), env: { ...process.env, ...envOverrides }, stdio: 'inherit' }) } else { result = spawnSync(command, args, { cwd: process.cwd(), env: { ...process.env, ...envOverrides }, stdio: 'inherit' }) } if (result.error) { throw result.error } if (result.status !== 0) { throw new Error(`${name} failed with exit code ${result.status}`) } } function readPreparedIndex(channel, basePrefix) { const indexPath = path.resolve( process.cwd(), 'release-output', ...basePrefix.split('/'), channel, 'index.json' ) assert(fs.existsSync(indexPath), `Prepared index not found: ${indexPath}`) const parsed = readJson(indexPath) assert(parsed && Array.isArray(parsed.releases), `Invalid prepared index: ${indexPath}`) return { indexPath, parsed } } function summarizeRelease(version, channel, releaseEntry, indexPath) { console.log('') console.log('Release published successfully.') console.log(`Version: ${version}`) console.log(`Channel: ${channel}`) console.log(`Artifact: ${releaseEntry.artifactKey}`) console.log(`SHA256: ${releaseEntry.sha256}`) console.log(`Changelog: ${releaseEntry.changelogKey}`) console.log(`Prepared Index: ${indexPath}`) console.log(`Published At: ${releaseEntry.publishedAt}`) } function main() { const args = parseArgs(process.argv.slice(2)) if (args.help || args.h) { usage() process.exit(0) } const channel = args.channel assert(channel === 'stable' || channel === 'preview', 'Missing or invalid --channel') const packageJsonPath = path.resolve(process.cwd(), 'package.json') assert(fs.existsSync(packageJsonPath), `package.json not found: ${packageJsonPath}`) const packageJson = readJson(packageJsonPath) const version = packageJson.version assert(version, 'package.json version is required') const configPath = path.resolve(process.cwd(), args.config || 'config.yaml') const updateConfig = validateUpdateConfig(configPath) validateVersionFiles(version) const changelogPath = resolveChangelogPath(version, args.changelog) assert(fs.existsSync(changelogPath), `Changelog not found: ${changelogPath}`) runStep('Build Windows package', 'npm', ['run', 'build:win'], { APP_CHANNEL: channel }) runStep('Prepare release package', 'npm', [ 'run', 'release:prepare', '--', '--channel', channel, '--changelog', changelogPath ]) const { indexPath, parsed } = readPreparedIndex(channel, updateConfig.basePrefix) const latestRelease = parsed.releases[0] assert(latestRelease, 'Prepared index does not contain any release entry') assert( latestRelease.version === version && latestRelease.channel === channel, `Prepared index latest entry mismatch: expected ${version}/${channel}, got ${latestRelease.version}/${latestRelease.channel}` ) runStep('Upload release package', 'npm', [ 'run', 'release:upload', '--', '--channel', channel, '--verify' ]) summarizeRelease(version, channel, latestRelease, indexPath) } try { main() } catch (error) { console.error(`publish-release failed: ${error.message}`) process.exit(1) }