openclaw/scripts/openclaw-npm-extended-stable-release.mjs
Kevin Lin 9eeebf7cb1
feat(release): preflight all extended-stable npm packages (#101757)
* feat(release): preflight all extended-stable npm packages

* fix(release): install packed ai runtime in smoke

* fix(release): install sibling tarballs before core

* fix(release): verify unpublished tarballs locally

* fix(release): declare local preflight dependencies

* fix(release): pack workspace dependencies for smoke

* fix(release): accept pnpm pack artifact path

* fix(release): seed ai in sdk smoke

* fix(release): preserve prepublish verifier compatibility
2026-07-08 08:01:00 -07:00

527 lines
18 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync, spawnSync } from "node:child_process";
import { appendFileSync, readFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
import { parseReleaseVersion } from "./lib/npm-publish-plan.mjs";
const SUPPORTED_DIST_TAGS = new Set(["alpha", "beta", "latest", "extended-stable"]);
export function parseExtendedStableGuardBypass(value = "") {
if (value === "" || value === "false") {
return false;
}
if (value === "true") {
return true;
}
throw new Error(`BYPASS_EXTENDED_STABLE_GUARD must be "true" or "false"; got "${value}".`);
}
function requireExtendedStableBypassTag(npmDistTag, bypassExtendedStableGuard) {
if (bypassExtendedStableGuard && npmDistTag !== "extended-stable") {
throw new Error(
"BYPASS_EXTENDED_STABLE_GUARD may only be used with the extended-stable npm dist-tag.",
);
}
}
export function validateNpmPublishBoundary(
packageVersion,
npmDistTag,
{ bypassExtendedStableGuard = false } = {},
) {
if (!SUPPORTED_DIST_TAGS.has(npmDistTag)) {
throw new Error(`Unsupported npm dist-tag "${npmDistTag}".`);
}
requireExtendedStableBypassTag(npmDistTag, bypassExtendedStableGuard);
const parsed = parseReleaseVersion(packageVersion);
if (parsed === null) {
throw new Error(`Unsupported release version "${packageVersion}".`);
}
if (parsed.channel === "alpha") {
if (npmDistTag !== "alpha") {
throw new Error("Alpha prereleases must publish to the alpha npm dist-tag.");
}
return parsed;
}
if (parsed.channel === "beta") {
if (npmDistTag !== "beta") {
throw new Error("Beta prereleases must publish to the beta npm dist-tag.");
}
return parsed;
}
if (npmDistTag === "extended-stable") {
if (parsed.correctionNumber !== undefined) {
throw new Error("Extended-stable npm publication does not allow correction suffixes.");
}
if (!bypassExtendedStableGuard && parsed.patch < 33) {
throw new Error("Extended-stable npm publication requires release patch 33 or above.");
}
return parsed;
}
if (parsed.patch >= 33) {
throw new Error(
`Final or correction release patch 33 and above must publish to the extended-stable npm dist-tag; got ${npmDistTag}.`,
);
}
return parsed;
}
export function validateExtendedStableNpmReleaseRequest(request) {
const bypassExtendedStableGuard = request.bypassExtendedStableGuard ?? false;
requireExtendedStableBypassTag(request.npmDistTag, bypassExtendedStableGuard);
const shaPreflight =
request.preflightOnly === true && /^[0-9a-f]{40}$/iu.test(request.releaseTag);
if (shaPreflight) {
if (!/^[0-9a-f]{40}$/iu.test(request.checkoutSha)) {
throw new Error("Validation-only SHA preflight requires the full checked-out commit SHA.");
}
if (request.releaseTag.toLowerCase() !== request.checkoutSha.toLowerCase()) {
throw new Error("Validation-only SHA must match the checked-out commit exactly.");
}
}
const effectiveReleaseTag = shaPreflight ? `v${request.packageVersion}` : request.releaseTag;
const taggedVersion = effectiveReleaseTag.startsWith("v")
? parseReleaseVersion(effectiveReleaseTag.slice(1))
: null;
if (request.npmDistTag !== "extended-stable") {
if (taggedVersion !== null) {
validateNpmPublishBoundary(taggedVersion.version, request.npmDistTag, {
bypassExtendedStableGuard,
});
} else if (!SUPPORTED_DIST_TAGS.has(request.npmDistTag)) {
throw new Error(`Unsupported npm dist-tag "${request.npmDistTag}".`);
}
return { extendedStable: false };
}
if (
taggedVersion === null ||
effectiveReleaseTag !== `v${taggedVersion.version}` ||
taggedVersion.channel !== "stable"
) {
throw new Error(
"Extended-stable npm publication requires an exact final vYYYY.M.P release tag.",
);
}
validateNpmPublishBoundary(taggedVersion.version, request.npmDistTag, {
bypassExtendedStableGuard,
});
const releaseVersion = taggedVersion.version;
const extendedStableBranch = `extended-stable/${taggedVersion.year}.${taggedVersion.month}.33`;
if (request.packageVersion !== releaseVersion) {
throw new Error(
`Extended-stable npm package version mismatch: expected ${releaseVersion}, got ${request.packageVersion}.`,
);
}
if (!shaPreflight) {
const expectedWorkflowRef = `refs/heads/${extendedStableBranch}`;
if (request.npmWorkflowRef !== expectedWorkflowRef) {
throw new Error(
`Extended-stable npm workflow ref mismatch: expected ${expectedWorkflowRef}, got ${request.npmWorkflowRef}.`,
);
}
const shaValues = [request.checkoutSha, request.tagSha, request.extendedStableBranchSha];
if (shaValues.some((sha) => !/^[0-9a-f]{40}$/iu.test(sha))) {
throw new Error("Extended-stable npm release identity requires full 40-character Git SHAs.");
}
if (new Set(shaValues.map((sha) => sha.toLowerCase())).size !== 1) {
throw new Error("Extended-stable npm checkout, tag, and branch tip SHAs must match exactly.");
}
}
if (bypassExtendedStableGuard) {
return {
extendedStable: true,
releaseVersion,
extendedStableBranch,
bypassExtendedStableGuard: true,
};
}
const mainVersion = parseReleaseVersion(request.mainPackageVersion);
if (
mainVersion === null ||
mainVersion.channel !== "stable" ||
mainVersion.correctionNumber !== undefined
) {
throw new Error("Protected main package version must be an exact final YYYY.M.P version.");
}
const mainCalendarMonth = mainVersion.year * 12 + mainVersion.month;
const releaseCalendarMonth = taggedVersion.year * 12 + taggedVersion.month;
if (mainCalendarMonth <= releaseCalendarMonth) {
throw new Error(
`Protected main must be in a later calendar month than ${taggedVersion.year}.${taggedVersion.month}; got ${request.mainPackageVersion}.`,
);
}
if (mainVersion.patch >= 33) {
throw new Error("Protected main must remain on a daily patch below 33.");
}
return { extendedStable: true, releaseVersion, extendedStableBranch };
}
export function validateExtendedStableRunIdentity({
run,
kind,
npmDistTag,
expectedBranch,
expectedSha,
}) {
const expectedWorkflowName =
kind === "preflight"
? "OpenClaw NPM Release"
: kind === "plugin"
? "Plugin NPM Release"
: "Full Release Validation";
const checks = [
["workflowName", expectedWorkflowName],
["event", "workflow_dispatch"],
...(kind === "validation" || kind === "plugin" ? [["status", "completed"]] : []),
["conclusion", "success"],
...(kind === "plugin" && npmDistTag === "extended-stable"
? [["displayTitle", `Plugin NPM Release [extended-stable] ${expectedSha}`]]
: []),
];
for (const [key, expected] of checks) {
if (run[key] !== expected) {
throw new Error(
`Referenced ${kind} run must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`,
);
}
}
if (
npmDistTag === "extended-stable" &&
(run.headBranch !== expectedBranch || run.headSha !== expectedSha)
) {
throw new Error(
`Referenced extended-stable ${kind} run must have headBranch=${expectedBranch} and headSha=${expectedSha}; got ${run.headBranch ?? "<missing>"} and ${run.headSha ?? "<missing>"}.`,
);
}
return run;
}
export function validateFullReleaseValidationManifest({
manifest,
npmDistTag,
expectedWorkflowRef,
expectedSha,
}) {
if (manifest.workflowName !== "Full Release Validation") {
throw new Error(
`Full release validation manifest workflow mismatch: ${manifest.workflowName ?? "<missing>"}.`,
);
}
if (manifest.targetSha !== expectedSha) {
throw new Error(
`Full release validation target SHA mismatch: expected ${expectedSha}, got ${manifest.targetSha ?? "<missing>"}.`,
);
}
if (npmDistTag === "extended-stable" && manifest.workflowRef !== expectedWorkflowRef) {
throw new Error(
`Full release validation workflow ref mismatch: expected ${expectedWorkflowRef}, got ${manifest.workflowRef ?? "<missing>"}.`,
);
}
return manifest;
}
export function parsePriorExtendedStableSelector(stdout) {
let tags;
try {
tags = JSON.parse(stdout);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`npm dist-tags query returned invalid JSON: ${message}`, {
cause: error,
});
}
if (tags === null || typeof tags !== "object" || Array.isArray(tags)) {
throw new Error("npm dist-tags query did not return a JSON object.");
}
if (!Object.hasOwn(tags, "extended-stable")) {
return "absent";
}
if (typeof tags["extended-stable"] !== "string" || tags["extended-stable"].trim() === "") {
throw new Error("npm extended-stable dist-tag was not a non-empty version string.");
}
return tags["extended-stable"];
}
export function capturePriorExtendedStableSelector({ query }) {
const result = query();
if (result.status !== 0) {
throw new Error(`npm dist-tags query failed with exit code ${result.status ?? "unknown"}.`);
}
return parsePriorExtendedStableSelector(result.stdout);
}
export async function verifyExtendedStableRegistryReadback({
expectedVersion,
query,
sleep,
attempts = 12,
delayMs = 10_000,
}) {
let exactVersion = "missing";
let extendedStableSelector = "missing";
for (let attempt = 1; attempt <= attempts; attempt += 1) {
const exactResult = await query(`openclaw@${expectedVersion}`);
const extendedStableResult = await query("openclaw@extended-stable");
exactVersion = exactResult.status === 0 ? exactResult.stdout.trim() : "missing";
extendedStableSelector =
extendedStableResult.status === 0 ? extendedStableResult.stdout.trim() : "missing";
if (exactVersion === expectedVersion && extendedStableSelector === expectedVersion) {
return { exactVersion, extendedStableSelector, attemptsUsed: attempt };
}
if (attempt < attempts) {
await sleep(delayMs);
}
}
throw new Error(
`npm registry did not converge to openclaw@${expectedVersion} and openclaw@extended-stable=${expectedVersion} after ${attempts} attempts (exact=${exactVersion}, extended-stable=${extendedStableSelector}).`,
);
}
export function extendedStableSelectorRepairCommand(expectedVersion) {
const normalizedVersion = (expectedVersion ?? "").replace(/^v/u, "");
const parsed = parseReleaseVersion(normalizedVersion);
if (parsed === null || parsed.channel !== "stable" || parsed.correctionNumber !== undefined) {
throw new Error("Extended-stable selector repair requires an exact final YYYY.M.P version.");
}
return `npm dist-tag add openclaw@${parsed.version} extended-stable`;
}
function git(args) {
return execFileSync("git", args, { encoding: "utf8" }).trim();
}
function packageVersionAt(ref) {
return JSON.parse(git(["show", `${ref}:package.json`])).version;
}
function validateRequestFromRepository() {
const npmDistTag = process.env.RELEASE_NPM_DIST_TAG ?? "";
const releaseTag = process.env.RELEASE_TAG ?? "";
const npmWorkflowRef = process.env.NPM_WORKFLOW_REF ?? "";
const preflightOnly = process.env.PREFLIGHT_ONLY === "true";
const bypassExtendedStableGuard = parseExtendedStableGuardBypass(
process.env.BYPASS_EXTENDED_STABLE_GUARD ?? "",
);
const packageVersion = JSON.parse(readFileSync("package.json", "utf8")).version;
if (npmDistTag !== "extended-stable") {
return validateExtendedStableNpmReleaseRequest({
bypassExtendedStableGuard,
npmDistTag,
releaseTag,
npmWorkflowRef,
checkoutSha: "",
tagSha: "",
extendedStableBranchSha: "",
packageVersion,
mainPackageVersion: "",
});
}
const shaPreflight = preflightOnly && /^[0-9a-f]{40}$/iu.test(releaseTag);
const effectiveReleaseTag = shaPreflight ? `v${packageVersion}` : releaseTag;
const parsed = effectiveReleaseTag.startsWith("v")
? parseReleaseVersion(effectiveReleaseTag.slice(1))
: null;
if (!parsed || parsed.channel !== "stable" || parsed.correctionNumber !== undefined) {
return validateExtendedStableNpmReleaseRequest({
npmDistTag,
bypassExtendedStableGuard,
releaseTag,
preflightOnly,
npmWorkflowRef,
checkoutSha: "",
tagSha: "",
extendedStableBranchSha: "",
packageVersion,
mainPackageVersion: "",
});
}
const extendedStableBranch = `extended-stable/${parsed.year}.${parsed.month}.33`;
if (shaPreflight) {
if (!bypassExtendedStableGuard) {
execFileSync(
"git",
["fetch", "--no-tags", "origin", "+refs/heads/main:refs/remotes/origin/main"],
{ stdio: "inherit" },
);
}
const checkoutSha = git(["rev-parse", "HEAD"]);
return validateExtendedStableNpmReleaseRequest({
npmDistTag,
bypassExtendedStableGuard,
releaseTag,
preflightOnly,
npmWorkflowRef,
checkoutSha,
tagSha: checkoutSha,
extendedStableBranchSha: "",
packageVersion,
mainPackageVersion: bypassExtendedStableGuard
? ""
: packageVersionAt("refs/remotes/origin/main"),
});
}
if (bypassExtendedStableGuard) {
execFileSync(
"git",
[
"fetch",
"--no-tags",
"origin",
`+refs/heads/${extendedStableBranch}:refs/remotes/origin/${extendedStableBranch}`,
],
{ stdio: "inherit" },
);
execFileSync(
"git",
["fetch", "--no-tags", "origin", `+refs/tags/${releaseTag}:refs/tags/${releaseTag}`],
{ stdio: "inherit" },
);
return validateExtendedStableNpmReleaseRequest({
npmDistTag,
bypassExtendedStableGuard,
releaseTag,
npmWorkflowRef,
checkoutSha: git(["rev-parse", "HEAD"]),
tagSha: git(["rev-parse", `${releaseTag}^{commit}`]),
extendedStableBranchSha: git(["rev-parse", `refs/remotes/origin/${extendedStableBranch}`]),
packageVersion,
mainPackageVersion: "",
});
}
execFileSync(
"git",
[
"fetch",
"--no-tags",
"origin",
`+refs/heads/${extendedStableBranch}:refs/remotes/origin/${extendedStableBranch}`,
"+refs/heads/main:refs/remotes/origin/main",
],
{ stdio: "inherit" },
);
execFileSync(
"git",
["fetch", "--no-tags", "origin", `+refs/tags/${releaseTag}:refs/tags/${releaseTag}`],
{ stdio: "inherit" },
);
return validateExtendedStableNpmReleaseRequest({
npmDistTag,
bypassExtendedStableGuard,
releaseTag,
npmWorkflowRef,
checkoutSha: git(["rev-parse", "HEAD"]),
tagSha: git(["rev-parse", `${releaseTag}^{commit}`]),
extendedStableBranchSha: git(["rev-parse", `refs/remotes/origin/${extendedStableBranch}`]),
packageVersion,
mainPackageVersion: packageVersionAt("refs/remotes/origin/main"),
});
}
function appendOutput(values) {
const output = process.env.GITHUB_OUTPUT;
if (!output) {
throw new Error("GITHUB_OUTPUT is required.");
}
appendFileSync(
output,
Object.entries(values)
.map(([key, value]) => `${key}=${value}\n`)
.join(""),
);
}
async function main() {
const command = process.argv[2];
if (command === "validate-request") {
const result = validateRequestFromRepository();
console.log(
result.extendedStable
? `Validated extended-stable npm release ${result.releaseVersion}.`
: "Validated regular npm release request.",
);
return;
}
if (command === "publish-plan") {
const npmDistTag = process.env.REQUESTED_PUBLISH_TAG ?? "";
const bypassExtendedStableGuard = parseExtendedStableGuardBypass(
process.env.BYPASS_EXTENDED_STABLE_GUARD ?? "",
);
const parsed = validateNpmPublishBoundary(process.env.PACKAGE_VERSION ?? "", npmDistTag, {
bypassExtendedStableGuard,
});
console.log(parsed.channel);
console.log(npmDistTag);
return;
}
if (command === "verify-run") {
const run = JSON.parse(readFileSync(0, "utf8"));
validateExtendedStableRunIdentity({
run,
kind: process.env.RUN_KIND,
npmDistTag: process.env.RELEASE_NPM_DIST_TAG,
expectedBranch: process.env.EXPECTED_EXTENDED_STABLE_BRANCH,
expectedSha: process.env.EXPECTED_RELEASE_SHA,
});
console.log(`Verified referenced ${process.env.RUN_KIND} run.`);
return;
}
if (command === "verify-manifest") {
const manifest = JSON.parse(readFileSync(process.env.MANIFEST_FILE, "utf8"));
validateFullReleaseValidationManifest({
manifest,
npmDistTag: process.env.RELEASE_NPM_DIST_TAG,
expectedWorkflowRef: process.env.EXPECTED_WORKFLOW_REF,
expectedSha: process.env.EXPECTED_RELEASE_SHA,
});
return;
}
if (command === "capture-selector") {
const previous = capturePriorExtendedStableSelector({
query: () =>
spawnSync("npm", ["view", "openclaw", "dist-tags", "--json"], { encoding: "utf8" }),
});
appendOutput({ previous });
return;
}
if (command === "verify-readback") {
const expectedVersion = (process.env.EXPECTED_VERSION ?? "").replace(/^v/u, "");
const result = await verifyExtendedStableRegistryReadback({
expectedVersion,
query: (target) => spawnSync("npm", ["view", target, "version"], { encoding: "utf8" }),
sleep: (delayMs) =>
new Promise((resolve) => {
setTimeout(resolve, delayMs);
}),
});
appendOutput({
exact_version: result.exactVersion,
extended_stable_selector: result.extendedStableSelector,
});
return;
}
if (command === "repair-command") {
console.log(extendedStableSelectorRepairCommand(process.env.EXPECTED_VERSION));
return;
}
throw new Error(`Unknown extended-stable npm release command: ${command ?? "<missing>"}.`);
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
try {
await main();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`openclaw-npm-extended-stable-release: ${message}`);
process.exit(1);
}
}