fix(release): keep frozen validation independent of main (#126622)

* fix(release): freeze validation tooling identity

* fix(release): enforce frozen validation contract

* fix(release): validate candidate identity in parent

* fix(ci): close release isolation gate findings
This commit is contained in:
Vincent Koc
2026-08-20 04:32:38 -07:00
committed by GitHub
parent a59abcf4a8
commit c28c279afa
17 changed files with 851 additions and 156 deletions
+1 -1
View File
@@ -8515,7 +8515,7 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
(step: WorkflowStep) => step.name === "Dispatch and await trusted Telegram QA",
);
const identityStep = telegramWorkflow.jobs.trusted_identity.steps.find(
(step: WorkflowStep) => step.name === "Verify dispatched-main identity",
(step: WorkflowStep) => step.name === "Verify dispatched workflow identity",
);
const provenanceSteps = [
telegramWorkflow.jobs.build_candidate.steps.find(
@@ -13,9 +13,26 @@ import {
releaseEvidenceVerifierPath,
resolveRemoteTargetRefSha,
shouldDeleteTemporaryWorkflowRef,
verifyTargetRef,
} from "../../scripts/full-release-validation-at-sha.mts";
const SCRIPT_PATH = resolve("scripts/full-release-validation-at-sha.mjs");
const CURRENT_WORKFLOW_SOURCE = `name: Full Release Validation
env:
RELEASE_ISOLATION_TOOLING_CONTRACT: "1"
on:
workflow_dispatch:
inputs:
expected_sha:
required: false
`;
const LEGACY_WORKFLOW_SOURCE = `name: Full Release Validation
on:
workflow_dispatch:
inputs:
expected_sha:
required: false
`;
function runGit(cwd: string, args: string[]): string {
return execFileSync("git", args, {
@@ -44,17 +61,10 @@ function createDispatchFixture(options: { workflowSource?: string } = {}) {
runGit(checkout, ["config", "user.name", "OpenClaw Release Test"]);
mkdirSync(join(checkout, ".github", "workflows"), { recursive: true });
mkdirSync(join(checkout, "scripts"), { recursive: true });
writeFileSync(join(checkout, "package.json"), '{"version":"2026.8.1"}\n');
writeFileSync(join(checkout, "package.json"), '{"version":"2026.7.9"}\n');
writeFileSync(
join(checkout, ".github", "workflows", "full-release-validation.yml"),
options.workflowSource ??
`name: Full Release Validation
on:
workflow_dispatch:
inputs:
expected_sha:
required: false
`,
LEGACY_WORKFLOW_SOURCE,
);
writeFileSync(
join(checkout, "scripts", "release-ci-summary.mjs"),
@@ -73,7 +83,15 @@ console.log(JSON.stringify({ valid: true, current: { runId: "123" }, root: { run
`,
);
runGit(checkout, ["add", "."]);
runGit(checkout, ["commit", "-m", "test: trusted workflow"]);
runGit(checkout, ["commit", "-m", "test: legacy workflow"]);
const oldWorkflowSha = runGit(checkout, ["rev-parse", "HEAD"]);
writeFileSync(
join(checkout, ".github", "workflows", "full-release-validation.yml"),
options.workflowSource ?? CURRENT_WORKFLOW_SOURCE,
);
writeFileSync(join(checkout, "package.json"), '{"version":"2026.8.1"}\n');
runGit(checkout, ["add", ".github/workflows/full-release-validation.yml", "package.json"]);
runGit(checkout, ["commit", "-m", "test: trusted workflow contract"]);
const workflowSha = runGit(checkout, ["rev-parse", "HEAD"]);
runGit(checkout, ["remote", "add", "origin", origin]);
runGit(checkout, ["push", "-u", "origin", "main"]);
@@ -151,6 +169,7 @@ if (args[0] === "workflow" && args[1] === "run") {
ghCallsPath,
gitCallsPath,
origin,
oldWorkflowSha,
readCalls,
releaseRef,
run,
@@ -166,7 +185,7 @@ describe("full-release-validation-at-sha", () => {
"--sha",
"abc123",
"--workflow-sha",
"origin/main",
"a".repeat(40),
"--target-ref",
"release/2026.7.1",
"--keep-branch",
@@ -187,7 +206,7 @@ describe("full-release-validation-at-sha", () => {
},
sha: "abc123",
targetRef: "release/2026.7.1",
workflowSha: "origin/main",
workflowSha: "a".repeat(40),
});
});
@@ -224,14 +243,21 @@ describe("full-release-validation-at-sha", () => {
});
it("accepts only canonical release branch or tag context", () => {
expect(parseArgs(["--target-ref", "extended-stable/2026.6.33"]).targetRef).toBe(
"extended-stable/2026.6.33",
);
expect(
parseArgs(["--target-ref", "extended-stable/2026.6.33", "--workflow-sha", "a".repeat(40)])
.targetRef,
).toBe("extended-stable/2026.6.33");
expect(parseArgs(["--target-ref", "v2026.7.1-beta.5"]).targetRef).toBe("v2026.7.1-beta.5");
expect(parseArgs(["--target-ref", "v2026.7.1"]).targetRef).toBe("v2026.7.1");
expect(() => parseArgs(["--target-ref", "feature/not-release"])).toThrow(
"canonical OpenClaw release branch or tag",
);
expect(() => parseArgs(["--target-ref", "release/2026.7.1"])).toThrow(
"requires --workflow-sha with an explicit full Tooling SHA",
);
expect(() =>
parseArgs(["--target-ref", "release/2026.7.1", "--workflow-sha", "origin/main"]),
).toThrow("explicit full Tooling SHA");
});
it("resolves annotated release tags through their peeled commit", () => {
@@ -259,6 +285,92 @@ describe("full-release-validation-at-sha", () => {
]);
});
it("binds frozen release candidates to the branch or tag package version", () => {
const candidateSha = "a".repeat(40);
const branchTipSha = "b".repeat(40);
expect(
verifyTargetRef(
"release/2026.7.1",
candidateSha,
"2026.7.1-beta.5",
() => branchTipSha,
(ancestor, descendant) => ancestor === candidateSha && descendant === branchTipSha,
),
).toBe("release/2026.7.1");
expect(() =>
verifyTargetRef(
"release/2026.7.1",
candidateSha,
"2026.7.1-alpha.5",
() => branchTipSha,
() => true,
),
).toThrow("expected 2026.7.1 or a beta prerelease of it");
expect(() =>
verifyTargetRef(
"release/2026.7.1",
candidateSha,
"2026.7.1",
() => branchTipSha,
() => false,
),
).toThrow("is not reachable from release branch");
expect(() =>
verifyTargetRef(
"release/2026.7.1",
candidateSha,
"2026.6.9",
() => branchTipSha,
() => true,
),
).toThrow("does not belong to release branch");
expect(
verifyTargetRef(
"extended-stable/2026.6.33",
candidateSha,
"2026.6.33",
() => branchTipSha,
() => true,
),
).toBe("extended-stable/2026.6.33");
expect(() =>
verifyTargetRef(
"extended-stable/2026.6.33",
candidateSha,
"2026.6.33-beta.1",
() => branchTipSha,
() => true,
),
).toThrow("does not match extended-stable branch");
expect(
verifyTargetRef(
"v2026.7.1-beta.5",
candidateSha,
"2026.7.1-beta.5",
() => candidateSha,
() => false,
),
).toBe("v2026.7.1-beta.5");
expect(() =>
verifyTargetRef(
"v2026.7.1-beta.5",
candidateSha,
"2026.7.1-beta.5",
() => branchTipSha,
() => true,
),
).toThrow("does not resolve");
expect(() =>
verifyTargetRef(
"v2026.7.1-beta.5",
candidateSha,
"2026.7.1-beta.4",
() => candidateSha,
() => true,
),
).toThrow("does not match release tag");
});
it("allows exact-target reuse to be disabled for a forced fresh run", () => {
expect(parseArgs(["-f", "reuse_evidence=false"]).inputs.reuse_evidence).toBe("false");
expect(() => parseArgs(["-f", "reuse_evidence=maybe"])).toThrow(
@@ -308,11 +420,13 @@ describe("full-release-validation-at-sha", () => {
const source = readFileSync("scripts/full-release-validation-at-sha.mts", "utf8");
expect(FULL_RELEASE_WAIT_TIMEOUT_MINUTES).toBe(720);
expect(FULL_RELEASE_WAIT_POLL_INTERVAL_MS).toBe(45_000);
expect(source).toContain("const FULL_RELEASE_PROGRESS_INTERVAL_MS = 5 * 60_000;");
expect(source).toContain("workflowRun.head_sha !== workflowSha");
expect(source).toContain("return suite;");
expect(source).toContain("Date.now() + FULL_RELEASE_WAIT_TIMEOUT_MINUTES * 60_000");
expect(source).toContain("startedAt + FULL_RELEASE_WAIT_TIMEOUT_MINUTES * 60_000");
expect(source).toContain("const remainingMs = deadline - Date.now();");
expect(source).toContain("Math.min(FULL_RELEASE_WAIT_POLL_INTERVAL_MS, remainingMs)");
expect(source).toContain("Parent run progress after ${elapsedMinutes}m");
expect(source).toContain(
"Timed out after ${FULL_RELEASE_WAIT_TIMEOUT_MINUTES} minutes waiting for Full Release Validation",
);
@@ -322,7 +436,7 @@ describe("full-release-validation-at-sha", () => {
it("bounds GitHub reads without applying a timeout to workflow dispatch", () => {
const source = readFileSync("scripts/full-release-validation-at-sha.mts", "utf8");
expect(source).toContain("timeout: GH_READ_TIMEOUT_MS");
expect(source.match(/GH_READ_OPTIONS/gu)).toHaveLength(3);
expect(source.match(/GH_READ_OPTIONS/gu)).toHaveLength(4);
expect(source).toContain('const dispatchOutput = run("gh", dispatchArgs');
});
@@ -337,7 +451,7 @@ describe("full-release-validation-at-sha", () => {
checked.push(relativePath);
return relativePath === workflowPath || relativePath === verifierPath;
},
() => "on:\n workflow_dispatch:\n inputs:\n expected_sha: {}\n",
() => CURRENT_WORKFLOW_SOURCE,
),
).toBe(verifierPath);
expect(checked).toEqual([workflowPath, verifierPath]);
@@ -346,14 +460,22 @@ describe("full-release-validation-at-sha", () => {
assertTrustedWorkflowHarness(
"a".repeat(40),
(relativePath) => relativePath === workflowPath,
() => "on:\n workflow_dispatch:\n inputs:\n expected_sha: {}\n",
() => CURRENT_WORKFLOW_SOURCE,
),
).toThrow("supported release evidence verifier");
expect(() =>
assertTrustedWorkflowHarness(
"b".repeat(40),
() => true,
() => "on:\n workflow_dispatch:\n inputs: {}\n",
() => LEGACY_WORKFLOW_SOURCE,
),
).toThrow("does not declare RELEASE_ISOLATION_TOOLING_CONTRACT=1");
expect(() =>
assertTrustedWorkflowHarness(
"b".repeat(40),
() => true,
() =>
'env:\n RELEASE_ISOLATION_TOOLING_CONTRACT: "1"\non:\n workflow_dispatch:\n inputs: {}\n',
),
).toThrow(`Tooling SHA ${"b".repeat(40)} is missing workflow_dispatch input expected_sha`);
});
@@ -396,7 +518,7 @@ describe("full-release-validation-at-sha", () => {
it("pushes an exact target ref, dispatches it, prints the run URL, and cleans both refs", () => {
const fixture = createDispatchFixture();
try {
const result = fixture.run();
const result = fixture.run(["--workflow-sha", fixture.workflowSha]);
expect(result.status, result.stderr).toBe(0);
const gitCalls = fixture.readCalls(fixture.gitCallsPath);
const ghCalls = fixture.readCalls(fixture.ghCallsPath);
@@ -451,6 +573,9 @@ describe("full-release-validation-at-sha", () => {
expect(ghCalls.some((args) => args[0] === "run" && args[1] === "watch")).toBe(false);
expect(result.stdout).toContain(`Validation SHA: ${fixture.targetSha}`);
expect(result.stdout).toContain(`Tooling SHA: ${fixture.workflowSha}`);
expect(result.stdout).toContain(
`Frozen validation tuple: candidate=${fixture.targetSha} tooling=${fixture.workflowSha} rerun_group=all`,
);
expect(result.stdout).toContain(
"Parent run: https://github.com/openclaw/openclaw/actions/runs/123",
);
@@ -473,7 +598,8 @@ describe("full-release-validation-at-sha", () => {
it("rejects pinned old-schema tooling before either remote ref is pushed", () => {
const fixture = createDispatchFixture({
workflowSource: "name: Full Release Validation\non:\n workflow_dispatch:\n",
workflowSource:
'name: Full Release Validation\nenv:\n RELEASE_ISOLATION_TOOLING_CONTRACT: "1"\non:\n workflow_dispatch:\n',
});
try {
const result = fixture.run(["--workflow-sha", fixture.workflowSha]);
@@ -489,10 +615,48 @@ describe("full-release-validation-at-sha", () => {
}
});
it("rejects pinned pre-contract tooling before either remote ref is pushed", () => {
const fixture = createDispatchFixture();
try {
const result = fixture.run(["--workflow-sha", fixture.oldWorkflowSha]);
expect(result.status).toBe(1);
expect(result.stderr).toContain(`Tooling SHA ${fixture.oldWorkflowSha}`);
expect(result.stderr).toContain("does not declare RELEASE_ISOLATION_TOOLING_CONTRACT=1");
expect(fixture.readCalls(fixture.gitCallsPath).filter((args) => args[0] === "push")).toEqual(
[],
);
expect(readFileSync(fixture.ghCallsPath, "utf8")).toBe("");
} finally {
fixture.cleanup();
}
});
it("rejects an arbitrary older release-branch ancestor with the wrong package version", () => {
const fixture = createDispatchFixture();
try {
const result = fixture.run([
"--sha",
fixture.oldWorkflowSha,
"--workflow-sha",
fixture.workflowSha,
]);
expect(result.status).toBe(1);
expect(result.stderr).toContain(
"Target package version 2026.7.9 does not belong to release branch release/2026.8.1; expected 2026.8.1 or a beta prerelease of it",
);
expect(fixture.readCalls(fixture.gitCallsPath).filter((args) => args[0] === "push")).toEqual(
[],
);
expect(readFileSync(fixture.ghCallsPath, "utf8")).toBe("");
} finally {
fixture.cleanup();
}
});
it("keeps both temporary refs with --keep-branch", () => {
const fixture = createDispatchFixture();
try {
const result = fixture.run(["--keep-branch"]);
const result = fixture.run(["--workflow-sha", fixture.workflowSha, "--keep-branch"]);
expect(result.status, result.stderr).toBe(0);
const gitCalls = fixture.readCalls(fixture.gitCallsPath);
expect(
@@ -524,7 +688,15 @@ describe("full-release-validation-at-sha", () => {
const missingSha = "f".repeat(40);
const result = spawnSync(
process.execPath,
[SCRIPT_PATH, "--sha", missingSha, "--target-ref", fixture.releaseRef],
[
SCRIPT_PATH,
"--sha",
missingSha,
"--target-ref",
fixture.releaseRef,
"--workflow-sha",
fixture.workflowSha,
],
{
cwd: fixture.checkout,
encoding: "utf8",
@@ -83,17 +83,18 @@ function runIdentityVerification(params: {
oidcJobWorkflowSha?: string;
oidcWorkflowSha?: string;
targetContextRef?: string;
workflowBranch?: string;
workflowSha?: string;
}) {
const repository = "openclaw/openclaw";
const trustedWorkflowRef = `${repository}/.github/workflows/openclaw-release-telegram-qa.yml@refs/heads/main`;
const workflowBranch = params.workflowBranch ?? "main";
const workflowRefName = `refs/heads/${workflowBranch}`;
const trustedWorkflowRef = `${repository}/.github/workflows/openclaw-release-telegram-qa.yml@${workflowRefName}`;
const invocation = params.invocation ?? "dispatch";
const workflowRef =
invocation === "dispatch"
? trustedWorkflowRef
: `${repository}/.github/workflows/openclaw-release-checks.yml@refs/heads/release-ci/test`;
const workflowRefName =
invocation === "dispatch" ? "refs/heads/main" : "refs/heads/release-ci/test";
: `${repository}/.github/workflows/openclaw-release-checks.yml@${workflowRefName}`;
const workdir = tempDirs.make("openclaw-telegram-identity-");
const fakeBin = join(workdir, "bin");
const githubOutput = join(workdir, "github-output");
@@ -128,7 +129,7 @@ function runIdentityVerification(params: {
);
return spawnSync(
"bash",
["-c", requireRun("trusted_identity", "Verify dispatched-main identity")],
["-c", requireRun("trusted_identity", "Verify dispatched workflow identity")],
{
cwd: workdir,
encoding: "utf8",
@@ -371,7 +372,7 @@ describe("release Telegram QA workflow", () => {
"runs-on": "ubuntu-24.04",
"timeout-minutes": 5,
});
expect(step("trusted_identity", "Verify dispatched-main identity").id).toBe("identity");
expect(step("trusted_identity", "Verify dispatched workflow identity").id).toBe("identity");
const candidateBuild = requireRun(
"build_candidate",
@@ -388,6 +389,14 @@ describe("release Telegram QA workflow", () => {
expect(requireRun("run_telegram", "Build trusted QA harness").trim()).toBe(
"pnpm build qaRuntime",
);
const extractCandidate = step("run_telegram", "Verify attestation and bounded extract");
expect(extractCandidate.env?.CALLED_WORKFLOW_REF).toBe(
"${{ needs.trusted_identity.outputs.workflow_ref }}",
);
expect(extractCandidate.run).toContain(
'--cert-identity "https://github.com/${CALLED_WORKFLOW_REF}"',
);
expect(extractCandidate.run).not.toContain("openclaw-release-telegram-qa.yml@refs/heads/main");
const runJob = job("run_telegram");
expect(runJob.environment).toBe("qa-live-shared");
@@ -410,9 +419,28 @@ describe("release Telegram QA workflow", () => {
}
});
it("accepts only the resolved trusted workflow identity", () => {
it("routes every documented workflow ref through exact direct and reusable identity", () => {
const trustedSha = "b".repeat(40);
const releaseCiBranch = `release-ci/${trustedSha.slice(0, 12)}-1787215404735`;
for (const workflowBranch of [
"main",
"release/2026.7.1",
"extended-stable/2026.7.33",
releaseCiBranch,
]) {
for (const invocation of ["dispatch", "reusable"] as const) {
const result = runIdentityVerification({
expectedTrustedWorkflowSha: trustedSha,
invocation,
workflowBranch,
});
expect(result.status, `${workflowBranch}/${invocation}: ${result.stderr}`).toBe(0);
}
}
});
it("accepts only canonical exact-SHA workflow and target identities", () => {
const trustedSha = "b".repeat(40);
expect(runIdentityVerification({ expectedTrustedWorkflowSha: trustedSha }).status).toBe(0);
for (const targetContextRef of [
"release/2026.7.1",
"extended-stable/2026.7.33",
@@ -438,6 +466,31 @@ describe("release Telegram QA workflow", () => {
oidcJobWorkflowSha: "c".repeat(40),
}).stderr,
).toContain("OIDC job_workflow_sha mismatch");
expect(
runIdentityVerification({
expectedTrustedWorkflowSha: trustedSha,
workflowBranch: "release-ci/not-canonical",
}).stderr,
).toContain("must be exact main, canonical release or extended-stable");
expect(
runIdentityVerification({
expectedTrustedWorkflowSha: trustedSha,
workflowBranch: `release-ci/${"c".repeat(12)}-1787215404735`,
}).stderr,
).toContain("release-ci ref does not match the authorized tooling SHA");
for (const workflowBranch of [
"release/2026.0.1",
"release/2026.07.1",
"extended-stable/2026.13.33",
"extended-stable/2026.7.32",
]) {
expect(
runIdentityVerification({
expectedTrustedWorkflowSha: trustedSha,
workflowBranch,
}).stderr,
).toContain("must be exact main, canonical release or extended-stable");
}
});
it("accepts trusted release provenance and rejects same-repository PR heads", () => {
@@ -176,6 +176,10 @@ type WorkflowJob = {
};
type Workflow = {
concurrency?: {
group?: string;
"cancel-in-progress"?: boolean | string;
};
env?: Record<string, string>;
jobs?: Record<string, WorkflowJob>;
on?: {
@@ -276,12 +280,88 @@ function runFullReleaseInputValidation(releaseProfile: string, skipTelegram: str
workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "resolve_target"),
"Validate release inputs",
);
const workdir = tempDirs.make("full-release-input-validation-");
mkdirSync(resolve(workdir, "target"));
writeFileSync(resolve(workdir, "target", "package.json"), '{"version":"2026.8.1"}\n', "utf8");
return spawnSync("bash", ["-c", step.run ?? ""], {
cwd: workdir,
encoding: "utf8",
env: {
PATH: process.env.PATH,
RELEASE_PROFILE: releaseProfile,
SKIP_PACKAGE_TELEGRAM_E2E: skipTelegram,
TARGET_CONTEXT_REF: "",
TARGET_REF: "main",
},
});
}
function runFullReleaseTargetIdentityValidation(params: {
comparisonStatus?: string;
remoteSha?: string;
targetContextRef?: string;
targetRef: string;
version: string;
}) {
const step = workflowStep(
workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "resolve_target"),
"Validate release inputs",
);
const workdir = tempDirs.make("full-release-target-identity-");
const fakeBin = resolve(workdir, "bin");
mkdirSync(fakeBin);
mkdirSync(resolve(workdir, "target"));
writeFileSync(
resolve(workdir, "target", "package.json"),
`${JSON.stringify({ version: params.version })}\n`,
"utf8",
);
writeFileSync(
resolve(fakeBin, "git"),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$*" == *"ls-remote"* ]]; then
printf '%s\\t%s\\n' "$FAKE_REMOTE_SHA" "$FAKE_REMOTE_REF"
exit 0
fi
exit 64
`,
{ mode: 0o755 },
);
writeFileSync(
resolve(fakeBin, "gh"),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$*" == *"api repos/"*"/compare/"* ]]; then
printf '%s\\n' "$FAKE_COMPARISON_STATUS"
exit 0
fi
exit 64
`,
{ mode: 0o755 },
);
const targetSha = params.targetRef.match(/^[a-f0-9]{40}$/u)?.[0] ?? "a".repeat(40);
const normalizedContextRef = (params.targetContextRef ?? params.targetRef)
.replace(/^refs\/heads\//u, "")
.replace(/^refs\/tags\//u, "");
const remoteRef = normalizedContextRef.startsWith("v")
? `refs/tags/${normalizedContextRef}`
: `refs/heads/${normalizedContextRef}`;
return spawnSync("bash", ["-c", step.run ?? ""], {
cwd: workdir,
encoding: "utf8",
env: {
FAKE_COMPARISON_STATUS: params.comparisonStatus ?? "ahead",
FAKE_REMOTE_REF: remoteRef,
FAKE_REMOTE_SHA: params.remoteSha ?? targetSha,
GH_TOKEN: "test-token",
GITHUB_REPOSITORY: "openclaw/openclaw",
PATH: `${fakeBin}:${process.env.PATH}`,
RELEASE_PROFILE: "beta",
SKIP_PACKAGE_TELEGRAM_E2E: "false",
TARGET_CONTEXT_REF: params.targetContextRef ?? "",
TARGET_REF: params.targetRef,
TARGET_SHA: targetSha,
},
});
}
@@ -2274,6 +2354,12 @@ describe("package acceptance workflow", () => {
expect(readFileSync(NPM_TELEGRAM_WORKFLOW, "utf8")).toContain(
"format('NPM Telegram Beta E2E {0}', inputs.dispatch_id)",
);
expect(readWorkflow(PLUGIN_PRERELEASE_WORKFLOW).concurrency?.group).toBe(
"plugin-prerelease-${{ inputs.target_ref }}-${{ github.sha }}",
);
expect(readWorkflow(RELEASE_CHECKS_WORKFLOW).concurrency?.group).toBe(
"openclaw-release-checks-${{ inputs.expected_sha || inputs.ref }}-${{ github.sha }}-${{ inputs.rerun_group }}",
);
});
it.each(FULL_RELEASE_CHILD_DISPATCHES)(
@@ -3763,6 +3849,73 @@ describe("package artifact reuse", () => {
expect(umbrella.status, umbrella.stderr).toBe(0);
});
it.each([
["release/2026.8.1", "2026.8.1"],
["release/2026.8.1", "2026.8.1-beta.3"],
["extended-stable/2026.7.33", "2026.7.33"],
["v2026.8.1", "2026.8.1"],
["v2026.8.1-alpha.2", "2026.8.1-alpha.2"],
["v2026.8.1-beta.3", "2026.8.1-beta.3"],
])("accepts direct Full Release Validation identity %s at package %s", (targetRef, version) => {
const result = runFullReleaseTargetIdentityValidation({ targetRef, version });
expect(result.status, result.stderr).toBe(0);
});
it.each([
["release/2026.8.1", "2026.8.2", "does not belong to release branch"],
["release/2026.8.1", "2026.8.1-alpha.2", "expected 2026.8.1 or a beta prerelease"],
["extended-stable/2026.7.33", "2026.7.33-beta.1", "does not match extended-stable branch"],
["v2026.8.1", "2026.8.1-beta.1", "does not match release tag"],
["v2026.8.1-alpha.2", "2026.8.1-alpha.3", "does not match release tag"],
])(
"rejects direct Full Release Validation identity %s at package %s",
(targetRef, version, error) => {
const result = runFullReleaseTargetIdentityValidation({ targetRef, version });
expect(result.status).toBe(1);
expect(result.stderr).toContain(error);
},
);
it("validates an exact-SHA helper target against its canonical release context", () => {
const accepted = runFullReleaseTargetIdentityValidation({
targetContextRef: "release/2026.8.1",
targetRef: "a".repeat(40),
version: "2026.8.1-beta.3",
});
const rejected = runFullReleaseTargetIdentityValidation({
targetContextRef: "release/2026.8.1",
targetRef: "a".repeat(40),
version: "2026.8.1-alpha.3",
});
expect(accepted.status, accepted.stderr).toBe(0);
expect(rejected.status).toBe(1);
expect(rejected.stderr).toContain("expected 2026.8.1 or a beta prerelease");
});
it("rejects exact-SHA release contexts outside the named branch or tag", () => {
const divergedBranch = runFullReleaseTargetIdentityValidation({
comparisonStatus: "diverged",
remoteSha: "b".repeat(40),
targetContextRef: "release/2026.8.1",
targetRef: "a".repeat(40),
version: "2026.8.1-beta.3",
});
const mismatchedTag = runFullReleaseTargetIdentityValidation({
remoteSha: "b".repeat(40),
targetContextRef: "v2026.8.1-alpha.2",
targetRef: "a".repeat(40),
version: "2026.8.1-alpha.2",
});
expect(divergedBranch.status).toBe(1);
expect(divergedBranch.stderr).toContain("is not reachable from release context branch");
expect(mismatchedTag.status).toBe(1);
expect(mismatchedTag.stderr).toContain("does not match release tag");
});
it.each(["stable", "full"])(
"preserves normal %s validation when Telegram deferral is false",
(releaseProfile) => {
@@ -4800,12 +4953,18 @@ describe("package artifact reuse", () => {
const workflowInputs = readWorkflow(FULL_RELEASE_VALIDATION_WORKFLOW).on?.workflow_dispatch
?.inputs;
const resolveTargetJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "resolve_target");
const resolveTargetSteps = resolveTargetJob.steps ?? [];
const evidenceReuseJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "evidence_reuse");
const releaseChecksJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "release_checks");
const npmTelegramJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "npm_telegram");
const performanceJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "performance");
const summaryJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "summary");
const targetSummaryStep = workflowStep(resolveTargetJob, "Summarize target");
const targetManifestCheckout = workflowStep(
resolveTargetJob,
"Checkout target package manifest",
);
const releaseInputValidation = workflowStep(resolveTargetJob, "Validate release inputs");
const evidenceReuseStep = workflowStep(evidenceReuseJob, "Find reusable validation evidence");
const releaseChecksDispatchStep = workflowStep(
releaseChecksJob,
@@ -4823,6 +4982,24 @@ describe("package artifact reuse", () => {
});
expect(workflow).toContain("CHILD_WORKFLOW_REF: ${{ github.ref_name }}");
expect(workflow).toContain('gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1');
expect(targetManifestCheckout.with).toMatchObject({
ref: "${{ steps.resolve.outputs.sha }}",
path: "target",
"sparse-checkout": "package.json",
"sparse-checkout-cone-mode": false,
"persist-credentials": false,
});
expect(resolveTargetSteps.indexOf(targetManifestCheckout)).toBeLessThan(
resolveTargetSteps.indexOf(releaseInputValidation),
);
expectTextToIncludeAll(releaseInputValidation.run, [
'target_version="$(jq -er',
"does not belong to release branch",
"does not match ${identity_kind}",
"is not reachable from release context branch",
"does not match release tag",
"target_context_ref must be a canonical OpenClaw release branch or tag.",
]);
expect(npmTelegramJob.name).toBe("Run package Telegram E2E");
expect(npmTelegramJob.needs).toEqual(["resolve_target", "evidence_reuse"]);
expect(npmTelegramJob["timeout-minutes"]).toBe(
@@ -4853,6 +5030,7 @@ describe("package artifact reuse", () => {
});
expectTextToIncludeAll(targetSummaryStep.run, [
"Validation SHA:",
"Frozen tuple:",
"Package Acceptance Telegram E2E deferred:",
"Package Telegram E2E: deferred by \\`skip_package_telegram_e2e\\`",
]);
@@ -4901,6 +5079,7 @@ describe("package artifact reuse", () => {
".display_title == env.DISPATCH_RUN_NAME and .head_branch == env.CHILD_WORKFLOW_REF",
"The dispatch was not retried to avoid creating a duplicate child.",
'if [[ "$child_head_sha" != "$PARENT_WORKFLOW_SHA" ]]; then',
"Still waiting on ${workflow} after ${elapsed_minutes}m:",
'-f harness_ref="$TARGET_SHA"',
'args=(-f package_spec="$PACKAGE_SPEC"',
'args+=(-f scenario="$SCENARIO")',
@@ -5539,13 +5718,17 @@ describe("package artifact reuse", () => {
const telegramDispatch = workflowStep(telegramCaller, "Dispatch and await trusted Telegram QA");
expect(telegramDispatch.run).toContain('workflow="openclaw-release-telegram-qa.yml"');
expect(telegramDispatch.run).toContain('--repo "$GITHUB_REPOSITORY"');
expect(telegramDispatch.run).toContain("--ref main");
expect(telegramDispatch.env).toMatchObject({
PARENT_WORKFLOW_REF: "${{ github.ref_name }}",
PARENT_WORKFLOW_SHA: "${{ github.sha }}",
});
expect(telegramDispatch.run).toContain('--ref "$PARENT_WORKFLOW_REF"');
expect(telegramDispatch.run).toContain(
'-f expected_trusted_workflow_sha="$expected_trusted_workflow_sha"',
);
expect(telegramDispatch.run).toContain(
'[[ "$child_head_sha" == "$expected_trusted_workflow_sha" ]]',
'-f expected_trusted_workflow_sha="$PARENT_WORKFLOW_SHA"',
);
expect(telegramDispatch.run).toContain('[[ "$child_head_sha" != "$PARENT_WORKFLOW_SHA" ]]');
expect(telegramDispatch.run).not.toContain("commits/main");
expect(telegramDispatch.run).not.toContain("dispatch_attempt");
expect(telegramCaller["continue-on-error"]).toBeUndefined();
expect(telegramCaller["timeout-minutes"]).toBe(210);
@@ -6904,6 +7087,10 @@ wait_for_run plugin-clawhub-new.yml 123 "${expectedSha}" || status=$?
'VALIDATION_SHA="<full-commit-sha>"',
'-f ref="$VALIDATION_SHA"',
'-f expected_sha="$VALIDATION_SHA"',
'TOOLING_SHA="<recorded-full-main-ancestor-sha>"',
'VALIDATION_SHA="<full-release-candidate-sha>"',
"--target-ref release/YYYY.M.PATCH",
'--workflow-sha "$TOOLING_SHA"',
]);
for (const text of [releaseCi, releaseCiNotes, testing, parallels, ciDocs, maintainer]) {
expect(text).toContain("Validation SHA + Tooling SHA");
@@ -846,7 +846,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => {
expect(releaseChecksWorkflow.concurrency).toEqual({
group:
"openclaw-release-checks-${{ inputs.expected_sha || inputs.ref }}-${{ inputs.rerun_group }}",
"openclaw-release-checks-${{ inputs.expected_sha || inputs.ref }}-${{ github.sha }}-${{ inputs.rerun_group }}",
"cancel-in-progress": "${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }}",
});
expect(fullReleaseWorkflow.concurrency).toEqual({