2026-05-15 01:24:10 +00:00
|
|
|
import { spawnSync } from "node:child_process";
|
|
|
|
|
|
|
|
|
|
function fail(message) {
|
|
|
|
|
console.error(`release preflight failed: ${message}`);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function run(command, args, options = {}) {
|
2026-06-20 10:15:58 +00:00
|
|
|
const result = spawn(command, args, options);
|
2026-05-15 01:24:10 +00:00
|
|
|
if (result.error) fail(`${command} ${args.join(" ")}: ${result.error.message}`);
|
|
|
|
|
if (result.status !== 0) {
|
|
|
|
|
if (options.capture) {
|
|
|
|
|
const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
|
|
|
|
if (output) console.error(output);
|
|
|
|
|
}
|
|
|
|
|
fail(`${command} ${args.join(" ")} exited with ${result.status}`);
|
|
|
|
|
}
|
|
|
|
|
return options.capture ? result.stdout.trim() : "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function maybeRun(command, args) {
|
2026-06-20 10:15:58 +00:00
|
|
|
const result = spawn(command, args, { capture: true });
|
|
|
|
|
return result.status === 0 ? result.stdout.trim() : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function spawn(command, args, options = {}) {
|
|
|
|
|
return spawnSync(command, args, {
|
2026-05-15 01:24:10 +00:00
|
|
|
encoding: "utf8",
|
2026-06-20 10:15:58 +00:00
|
|
|
stdio: options.capture ? "pipe" : "inherit",
|
2026-05-15 01:24:10 +00:00
|
|
|
shell: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 05:12:19 +00:00
|
|
|
function assertGitReleaseState(packageVersion) {
|
|
|
|
|
const branch = run("git", ["branch", "--show-current"], { capture: true });
|
|
|
|
|
if (branch !== "main") fail(`release must be prepared from main, got ${branch || "(detached HEAD)"}`);
|
2026-05-15 01:24:10 +00:00
|
|
|
|
2026-05-20 05:12:19 +00:00
|
|
|
const status = run("git", ["status", "--short"], { capture: true });
|
|
|
|
|
if (status) fail(`working tree must be clean before release preflight\n${status}`);
|
2026-05-15 01:24:10 +00:00
|
|
|
|
2026-05-20 05:12:19 +00:00
|
|
|
const upstream = maybeRun("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
|
|
|
|
|
if (!upstream) fail("main must have an upstream tracking branch");
|
2026-05-15 01:24:10 +00:00
|
|
|
|
2026-06-20 10:15:58 +00:00
|
|
|
const upstreamAncestor = spawn("git", ["merge-base", "--is-ancestor", upstream, "HEAD"], { capture: true });
|
2026-05-20 05:12:19 +00:00
|
|
|
if (upstreamAncestor.status !== 0) fail(`HEAD must contain ${upstream}; pull or rebase before tagging`);
|
|
|
|
|
|
|
|
|
|
const existingTag = run("git", ["tag", "--list", packageVersion], { capture: true });
|
|
|
|
|
if (existingTag) fail(`local tag ${packageVersion} already exists`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function jjCommitId(revision) {
|
|
|
|
|
return run("jj", ["log", "-r", revision, "--no-graph", "-T", 'commit_id ++ "\\n"'], { capture: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function jjRevisionCount(revision) {
|
|
|
|
|
return Number(run("jj", ["log", "-r", revision, "--count"], { capture: true }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function assertJujutsuReleaseState(packageVersion) {
|
|
|
|
|
const workingCopyDiff = run("jj", ["diff", "--summary", "-r", "@"], { capture: true });
|
|
|
|
|
if (workingCopyDiff) fail(`Jujutsu working-copy commit must be empty before release preflight\n${workingCopyDiff}`);
|
|
|
|
|
|
|
|
|
|
const mainCommit = jjCommitId("main");
|
|
|
|
|
const workingCopyParent = jjCommitId("@-");
|
|
|
|
|
if (mainCommit !== workingCopyParent) {
|
|
|
|
|
fail("main bookmark must point to the release commit and be the parent of the empty working-copy commit");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const existingTag = run("jj", ["tag", "list", packageVersion], { capture: true });
|
|
|
|
|
if (existingTag) fail(`local tag ${packageVersion} already exists`);
|
|
|
|
|
|
|
|
|
|
if (jjRevisionCount("main@origin & ::main") !== 1) {
|
|
|
|
|
fail("main@origin must be an ancestor of main; fetch or rebase before tagging");
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-15 01:24:10 +00:00
|
|
|
|
|
|
|
|
const packageVersion = run("node", ["-p", "require('./package.json').version"], { capture: true });
|
2026-05-20 05:12:19 +00:00
|
|
|
if (maybeRun("jj", ["root"])) {
|
|
|
|
|
assertJujutsuReleaseState(packageVersion);
|
|
|
|
|
} else {
|
|
|
|
|
assertGitReleaseState(packageVersion);
|
|
|
|
|
}
|
2026-05-15 01:24:10 +00:00
|
|
|
|
|
|
|
|
run("npm", ["run", "release:check"]);
|
2026-06-20 00:18:17 +00:00
|
|
|
run("npm", ["run", "api:baseline"]);
|
2026-05-15 01:24:10 +00:00
|
|
|
run("npm", ["ci", "--dry-run"]);
|
2026-06-19 23:49:34 +00:00
|
|
|
run("npm", ["run", "check:ci"]);
|
2026-05-15 01:24:10 +00:00
|
|
|
|
|
|
|
|
console.log(`release preflight passed for ${packageVersion}`);
|