mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 12:58:09 +00:00
One-time maintainer-authorized bootstrap merge for the release-gate verifier policy. Exact hosted CI and all supporting workflow gates passed on 66133de419.
397 lines
12 KiB
TypeScript
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",
|
|
);
|
|
});
|
|
});
|