Files
openclaw/test/scripts/release-candidate-checklist.test.ts
Vincent Koc abb6f04e0c ci(release): harden release controls
One-time maintainer-authorized bootstrap merge for the release-gate verifier policy. Exact hosted CI and all supporting workflow gates passed on 66133de419.
2026-06-18 03:11:20 +08:00

397 lines
12 KiB
TypeScript

// Release Candidate Checklist tests cover release candidate checklist script behavior.
import { describe, expect, it, vi } from "vitest";
import {
buildPublishCommand,
candidateParallelsArgs,
candidateParallelsShellCommand,
githubApi,
parseArgs,
parseRunIdFromDispatchOutput,
resolveArtifactName,
validateFullManifest,
validateWindowsSourceRelease,
} from "../../scripts/release-candidate-checklist.mjs";
describe("release candidate checklist", () => {
it("infers validation profiles from candidate tags", () => {
expect(parseArgs(["--tag", "v2026.5.14-beta.3"]).releaseProfile).toBe("beta");
expect(parseArgs(["--tag", "v2026.5.14", "--windows-node-tag", "v0.6.3"]).releaseProfile).toBe(
"stable",
);
expect(
parseArgs([
"--tag",
"v2026.5.14",
"--windows-node-tag",
"v0.6.3",
"--release-profile",
"full",
]).releaseProfile,
).toBe("full");
});
it("runs Parallels against the exact prepared candidate tarball", () => {
expect(candidateParallelsArgs(".artifacts/preflight/openclaw.tgz")).toEqual([
"test:parallels:npm-update",
"--",
"--target-tarball",
".artifacts/preflight/openclaw.tgz",
"--json",
]);
expect(
candidateParallelsShellCommand(
".artifacts/preflight/openclaw candidate.tgz",
"/opt/homebrew/bin/gtimeout",
),
).toContain(
"set -a; source \"$HOME/.profile\" >/dev/null 2>&1 || true; set +a; exec '/opt/homebrew/bin/gtimeout' --foreground 150m pnpm",
);
expect(
candidateParallelsShellCommand(
".artifacts/preflight/openclaw candidate.tgz",
"/opt/homebrew/bin/gtimeout",
),
).toContain("'--target-tarball' '.artifacts/preflight/openclaw candidate.tgz'");
});
it("requires run ids when dispatch is disabled", () => {
expect(() => parseArgs(["--tag", "v2026.5.14-beta.3", "--skip-dispatch"])).toThrow(
"--skip-dispatch requires --full-release-run and --npm-preflight-run",
);
});
it("requires stable validation evidence to include soak and blocking performance", () => {
const stableManifest = {
workflowName: "Full Release Validation",
targetSha: "candidate-sha",
releaseProfile: "stable",
rerunGroup: "all",
runReleaseSoak: "true",
controls: { performanceBlocking: true },
};
expect(() =>
validateFullManifest(stableManifest, {
targetSha: "candidate-sha",
releaseProfile: "stable",
}),
).not.toThrow();
expect(() =>
validateFullManifest(
{
...stableManifest,
runReleaseSoak: "false",
},
{
targetSha: "candidate-sha",
releaseProfile: "stable",
},
),
).toThrow("runReleaseSoak=true");
expect(() =>
validateFullManifest(
{
...stableManifest,
controls: { performanceBlocking: false },
},
{
targetSha: "candidate-sha",
releaseProfile: "stable",
},
),
).toThrow("blocking product performance");
});
it("stops parsing options after the argument terminator", () => {
const options = parseArgs([
"--tag",
"v2026.5.14-beta.3",
"--full-release-run",
"111",
"--npm-preflight-run",
"222",
"--skip-dispatch",
"--",
"--plugin-publish-scope",
"selected",
]);
expect(options.pluginPublishScope).toBe("all-publishable");
});
it("accepts package-manager argument separators before script options", () => {
const options = parseArgs([
"--",
"--tag",
"v2026.5.14-beta.3",
"--full-release-run",
"111",
"--npm-preflight-run",
"222",
"--skip-dispatch",
"--skip-parallels",
]);
expect(options.tag).toBe("v2026.5.14-beta.3");
expect(options.skipParallels).toBe(true);
});
it("builds the gated release publish command from green evidence inputs", () => {
const options = {
...parseArgs([
"--tag",
"v2026.5.14-beta.3",
"--workflow-ref",
"release/2026.5.14",
"--full-release-run",
"111",
"--npm-preflight-run",
"222",
"--skip-dispatch",
]),
workflowRef: "release/2026.5.14",
};
expect(buildPublishCommand(options)).toContain("'full_release_validation_run_id=111'");
expect(buildPublishCommand(options)).toContain("'preflight_run_id=222'");
expect(buildPublishCommand(options)).toContain("'tag=v2026.5.14-beta.3'");
expect(buildPublishCommand(options)).toContain("'plugin_publish_scope=all-publishable'");
expect(buildPublishCommand(options)).not.toContain("windows_node_tag=");
});
it("requires and carries an exact Windows Node tag for stable release candidates", () => {
expect(() => parseArgs(["--tag", "v2026.5.14"])).toThrow(
"stable release candidates require --windows-node-tag",
);
expect(() => parseArgs(["--tag", "v2026.5.14", "--windows-node-tag", "latest"])).toThrow(
"--windows-node-tag must be an explicit version tag, not latest",
);
const options = {
...parseArgs([
"--tag",
"v2026.5.14",
"--windows-node-tag",
"v0.6.3",
"--workflow-ref",
"release/2026.5.14",
]),
workflowRef: "release/2026.5.14",
windowsNodeInstallerDigests: JSON.stringify({
"OpenClawCompanion-Setup-x64.exe": `sha256:${"a".repeat(64)}`,
"OpenClawCompanion-Setup-arm64.exe": `sha256:${"b".repeat(64)}`,
}),
};
expect(buildPublishCommand(options)).toContain("'windows_node_tag=v0.6.3'");
expect(buildPublishCommand(options)).toContain(
`'windows_node_installer_digests={"OpenClawCompanion-Setup-x64.exe":"sha256:${"a".repeat(64)}","OpenClawCompanion-Setup-arm64.exe":"sha256:${"b".repeat(64)}"}'`,
);
});
it("validates the stable Windows source release and immutable installer digests", async () => {
const assets = [
{
name: "OpenClawCompanion-Setup-x64.exe",
digest: `sha256:${"a".repeat(64)}`,
},
{
name: "OpenClawCompanion-Setup-arm64.exe",
digest: `sha256:${"b".repeat(64)}`,
},
];
const fetchImpl = vi.fn(async () => ({
ok: true,
json: async () => ({
tag_name: "v0.6.3",
draft: false,
prerelease: false,
html_url: "https://github.com/openclaw/openclaw-windows-node/releases/tag/v0.6.3",
assets,
}),
}));
await expect(
validateWindowsSourceRelease("v0.6.3", {
fetchImpl,
timeoutMs: 1234,
token: "test-token",
}),
).resolves.toEqual({
tag: "v0.6.3",
url: "https://github.com/openclaw/openclaw-windows-node/releases/tag/v0.6.3",
assets,
});
});
it.each([
[{ draft: true }, "must be published"],
[{ prerelease: true }, "must not be a prerelease"],
[{ tag_name: "v0.6.4" }, "Windows source release tag mismatch: expected v0.6.3, got v0.6.4"],
[
{ assets: [] },
"must contain exactly one required asset OpenClawCompanion-Setup-x64.exe; found 0",
],
[
{
assets: [
{
name: "OpenClawCompanion-Setup-x64.exe",
digest: `sha256:${"a".repeat(64)}`,
},
{
name: "OpenClawCompanion-Setup-x64.exe",
digest: `sha256:${"c".repeat(64)}`,
},
{
name: "OpenClawCompanion-Setup-arm64.exe",
digest: `sha256:${"b".repeat(64)}`,
},
],
},
"must contain exactly one required asset OpenClawCompanion-Setup-x64.exe; found 2",
],
[
{
assets: [
{ name: "OpenClawCompanion-Setup-x64.exe", digest: "" },
{ name: "OpenClawCompanion-Setup-arm64.exe", digest: `sha256:${"b".repeat(64)}` },
],
},
"asset OpenClawCompanion-Setup-x64.exe is missing its SHA-256 digest",
],
])("rejects an invalid stable Windows source release", async (override, message) => {
const fetchImpl = vi.fn(async () => ({
ok: true,
json: async () => ({
tag_name: "v0.6.3",
draft: false,
prerelease: false,
html_url: "https://github.com/openclaw/openclaw-windows-node/releases/tag/v0.6.3",
assets: [
{
name: "OpenClawCompanion-Setup-x64.exe",
digest: `sha256:${"a".repeat(64)}`,
},
{
name: "OpenClawCompanion-Setup-arm64.exe",
digest: `sha256:${"b".repeat(64)}`,
},
],
...override,
}),
}));
await expect(
validateWindowsSourceRelease("v0.6.3", {
fetchImpl,
timeoutMs: 1234,
token: "test-token",
}),
).rejects.toThrow(message);
});
it("carries the Telegram proof run into the publish command when available", () => {
const options = {
...parseArgs([
"--tag",
"v2026.5.14-beta.3",
"--workflow-ref",
"release/2026.5.14",
"--full-release-run",
"111",
"--npm-preflight-run",
"222",
"--skip-dispatch",
]),
workflowRef: "release/2026.5.14",
npmTelegramRunId: "333",
};
expect(buildPublishCommand(options)).toContain("'npm_telegram_run_id=333'");
});
it("requires explicit plugin names for selected plugin publish scope", () => {
expect(() =>
parseArgs(["--tag", "v2026.5.14-beta.3", "--plugin-publish-scope", "selected"]),
).toThrow("--plugin-publish-scope selected requires --plugins");
});
it("rejects selected plugin publish scope for release candidates", () => {
expect(() =>
parseArgs([
"--tag",
"v2026.5.14-beta.3",
"--plugin-publish-scope",
"selected",
"--plugins",
"@openclaw/diffs",
]),
).toThrow("release candidates publish OpenClaw with --plugin-publish-scope all-publishable");
});
it("extracts a workflow run id from gh dispatch output", () => {
expect(
parseRunIdFromDispatchOutput(
"https://github.com/openclaw/openclaw/actions/runs/25922042055\n",
),
).toBe("25922042055");
});
it("falls back to a single compatible artifact from the same run", () => {
expect(
resolveArtifactName(
[{ name: "openclaw-npm-preflight-dba00", expired: false }],
"openclaw-npm-preflight-v2026.5.16-beta.2",
"openclaw-npm-preflight-",
),
).toBe("openclaw-npm-preflight-dba00");
});
it("bounds GitHub API requests with a timeout signal", async () => {
const fetchImpl = vi.fn(async (_url: string, init?: RequestInit) => {
expect(init?.signal).toBeInstanceOf(AbortSignal);
expect(init?.headers).toMatchObject({
Accept: "application/vnd.github+json",
Authorization: "Bearer test-token",
"X-GitHub-Api-Version": "2022-11-28",
});
return {
ok: true,
json: async () => ({ workflow_runs: [] }),
};
});
await expect(
githubApi("repos/openclaw/openclaw/actions/runs", {
fetchImpl,
timeoutMs: 1234,
token: "test-token",
}),
).resolves.toEqual({ workflow_runs: [] });
expect(fetchImpl).toHaveBeenCalledWith(
"https://api.github.com/repos/openclaw/openclaw/actions/runs",
expect.objectContaining({
signal: expect.any(AbortSignal),
}),
);
});
it("includes the GitHub API path when a request times out", async () => {
const fetchImpl = vi.fn(async () => {
throw new DOMException("request timed out", "TimeoutError");
});
await expect(
githubApi("repos/openclaw/openclaw/actions/runs/123/jobs", {
fetchImpl,
timeoutMs: 5,
token: "test-token",
}),
).rejects.toThrow(
"GitHub API repos/openclaw/openclaw/actions/runs/123/jobs timed out after 5ms",
);
});
});