Files
KoalaSync/scripts/release-preflight.mjs
T
2026-08-21 15:49:51 +02:00

112 lines
4.4 KiB
JavaScript

#!/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 || '<empty>'}`);
}
return repo;
}
export function validateVersionSnapshot(expectedVersion, snapshot) {
for (const [label, actualVersion] of Object.entries(snapshot)) {
if (actualVersion !== expectedVersion) {
throw new Error(`${label} version ${actualVersion || '<missing>'} 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;
}
}