#!/usr/bin/env node import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; import { versionFromTag } from './release-artifact-checks.mjs'; export const REQUIRED_RELEASE_CHECKS = Object.freeze(['verify', 'node20', 'e2e']); export function parseCheckRuns(text) { return String(text).split(/\r?\n/u).filter(Boolean).map(line => { const [name, conclusion, url = ''] = line.split('\t'); if (!name || !conclusion) throw new Error(`Invalid check-run record: ${line}`); return { name, conclusion, url }; }); } export function validateRequiredChecks(checkRuns, required = REQUIRED_RELEASE_CHECKS) { for (const name of required) { const matches = checkRuns.filter(check => check.name === name); if (matches.length === 0) throw new Error(`Required check is missing for the release commit: ${name}`); if (matches.some(check => check.conclusion !== 'success')) { const conclusions = matches.map(check => check.conclusion).join(', '); throw new Error(`Required check ${name} did not succeed: ${conclusions}`); } } } function run(command, args) { return execFileSync(command, args, { cwd: process.cwd(), encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); } export function validateRepositoryName(repo) { if (!/^[^/\s]+\/[^/\s]+$/u.test(repo || '')) { throw new Error(`Invalid GitHub repository: ${repo || ''}`); } return repo; } export function validateVersionSnapshot(expectedVersion, snapshot) { for (const [label, actualVersion] of Object.entries(snapshot)) { if (actualVersion !== expectedVersion) { throw new Error(`${label} version ${actualVersion || ''} does not match tag version ${expectedVersion}`); } } } export function validateReleaseSourceVersion(expectedVersion, repoRoot = process.cwd()) { const readJson = relativePath => JSON.parse(fs.readFileSync(path.join(repoRoot, relativePath), 'utf8')); const packageJson = readJson('package.json'); const packageLock = readJson('package-lock.json'); const manifest = readJson('extension/manifest.base.json'); const websiteVersion = readJson('website/version.json'); const constants = fs.readFileSync(path.join(repoRoot, 'shared/constants.js'), 'utf8'); const appVersion = /export const APP_VERSION = ["']([^"']+)["']/u.exec(constants)?.[1] || ''; validateVersionSnapshot(expectedVersion, { 'package.json': packageJson.version, 'package-lock.json': packageLock.version, 'package-lock root package': packageLock.packages?.['']?.version, 'extension manifest': manifest.version, 'shared constants': appVersion, 'website/version.json': websiteVersion.version }); } export function verifyReleaseRef({ tag, repo }) { const version = versionFromTag(tag); validateRepositoryName(repo); validateReleaseSourceVersion(version); const tagRef = `refs/tags/${tag}`; if (run('git', ['cat-file', '-t', tagRef]) !== 'tag') { throw new Error(`${tag} must be an annotated tag`); } const tagCommit = run('git', ['rev-list', '-n', '1', tagRef]); const mainCommit = run('git', ['rev-parse', 'origin/main']); if (tagCommit !== mainCommit) { throw new Error(`Release tag ${tag} points to ${tagCommit}, but origin/main is ${mainCommit}`); } const checks = parseCheckRuns(run('gh', [ 'api', `repos/${repo}/commits/${tagCommit}/check-runs`, '--jq', '.check_runs[] | [.name, .conclusion, .html_url] | @tsv' ])); validateRequiredChecks(checks); return { version, tagCommit }; } function main() { const tag = process.env.GITHUB_REF_NAME || ''; const repo = process.env.GITHUB_REPOSITORY || ''; const outputPath = process.env.GITHUB_OUTPUT || ''; const result = verifyReleaseRef({ tag, repo }); if (!outputPath) throw new Error('GITHUB_OUTPUT is required'); fs.appendFileSync(outputPath, `version=${result.version}\ntag_commit=${result.tagCommit}\n`, 'utf8'); console.log(`Release preflight accepted ${tag} at ${result.tagCommit}`); } const isMainModule = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]; if (isMainModule) { try { main(); } catch (error) { console.error(`Release preflight failed: ${error.message}`); process.exitCode = 1; } }