fix(release): keep protected tooling trusted after main moves (#126881)

* fix(release): keep protected tooling trusted after main moves

* fix(release): cover protected tooling recovery paths

* fix(release): honor live tooling contracts

* fix(release): revalidate tooling at npm publish

* fix(release): bind npm publishers to live tooling

* fix(release): preserve trusted dispatch identity

* fix(release): revalidate parent authorization

* fix(release): bind ClawHub to release parent

* docs(release): define frozen tooling identity

* test(release): align ClawHub protected dispatch ref

* fix(release): trust protected plugin npm preflight tooling

* docs(release): scope protected writer guarantees

* fix(release): keep protected tooling foundation npm-only

* test(release): cover trusted npm preflight tooling
This commit is contained in:
Vincent Koc
2026-08-21 00:24:31 -07:00
committed by GitHub
parent b470422371
commit fa86caf94f
28 changed files with 3119 additions and 248 deletions
@@ -207,6 +207,7 @@ function normalizedEvidence(options: {
validationInputs?: Record<string, string> | null;
verifierSha?: string | null;
workflowRef?: string;
trustedWorkflowRef?: string;
}): NormalizedEvidence {
const runId = options.runId ?? "111";
const producerSha = options.producerSha ?? PRODUCER_SHA;
@@ -215,6 +216,11 @@ function normalizedEvidence(options: {
const workflowRef = options.workflowRef ?? "main";
const workflowFullRef = `refs/heads/${workflowRef}`;
const shaPinned = workflowRef.startsWith("release-ci/");
const trustedWorkflowRef = options.trustedWorkflowRef ?? "main";
const protectedTagRoute = trustedWorkflowRef.startsWith("release-publish/");
const trustedWorkflowFullRef = protectedTagRoute
? `refs/tags/${trustedWorkflowRef}`
: "refs/heads/main";
const validationInputs =
options.validationInputs === undefined ? DEFAULT_INPUTS : options.validationInputs;
const npmTelegramRequired =
@@ -269,14 +275,16 @@ function normalizedEvidence(options: {
status: "completed",
targetSha: options.targetSha,
url: `https://example.test/runs/${runId}`,
producerOnTrustedMainLineage: true,
producerOnTrustedMainLineage: !protectedTagRoute,
workflowFullRef,
workflowPath: ".github/workflows/full-release-validation.yml",
workflowQualifiedPath: `.github/workflows/full-release-validation.yml@${workflowFullRef}`,
workflowRef,
workflowRefProof: shaPinned
? "manifest-v3-sha-pinned-main-ancestry"
: "legacy-v2-main-ancestry",
workflowRefProof: protectedTagRoute
? "manifest-v3-protected-tag-exact-sha"
: shaPinned
? "manifest-v3-sha-pinned-main-ancestry"
: "legacy-v2-main-ancestry",
workflowRefType: "branch",
workflowRunPath: shaPinned
? `.github/workflows/full-release-validation.yml@${workflowFullRef}`
@@ -362,9 +370,9 @@ function normalizedEvidence(options: {
root,
runReleaseSoak: soak,
schema: "openclaw.release-validation-evidence/v3",
producerOnTrustedMainLineage: true,
trustedWorkflowFullRef: "refs/heads/main",
trustedWorkflowRef: "main",
producerOnTrustedMainLineage: !protectedTagRoute,
trustedWorkflowFullRef,
trustedWorkflowRef,
valid: true,
validationInputs,
verifier: {
@@ -404,16 +412,22 @@ import { join } from "node:path";
const runIndex = process.argv.indexOf("--validate-run");
const repoIndex = process.argv.indexOf("--repo");
const trustedRefIndex = process.argv.indexOf("--trusted-workflow-ref");
const trustedFullRefIndex = process.argv.indexOf("--trusted-workflow-full-ref");
const trustedShaIndex = process.argv.indexOf("--trusted-workflow-sha");
const verifierShaIndex = process.argv.indexOf("--verifier-source-sha");
const verifierFileIndex = process.argv.indexOf("--verifier-source-file");
if (
runIndex < 0 ||
repoIndex < 0 ||
trustedRefIndex < 0 ||
trustedFullRefIndex < 0 ||
trustedShaIndex < 0 ||
verifierShaIndex < 0 ||
verifierFileIndex < 0 ||
process.argv[repoIndex + 1] !== "openclaw/openclaw" ||
process.argv[trustedRefIndex + 1] !== "main" ||
process.argv[trustedRefIndex + 1] !== process.env.FAKE_TRUSTED_WORKFLOW_REF ||
process.argv[trustedFullRefIndex + 1] !== process.env.FAKE_TRUSTED_WORKFLOW_FULL_REF ||
process.argv[trustedShaIndex + 1] !== process.env.FAKE_TRUSTED_WORKFLOW_SHA ||
process.argv[verifierShaIndex + 1] !== process.env.FAKE_VERIFIER_SHA ||
process.argv[verifierFileIndex + 1] !== process.argv[1] ||
!process.argv.includes("--json")
@@ -481,12 +495,22 @@ function runResolver(args: {
repoDir: string;
runReleaseSoak?: string;
targetSha: string;
trustedTagSha?: string;
trustedTagType?: string;
trustedWorkflowFullRef?: string;
trustedWorkflowRef?: string;
trustedWorkflowSha?: string;
validatorPath: string;
verifierOnMain?: boolean;
verifierSha?: string;
workflowRef?: string;
}) {
const verifierSha = args.verifierSha ?? VERIFIER_SHA;
const trustedWorkflowRef = args.trustedWorkflowRef ?? "main";
const trustedWorkflowFullRef =
args.trustedWorkflowFullRef ??
(trustedWorkflowRef === "main" ? "refs/heads/main" : `refs/tags/${trustedWorkflowRef}`);
const trustedWorkflowSha = args.trustedWorkflowSha ?? verifierSha;
writeFileSync(
fixtureName(args.fixtures, `repos/${REPOSITORY}/compare/${verifierSha}...main`),
JSON.stringify({
@@ -494,6 +518,17 @@ function runResolver(args: {
status: args.verifierOnMain === false ? "diverged" : "ahead",
}),
);
if (trustedWorkflowRef !== "main") {
writeFileSync(
fixtureName(args.fixtures, `repos/${REPOSITORY}/git/ref/tags/${trustedWorkflowRef}`),
JSON.stringify({
object: {
sha: args.trustedTagSha ?? trustedWorkflowSha,
type: args.trustedTagType ?? "commit",
},
}),
);
}
if (args.compareBaseSha) {
writeFileSync(
fixtureName(
@@ -525,6 +560,12 @@ function runResolver(args: {
verifierSha,
"--workflow-ref",
args.workflowRef ?? "main",
"--trusted-workflow-ref",
trustedWorkflowRef,
"--trusted-workflow-full-ref",
trustedWorkflowFullRef,
"--trusted-workflow-sha",
trustedWorkflowSha,
"--release-profile",
args.releaseProfile ?? "full",
"--run-release-soak",
@@ -542,6 +583,9 @@ function runResolver(args: {
env: {
...process.env,
FAKE_GH_FIXTURES: args.fixtures,
FAKE_TRUSTED_WORKFLOW_FULL_REF: trustedWorkflowFullRef,
FAKE_TRUSTED_WORKFLOW_REF: trustedWorkflowRef,
FAKE_TRUSTED_WORKFLOW_SHA: trustedWorkflowSha,
FAKE_VALIDATOR_FIXTURES: args.fixtures,
FAKE_VERIFIER_SHA: verifierSha,
GITHUB_OUTPUT: "",
@@ -593,6 +637,82 @@ describe("scripts/github/find-reusable-release-validation.sh", () => {
});
});
it("reuses strict evidence through the exact lightweight protected tooling tag", () => {
const { clone, priorSha } = getSharedRepo();
const trustedWorkflowRef = `release-publish/${VERIFIER_SHA.slice(0, 12)}-456`;
const producerRef = `release-ci/${VERIFIER_SHA.slice(0, 12)}-122`;
const record = normalizedEvidence({
producerSha: VERIFIER_SHA,
targetSha: priorSha,
trustedWorkflowRef,
workflowRef: producerRef,
});
const { binDir, fixtures, validatorPath } = setUpFixtures([{ record, runId: "111" }]);
const result = runResolver({
binDir,
fixtures,
repoDir: clone,
targetSha: priorSha,
trustedWorkflowRef,
validatorPath,
verifierOnMain: false,
workflowRef: `release-ci/${VERIFIER_SHA.slice(0, 12)}-123`,
});
expect(result.status).toBe(0);
expect(parseOutput(result.stdout)).toMatchObject({
evidence_run_id: "111",
reuse: "true",
});
});
it.each([
{
label: "moved protected tag",
options: {
trustedTagSha: "d".repeat(40),
},
},
{
label: "annotated protected tag",
options: {
trustedTagType: "tag",
},
},
{
label: "same-name branch",
options: {
trustedWorkflowFullRef: `refs/heads/release-publish/${VERIFIER_SHA.slice(0, 12)}-456`,
},
},
])("rejects protected tooling identity drift: $label", ({ options }) => {
const { clone, priorSha } = getSharedRepo();
const trustedWorkflowRef = `release-publish/${VERIFIER_SHA.slice(0, 12)}-456`;
const producerRef = `release-ci/${VERIFIER_SHA.slice(0, 12)}-122`;
const record = normalizedEvidence({
producerSha: VERIFIER_SHA,
targetSha: priorSha,
trustedWorkflowRef,
workflowRef: producerRef,
});
const { binDir, fixtures, validatorPath } = setUpFixtures([{ record, runId: "111" }]);
const result = runResolver({
binDir,
fixtures,
repoDir: clone,
targetSha: priorSha,
trustedWorkflowRef,
validatorPath,
workflowRef: `release-ci/${VERIFIER_SHA.slice(0, 12)}-123`,
...options,
});
expect(result.status).toBe(0);
expect(parseOutput(result.stdout)).toMatchObject({ reuse: "false" });
});
it("reuses npm Telegram evidence only when its selectors match exactly", () => {
const { clone, priorSha } = getSharedRepo();
const validationInputs = {
@@ -3,6 +3,7 @@ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { parse as parseYaml } from "yaml";
import {
assertTrustedWorkflowHarness,
FULL_RELEASE_WAIT_POLL_INTERVAL_MS,
@@ -14,18 +15,25 @@ import {
resolveRemoteTargetRefSha,
shouldDeleteTemporaryWorkflowRef,
verifyTargetRef,
verifyTrustedWorkflowRef,
} 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 CURRENT_WORKFLOW_SOURCE = readFileSync(
".github/workflows/full-release-validation.yml",
"utf8",
);
const CONTRACT_ONE_WORKFLOW_SOURCE = CURRENT_WORKFLOW_SOURCE.replace(
'RELEASE_ISOLATION_TOOLING_CONTRACT: "2"',
'RELEASE_ISOLATION_TOOLING_CONTRACT: "1"',
).replace(
` trusted_workflow_json:
description: Trusted release tooling identity JSON
required: true
type: string
`,
"",
);
const LEGACY_WORKFLOW_SOURCE = `name: Full Release Validation
on:
workflow_dispatch:
@@ -70,8 +78,10 @@ function createDispatchFixture(options: { workflowSource?: string } = {}) {
join(checkout, "scripts", "release-ci-summary.mjs"),
`const expected = [
"--validate-run", "123",
"--trusted-workflow-ref", "main",
"--json",
"--trusted-workflow-ref", process.env.MOCK_TRUSTED_WORKFLOW_REF,
"--trusted-workflow-full-ref", process.env.MOCK_TRUSTED_WORKFLOW_FULL_REF,
"--trusted-workflow-sha", process.env.MOCK_WORKFLOW_SHA,
"--json",
"--verifier-source-sha", process.env.MOCK_WORKFLOW_SHA,
"--verifier-source-file", process.argv[1],
];
@@ -89,12 +99,21 @@ console.log(JSON.stringify({ valid: true, current: { runId: "123" }, root: { run
join(checkout, ".github", "workflows", "full-release-validation.yml"),
options.workflowSource ?? CURRENT_WORKFLOW_SOURCE,
);
const workflow = parseYaml(
readFileSync(join(checkout, ".github", "workflows", "full-release-validation.yml"), "utf8"),
) as {
on?: { workflow_dispatch?: { inputs?: Record<string, unknown> } };
};
const declaredWorkflowInputs = Object.keys(workflow.on?.workflow_dispatch?.inputs ?? {});
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"]);
const trustedWorkflowTag = `release-publish/${workflowSha.slice(0, 12)}-123`;
runGit(checkout, ["remote", "add", "origin", origin]);
runGit(checkout, ["push", "-u", "origin", "main"]);
runGit(checkout, ["tag", trustedWorkflowTag, workflowSha]);
runGit(checkout, ["push", "origin", `refs/tags/${trustedWorkflowTag}`]);
runGit(checkout, ["checkout", "-b", releaseRef]);
writeFileSync(join(checkout, "target.txt"), "release target\n");
runGit(checkout, ["add", "target.txt"]);
@@ -128,6 +147,17 @@ const fs = require("node:fs");
const args = process.argv.slice(2);
fs.appendFileSync(process.env.MOCK_GH_CALLS, JSON.stringify(args) + "\\n");
if (args[0] === "workflow" && args[1] === "run") {
const declaredInputs = new Set(JSON.parse(process.env.MOCK_WORKFLOW_INPUTS));
for (let index = 0; index < args.length; index += 1) {
if (args[index] !== "-f") continue;
const assignment = args[index + 1] || "";
const key = assignment.slice(0, assignment.indexOf("="));
if (!declaredInputs.has(key)) {
console.error("workflow input is not declared: " + key);
process.exit(2);
}
index += 1;
}
console.log("https://github.com/openclaw/openclaw/actions/runs/123");
} else if (args[0] === "api" && args.at(-1).endsWith("/actions/runs/123")) {
console.log(JSON.stringify({ status: "completed", conclusion: "success", head_sha: process.env.MOCK_WORKFLOW_SHA }));
@@ -139,8 +169,13 @@ if (args[0] === "workflow" && args[1] === "run") {
);
chmodSync(ghPath, 0o755);
const run = (extraArgs: string[] = []) =>
spawnSync(
const run = (extraArgs: string[] = []) => {
const trustedRefIndex = extraArgs.indexOf("--trusted-workflow-ref");
const trustedWorkflowRef =
trustedRefIndex >= 0 ? (extraArgs[trustedRefIndex + 1] ?? "") : "main";
const trustedWorkflowFullRef =
trustedWorkflowRef === "main" ? "refs/heads/main" : `refs/tags/${trustedWorkflowRef}`;
return spawnSync(
process.execPath,
[SCRIPT_PATH, "--sha", targetSha, "--target-ref", releaseRef, ...extraArgs],
{
@@ -151,11 +186,15 @@ if (args[0] === "workflow" && args[1] === "run") {
MOCK_GH_CALLS: ghCallsPath,
MOCK_GIT_CALLS: gitCallsPath,
MOCK_REAL_PATH: process.env.PATH,
MOCK_TRUSTED_WORKFLOW_FULL_REF: trustedWorkflowFullRef,
MOCK_TRUSTED_WORKFLOW_REF: trustedWorkflowRef,
MOCK_WORKFLOW_INPUTS: JSON.stringify(declaredWorkflowInputs),
MOCK_WORKFLOW_SHA: workflowSha,
PATH: `${binDir}:${process.env.PATH}`,
},
},
);
};
const readCalls = (path: string): string[][] =>
readFileSync(path, "utf8")
.trim()
@@ -174,6 +213,7 @@ if (args[0] === "workflow" && args[1] === "run") {
releaseRef,
run,
targetSha,
trustedWorkflowTag,
workflowSha,
};
}
@@ -186,6 +226,8 @@ describe("full-release-validation-at-sha", () => {
"abc123",
"--workflow-sha",
"a".repeat(40),
"--trusted-workflow-ref",
`release-publish/${"a".repeat(12)}-123`,
"--target-ref",
"release/2026.7.1",
"--keep-branch",
@@ -206,6 +248,7 @@ describe("full-release-validation-at-sha", () => {
},
sha: "abc123",
targetRef: "release/2026.7.1",
trustedWorkflowRef: `release-publish/${"a".repeat(12)}-123`,
workflowSha: "a".repeat(40),
});
});
@@ -221,6 +264,16 @@ describe("full-release-validation-at-sha", () => {
expect(() => parseArgs(["--", "-f"])).toThrow("-f requires a value");
});
it("requires an exact Tooling SHA for protected workflow tags", () => {
const trustedTag = `release-publish/${"a".repeat(12)}-123`;
expect(() => parseArgs(["--trusted-workflow-ref", trustedTag])).toThrow(
"explicit full Tooling SHA",
);
expect(() =>
parseArgs(["--workflow-sha", "a".repeat(40), "--trusted-workflow-ref", "release/2026.8.1"]),
).toThrow("protected release-publish");
});
it("rejects retry groups that are not controller APIs", () => {
expect(() => parseArgs(["-f", "rerun_group=release-checks"])).toThrow(
"rerun_group must be one of",
@@ -403,6 +456,9 @@ describe("full-release-validation-at-sha", () => {
expect(() => parseArgs(["--", `expected_sha=${"a".repeat(40)}`])).toThrow(
"reserves expected_sha",
);
expect(() => parseArgs(["-f", "trusted_workflow_json={}"])).toThrow(
"reserves trusted_workflow_json",
);
});
it("validates direct and reused runs through the strict evidence verifier", () => {
@@ -413,6 +469,10 @@ describe("full-release-validation-at-sha", () => {
"123",
"--trusted-workflow-ref",
"main",
"--trusted-workflow-full-ref",
"refs/heads/main",
"--trusted-workflow-sha",
workflowSha,
"--json",
"--verifier-source-sha",
workflowSha,
@@ -422,6 +482,71 @@ describe("full-release-validation-at-sha", () => {
expect(() => releaseEvidenceVerificationArgs("", workflowSha, verifier)).toThrow(
"positive decimal",
);
const trustedTag = `release-publish/${workflowSha.slice(0, 12)}-123`;
expect(releaseEvidenceVerificationArgs("123", workflowSha, verifier, trustedTag)).toEqual([
"--validate-run",
"123",
"--trusted-workflow-ref",
trustedTag,
"--trusted-workflow-full-ref",
`refs/tags/${trustedTag}`,
"--trusted-workflow-sha",
workflowSha,
"--json",
"--verifier-source-sha",
workflowSha,
"--verifier-source-file",
verifier,
]);
expect(() =>
releaseEvidenceVerificationArgs("123", workflowSha, verifier, "release/2026.8.1"),
).toThrow("protected release-publish tag");
});
it("accepts only exact protected workflow tags outside main ancestry", () => {
const workflowSha = "a".repeat(40);
const trustedTag = `release-publish/${workflowSha.slice(0, 12)}-123`;
expect(() =>
verifyTrustedWorkflowRef(
workflowSha,
"main",
() => "",
() => true,
),
).not.toThrow();
expect(() =>
verifyTrustedWorkflowRef(
workflowSha,
"main",
() => "",
() => false,
),
).toThrow("not reachable from current origin/main");
expect(() =>
verifyTrustedWorkflowRef(
workflowSha,
trustedTag,
() => workflowSha,
() => false,
),
).not.toThrow();
expect(() =>
verifyTrustedWorkflowRef(
workflowSha,
`release-publish/${"b".repeat(12)}-123`,
() => workflowSha,
),
).toThrow("does not match Tooling SHA");
expect(() => verifyTrustedWorkflowRef(workflowSha, trustedTag, () => "")).toThrow(
"does not exist on origin",
);
expect(() => verifyTrustedWorkflowRef(workflowSha, trustedTag, () => "c".repeat(40))).toThrow(
`expected ${workflowSha}`,
);
expect(() =>
verifyTrustedWorkflowRef(workflowSha, "release/2026.8.1", () => workflowSha),
).toThrow("protected release-publish");
});
it("bounds polling for the exact workflow run", () => {
@@ -461,7 +586,7 @@ describe("full-release-validation-at-sha", () => {
},
() => CURRENT_WORKFLOW_SOURCE,
),
).toBe(verifierPath);
).toEqual({ contract: "2", verifierPath });
expect(checked).toEqual([workflowPath, verifierPath]);
expect(() => assertTrustedWorkflowHarness("a".repeat(40), () => false)).toThrow(workflowPath);
expect(() =>
@@ -477,15 +602,30 @@ describe("full-release-validation-at-sha", () => {
() => true,
() => LEGACY_WORKFLOW_SOURCE,
),
).toThrow("does not declare RELEASE_ISOLATION_TOOLING_CONTRACT=1");
).toThrow("does not declare a supported RELEASE_ISOLATION_TOOLING_CONTRACT");
expect(() =>
assertTrustedWorkflowHarness(
"b".repeat(40),
() => true,
() =>
'env:\n RELEASE_ISOLATION_TOOLING_CONTRACT: "1"\non:\n workflow_dispatch:\n inputs: {}\n',
'env:\n RELEASE_ISOLATION_TOOLING_CONTRACT: "2"\non:\n workflow_dispatch:\n inputs: {}\n',
),
).toThrow(`Tooling SHA ${"b".repeat(40)} is missing workflow_dispatch input expected_sha`);
expect(() =>
assertTrustedWorkflowHarness(
"b".repeat(40),
() => true,
() =>
'env:\n RELEASE_ISOLATION_TOOLING_CONTRACT: "2"\non:\n workflow_dispatch:\n inputs:\n expected_sha: {}\n',
),
).toThrow("missing workflow_dispatch input trusted_workflow_json");
expect(
assertTrustedWorkflowHarness(
"b".repeat(40),
() => true,
() => CONTRACT_ONE_WORKFLOW_SOURCE,
),
).toEqual({ contract: "1", verifierPath });
});
it("retains a failed parent workflow ref for GitHub reruns", () => {
@@ -576,6 +716,11 @@ describe("full-release-validation-at-sha", () => {
target_context_ref: fixture.releaseRef,
allow_unreleased_changelog: "false",
});
expect(JSON.parse(dispatchInputs.trusted_workflow_json ?? "{}")).toEqual({
ref: "main",
fullRef: "refs/heads/main",
sha: fixture.workflowSha,
});
expect(ghCalls).toContainEqual(["api", "repos/openclaw/openclaw/actions/runs/123"]);
expect(ghCalls.some((args) => args[0] === "graphql")).toBe(false);
expect(ghCalls.some((args) => args[0] === "run" && args[1] === "watch")).toBe(false);
@@ -604,10 +749,66 @@ describe("full-release-validation-at-sha", () => {
}
});
it("dispatches non-main tooling only when its exact protected tag is supplied", () => {
const fixture = createDispatchFixture();
try {
const result = fixture.run([
"--workflow-sha",
fixture.workflowSha,
"--trusted-workflow-ref",
fixture.trustedWorkflowTag,
]);
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toContain(`Trusted workflow ref: ${fixture.trustedWorkflowTag}`);
expect(fixture.readCalls(fixture.gitCallsPath)).toContainEqual([
"ls-remote",
"--tags",
"origin",
`refs/tags/${fixture.trustedWorkflowTag}`,
]);
const dispatch = fixture
.readCalls(fixture.ghCallsPath)
.find((args) => args[0] === "workflow" && args[1] === "run");
const trustedIdentity = dispatch
?.find((arg) => arg.startsWith("trusted_workflow_json="))
?.slice("trusted_workflow_json=".length);
expect(JSON.parse(trustedIdentity ?? "{}")).toEqual({
ref: fixture.trustedWorkflowTag,
fullRef: `refs/tags/${fixture.trustedWorkflowTag}`,
sha: fixture.workflowSha,
});
} finally {
fixture.cleanup();
}
});
it("disables evidence reuse and omits the contract 2 input for contract 1 tooling", () => {
const fixture = createDispatchFixture({ workflowSource: CONTRACT_ONE_WORKFLOW_SOURCE });
try {
const result = fixture.run([
"--workflow-sha",
fixture.workflowSha,
"--trusted-workflow-ref",
fixture.trustedWorkflowTag,
]);
expect(result.status, result.stderr).toBe(0);
const dispatch = fixture
.readCalls(fixture.ghCallsPath)
.find((args) => args[0] === "workflow" && args[1] === "run");
const assignments = (dispatch ?? [])
.filter((_value, index, values) => values[index - 1] === "-f")
.map((value) => value.split("=", 1)[0]);
expect(assignments).not.toContain("trusted_workflow_json");
expect(dispatch).toContain("reuse_evidence=false");
} finally {
fixture.cleanup();
}
});
it("rejects pinned old-schema tooling before either remote ref is pushed", () => {
const fixture = createDispatchFixture({
workflowSource:
'name: Full Release Validation\nenv:\n RELEASE_ISOLATION_TOOLING_CONTRACT: "1"\non:\n workflow_dispatch:\n',
'name: Full Release Validation\nenv:\n RELEASE_ISOLATION_TOOLING_CONTRACT: "2"\non:\n workflow_dispatch:\n',
});
try {
const result = fixture.run(["--workflow-sha", fixture.workflowSha]);
@@ -629,7 +830,9 @@ describe("full-release-validation-at-sha", () => {
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(result.stderr).toContain(
"does not declare a supported RELEASE_ISOLATION_TOOLING_CONTRACT",
);
expect(fixture.readCalls(fixture.gitCallsPath).filter((args) => args[0] === "push")).toEqual(
[],
);
+108 -5
View File
@@ -24,7 +24,7 @@ function fixture(
head_branch: BRANCH,
head_sha: SHA,
html_url: URL,
path: `.github/workflows/openclaw-npm-release.yml@refs/tags/${BRANCH}`,
path: ".github/workflows/openclaw-npm-release.yml",
workflow_id: 101,
},
tag: {
@@ -32,6 +32,8 @@ function fixture(
verification: { verified: true },
},
tagRef: { object: { sha: TAG_OBJECT_SHA, type: "tag" } },
trustedWorkflowFullRef: `refs/tags/${BRANCH}`,
trustedWorkflowRef: BRANCH,
...overrides,
};
}
@@ -80,8 +82,46 @@ describe("openclaw npm resume run identity", () => {
});
});
it("accepts a successful run bound to the exact lightweight protected tooling tag", () => {
expect(
validateOpenClawNpmResumeRun(
fixture({
compareStatus: undefined,
tag: {},
tagRef: { object: { sha: SHA, type: "commit" } },
}),
),
).toEqual({
tagObjectSha: SHA,
url: URL,
workflowRef: `refs/tags/${BRANCH}`,
workflowSha: SHA,
});
});
it("accepts the canonical path shape returned by the Actions workflow run API", () => {
expect(
validateOpenClawNpmResumeRun(
fixture({
run: {
conclusion: "success",
event: "workflow_dispatch",
head_branch: BRANCH,
head_sha: SHA,
html_url: URL,
path: ".github/workflows/openclaw-npm-release.yml",
workflow_id: 101,
},
}),
),
).toMatchObject({
workflowRef: `refs/tags/${BRANCH}`,
workflowSha: SHA,
});
});
it.each([
["branch", { run: { ...fixture().run, head_branch: "main" } }, "untrusted workflow ref"],
["branch", { run: { ...fixture().run, head_branch: "main" } }, "untrusted workflow identity"],
["workflow", { run: { ...fixture().run, workflow_id: 999 } }, "untrusted workflow identity"],
["event", { run: { ...fixture().run, event: "push" } }, "untrusted workflow identity"],
[
@@ -94,10 +134,29 @@ describe("openclaw npm resume run identity", () => {
{ run: { ...fixture().run, path: ".github/workflows/ci.yml" } },
"untrusted workflow identity",
],
[
"same-name branch full ref",
{ trustedWorkflowFullRef: `refs/heads/${BRANCH}` },
"untrusted workflow ref",
],
[
"mismatched supplied ref",
{ trustedWorkflowRef: `release-publish/${SHA.slice(0, 12)}-124` },
"untrusted workflow ref",
],
[
"tag kind",
{ tagRef: { object: { sha: TAG_OBJECT_SHA, type: "commit" } } },
"not a signed annotated tag",
{ tagRef: { object: { sha: TAG_OBJECT_SHA, type: "tree" } } },
"not a protected tag",
],
[
"moved lightweight tag",
{
compareStatus: undefined,
tag: {},
tagRef: { object: { sha: "c".repeat(40), type: "commit" } },
},
"moved after dispatch",
],
[
"tag target",
@@ -140,8 +199,52 @@ describe("openclaw npm resume run identity", () => {
});
expect(
resolveOpenClawNpmResumeRun({ repo: "openclaw/openclaw", runGh, runId: "456" }),
resolveOpenClawNpmResumeRun({
repo: "openclaw/openclaw",
runGh,
runId: "456",
trustedWorkflowFullRef: `refs/tags/${BRANCH}`,
trustedWorkflowRef: BRANCH,
}),
).toMatchObject({ workflowRef: `refs/tags/${BRANCH}`, workflowSha: SHA });
expect(runGh).toHaveBeenCalledTimes(6);
});
it("loads a lightweight protected tag without requiring tag metadata or main ancestry", () => {
const lightweight = fixture({
compareStatus: undefined,
tag: {},
tagRef: { object: { sha: SHA, type: "commit" } },
});
const responses = new Map<string, unknown>([
[`api repos/openclaw/openclaw/actions/runs/456 --method GET`, lightweight.run],
[
`api repos/openclaw/openclaw/actions/workflows/openclaw-npm-release.yml --method GET`,
{ id: 101 },
],
[`api repos/openclaw/openclaw/git/ref/tags/${BRANCH} --method GET`, lightweight.tagRef],
[`run view 456 --repo openclaw/openclaw --json jobs --jq .jobs`, lightweight.jobs],
]);
const runGh = vi.fn((args: string[]) => {
const response = responses.get(args.join(" "));
if (!response) {
throw new Error(`Unexpected gh invocation: ${args.join(" ")}`);
}
return JSON.stringify(response);
});
expect(
resolveOpenClawNpmResumeRun({
repo: "openclaw/openclaw",
runGh,
runId: "456",
trustedWorkflowFullRef: `refs/tags/${BRANCH}`,
trustedWorkflowRef: BRANCH,
}),
).toMatchObject({ workflowRef: `refs/tags/${BRANCH}`, workflowSha: SHA });
expect(runGh).toHaveBeenCalledTimes(4);
expect(runGh.mock.calls.flatMap(([args]) => args)).not.toContain(
`repos/openclaw/openclaw/compare/${SHA}...main`,
);
});
});
+583 -22
View File
@@ -931,8 +931,11 @@ function runOpenClawNpmTrustedRefGuard(overrides: Record<string, string>) {
throw new Error("Expected OpenClaw npm trusted ref guard");
}
const binDir = tempDirs.make("openclaw-npm-trusted-ref-");
const ghPath = `${binDir}/gh`;
const gitPath = `${binDir}/git`;
const timeoutPath = `${binDir}/timeout`;
writeFileSync(ghPath, `#!/bin/sh\nprintf '%s\\n' "\${MOCK_REMOTE_TAG_SHA}"\n`);
chmodSync(ghPath, 0o755);
writeFileSync(
gitPath,
`#!/bin/sh\nif [ "$1" = "fetch" ]; then exit 0; fi\nif [ "$1" = "merge-base" ]; then [ "\${MOCK_WORKFLOW_ANCESTOR}" = "true" ]; exit $?; fi\nexit 2\n`,
@@ -946,6 +949,8 @@ function runOpenClawNpmTrustedRefGuard(overrides: Record<string, string>) {
return spawnSync("bash", ["-c", script], {
encoding: "utf8",
env: {
GITHUB_REPOSITORY: "openclaw/openclaw",
MOCK_REMOTE_TAG_SHA: "a".repeat(40),
MOCK_WORKFLOW_ANCESTOR: "true",
PATH: `${binDir}:${process.env.PATH}`,
RELEASE_NPM_DIST_TAG: "beta",
@@ -957,6 +962,203 @@ function runOpenClawNpmTrustedRefGuard(overrides: Record<string, string>) {
});
}
function runPluginNpmPreflightToolingGuard(overrides: Record<string, string>) {
const job = workflowJob(PLUGIN_NPM_RELEASE_WORKFLOW, "preview_plugins_npm");
const script = workflowStep(job, "Verify trusted preflight tooling identity").run;
if (!script) {
throw new Error("Expected plugin npm preflight tooling identity guard");
}
const workdir = tempDirs.make("plugin-npm-preflight-tooling-");
const binDir = resolve(workdir, "bin");
const toolingDir = resolve(workdir, ".release-tooling/scripts");
const toolingLibDir = resolve(toolingDir, "lib");
mkdirSync(binDir, { recursive: true });
mkdirSync(toolingLibDir, { recursive: true });
writeFileSync(
resolve(toolingDir, "release-tooling-identity.mjs"),
readFileSync(resolve(REPO_ROOT, "scripts/release-tooling-identity.mjs")),
);
writeFileSync(
resolve(toolingLibDir, "record-shared.mjs"),
readFileSync(resolve(REPO_ROOT, "scripts/lib/record-shared.mjs")),
);
writeFileSync(
resolve(binDir, "gh"),
`#!/usr/bin/env bash
set -euo pipefail
[[ "$1" == "api" ]] || exit 64
case "$2" in
*/git/ref/tags/*)
[[ "$MOCK_TAG_MISSING" != "true" ]] || exit 1
jq -cn \
--arg ref "$MOCK_TAG_FULL_REF" \
--arg sha "$MOCK_TAG_SHA" \
--arg type "$MOCK_TAG_TYPE" \
'{ref: $ref, object: {sha: $sha, type: $type}}'
;;
*/compare/*)
jq -cn --arg status "$MOCK_COMPARE_STATUS" '{status: $status}'
;;
*)
exit 64
;;
esac
`,
{ mode: 0o755 },
);
return spawnSync("bash", ["-c", script], {
cwd: workdir,
encoding: "utf8",
env: {
GITHUB_REPOSITORY: "openclaw/openclaw",
MOCK_COMPARE_STATUS: "identical",
MOCK_TAG_FULL_REF: "",
MOCK_TAG_MISSING: "false",
MOCK_TAG_SHA: "",
MOCK_TAG_TYPE: "commit",
PATH: `${binDir}:${process.env.PATH}`,
...overrides,
},
});
}
type ProtectedPreflightConsumerParams = {
currentRef: string;
currentWorkflowSha: string;
liveTagSha?: string;
preflightHeadBranch: string;
preflightHeadSha: string;
};
function runReleasePublishPreflightConsumerGuard(params: ProtectedPreflightConsumerParams) {
const job = workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target");
const script = workflowStep(job, "Download OpenClaw npm preflight manifest").run;
if (!script) {
throw new Error("Expected release publish preflight consumer guard");
}
const workdir = tempDirs.make("release-publish-preflight-consumer-");
const binDir = resolve(workdir, "bin");
const runnerTemp = resolve(workdir, "runner");
mkdirSync(binDir);
mkdirSync(runnerTemp);
writeFileSync(
resolve(binDir, "gh"),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$1" == "run" && "$2" == "download" ]]; then
exit 0
fi
if [[ "$1" == "api" ]]; then
printf '%s\\n' "$MOCK_PREFLIGHT_RUN"
exit 0
fi
exit 64
`,
{ mode: 0o755 },
);
return spawnSync("bash", ["-c", script], {
cwd: workdir,
encoding: "utf8",
env: {
GITHUB_OUTPUT: resolve(workdir, "github-output"),
GITHUB_REF: params.currentRef,
GITHUB_REPOSITORY: "openclaw/openclaw",
MOCK_PREFLIGHT_RUN: JSON.stringify({
conclusion: "success",
event: "workflow_dispatch",
head_branch: params.preflightHeadBranch,
head_sha: params.preflightHeadSha,
path: ".github/workflows/openclaw-npm-release.yml",
run_attempt: 1,
}),
PATH: `${binDir}:${process.env.PATH}`,
PREFLIGHT_RUN_ID: "111",
RELEASE_NPM_DIST_TAG: "beta",
RELEASE_TAG: "v2026.8.1-beta.3",
RUNNER_TEMP: runnerTemp,
WORKFLOW_SHA: params.currentWorkflowSha,
},
});
}
function runOpenClawNpmPreflightConsumerGuard(params: ProtectedPreflightConsumerParams) {
const job = workflowJob(OPENCLAW_NPM_RELEASE_WORKFLOW, "publish_openclaw_npm");
const script = workflowStep(job, "Verify preflight run metadata").run;
if (!script) {
throw new Error("Expected OpenClaw npm preflight consumer guard");
}
const workdir = tempDirs.make("openclaw-npm-preflight-consumer-");
const binDir = resolve(workdir, "bin");
mkdirSync(binDir);
writeFileSync(
resolve(binDir, "gh"),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$1" == "run" && "$2" == "view" ]]; then
printf '%s\\n' "$MOCK_PREFLIGHT_RUN"
exit 0
fi
if [[ "$1" == "api" ]]; then
if [[ "$2" == *"/git/ref/tags/"* ]]; then
printf '%s\\n' "$MOCK_REMOTE_TAG_SHA"
exit 0
fi
printf '1\\n'
exit 0
fi
exit 64
`,
{ mode: 0o755 },
);
writeFileSync(
resolve(binDir, "git"),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$*" == "rev-parse HEAD" ]]; then
printf '%s\\n' "$MOCK_RELEASE_SHA"
exit 0
fi
if [[ "$*" == *"cat-file -e"* || "$*" == *"merge-base --is-ancestor"* || "$*" == *" fetch "* ]]; then
exit 0
fi
exit 64
`,
{ mode: 0o755 },
);
writeFileSync(
resolve(binDir, "node"),
`#!/usr/bin/env bash
cat >/dev/null
`,
{ mode: 0o755 },
);
return spawnSync("bash", ["-c", script], {
cwd: workdir,
encoding: "utf8",
env: {
EXPECTED_EXTENDED_STABLE_BRANCH: "",
GITHUB_OUTPUT: resolve(workdir, "github-output"),
GITHUB_REPOSITORY: "openclaw/openclaw",
MOCK_PREFLIGHT_RUN: JSON.stringify({
conclusion: "success",
event: "workflow_dispatch",
headBranch: params.preflightHeadBranch,
headSha: params.preflightHeadSha,
url: "https://github.com/openclaw/openclaw/actions/runs/111",
workflowName: "OpenClaw NPM Release",
}),
MOCK_RELEASE_SHA: "d".repeat(40),
MOCK_REMOTE_TAG_SHA: params.liveTagSha ?? params.currentWorkflowSha,
PATH: `${binDir}:${process.env.PATH}`,
PREFLIGHT_RUN_ID: "111",
RELEASE_NPM_DIST_TAG: "beta",
RUN_KIND: "preflight",
WORKFLOW_REF: params.currentRef,
WORKFLOW_SHA: params.currentWorkflowSha,
},
});
}
type ReleaseCheckArtifact = {
expired: boolean;
id: number;
@@ -1224,6 +1426,8 @@ describe("package acceptance workflow", () => {
expect(dispatch.run).toContain(
'-f plugin_sdk_api_acknowledgement="${PLUGIN_SDK_API_ACKNOWLEDGEMENT}"',
);
expect(dispatch.run).toContain('--trusted-workflow-ref "${PARENT_WORKFLOW_BRANCH}"');
expect(dispatch.run).toContain('--trusted-workflow-full-ref "${GITHUB_REF}"');
});
it("requires selected plugin names or complete immutable evidence for broad publication", () => {
@@ -1318,11 +1522,11 @@ describe("package acceptance workflow", () => {
expect(verifyStep.run).not.toContain("npm view openclaw@extended-stable version");
});
it("accepts only main-reachable protected SHA-pinned release publish tags", () => {
it("accepts only exact protected SHA-pinned release publish tags", () => {
const workflowSha = "a".repeat(40);
const binDir = tempDirs.make("release-publish-gh-");
const ghPath = `${binDir}/gh`;
writeFileSync(ghPath, `#!/bin/sh\nprintf '%s\\n' "\${MOCK_MERGE_BASE_SHA}"\n`);
writeFileSync(ghPath, `#!/bin/sh\nprintf '%s\\n' "\${MOCK_REMOTE_TAG_SHA}"\n`);
chmodSync(ghPath, 0o755);
const pinnedEnv = {
GITHUB_REPOSITORY: "openclaw/openclaw",
@@ -1333,7 +1537,7 @@ describe("package acceptance workflow", () => {
const valid = runReleasePublishInputValidation({
...pinnedEnv,
MOCK_MERGE_BASE_SHA: workflowSha,
MOCK_REMOTE_TAG_SHA: workflowSha,
});
expect(valid.status, valid.stderr).toBe(0);
@@ -1346,13 +1550,13 @@ describe("package acceptance workflow", () => {
"SHA-pinned release publish tag does not match workflow SHA",
);
const unreachable = runReleasePublishInputValidation({
const moved = runReleasePublishInputValidation({
...pinnedEnv,
MOCK_MERGE_BASE_SHA: "c".repeat(40),
MOCK_REMOTE_TAG_SHA: "c".repeat(40),
});
expect(unreachable.status).toBe(1);
expect(unreachable.stderr).toContain(
"SHA-pinned release publish tag revision is not reachable from current main",
expect(moved.status).toBe(1);
expect(moved.stderr).toContain(
"SHA-pinned release publish tag does not resolve to workflow SHA",
);
});
@@ -1363,6 +1567,7 @@ describe("package acceptance workflow", () => {
const valid = runOpenClawNpmTrustedRefGuard({
WORKFLOW_REF: protectedRef,
WORKFLOW_SHA: workflowSha,
MOCK_REMOTE_TAG_SHA: workflowSha,
});
expect(valid.status, valid.stderr).toBe(0);
@@ -1375,26 +1580,327 @@ describe("package acceptance workflow", () => {
"SHA-pinned release-publish tag does not match the OpenClaw npm workflow SHA",
);
const unreachable = runOpenClawNpmTrustedRefGuard({
MOCK_WORKFLOW_ANCESTOR: "false",
const moved = runOpenClawNpmTrustedRefGuard({
MOCK_REMOTE_TAG_SHA: "c".repeat(40),
WORKFLOW_REF: protectedRef,
WORKFLOW_SHA: workflowSha,
});
expect(unreachable.status).toBe(1);
expect(unreachable.stderr).toContain(
"SHA-pinned OpenClaw npm workflow revision is not reachable from current main",
expect(moved.status).toBe(1);
expect(moved.stderr).toContain(
"SHA-pinned release-publish tag does not resolve to the OpenClaw npm workflow SHA",
);
});
it("allows protected SHA-pinned tooling tags to consume token-bootstrap evidence", () => {
it("runs plugin npm preflight trust from the exact workflow tooling checkout", () => {
const job = workflowJob(PLUGIN_NPM_RELEASE_WORKFLOW, "preview_plugins_npm");
const checkout = workflowStep(job, "Checkout trusted preflight tooling");
const identity = workflowStep(job, "Verify trusted preflight tooling identity");
const target = workflowStep(job, "Validate ref is on a trusted publish branch");
expect(checkout.if).toBe("github.event_name == 'workflow_dispatch' && inputs.preflight_only");
expect(checkout.with).toMatchObject({
"fetch-depth": 1,
path: ".release-tooling",
"persist-credentials": false,
ref: "${{ github.workflow_sha }}",
"sparse-checkout": "scripts/lib/record-shared.mjs\nscripts/release-tooling-identity.mjs\n",
"sparse-checkout-cone-mode": false,
});
expect(identity.if).toBe("github.event_name == 'workflow_dispatch' && inputs.preflight_only");
expect(identity.env).toMatchObject({
GH_TOKEN: "${{ github.token }}",
WORKFLOW_FULL_REF: "${{ github.ref }}",
WORKFLOW_REF: "${{ github.ref_name }}",
WORKFLOW_SHA: "${{ github.workflow_sha }}",
});
expect(identity.run).toContain(
"node .release-tooling/scripts/release-tooling-identity.mjs verify",
);
expect(target.run).not.toContain('WORKFLOW_REF}" != "refs/heads/main');
expect(target.run).not.toContain('git merge-base --is-ancestor "${WORKFLOW_SHA}" origin/main');
});
it("accepts only the live exact lightweight protected tag for plugin npm preflight", () => {
const workflowSha = "a".repeat(40);
const workflowRef = `release-publish/${workflowSha.slice(0, 12)}-123`;
const workflowFullRef = `refs/tags/${workflowRef}`;
const baseEnv = {
MOCK_TAG_FULL_REF: workflowFullRef,
MOCK_TAG_SHA: workflowSha,
WORKFLOW_FULL_REF: workflowFullRef,
WORKFLOW_REF: workflowRef,
WORKFLOW_SHA: workflowSha,
};
const valid = runPluginNpmPreflightToolingGuard(baseEnv);
expect(valid.status, valid.stderr).toBe(0);
for (const rejected of [
{
name: "moved tag",
env: { ...baseEnv, MOCK_TAG_SHA: "b".repeat(40) },
error: "missing, moved, annotated, or bound to the wrong SHA",
},
{
name: "annotated tag",
env: { ...baseEnv, MOCK_TAG_TYPE: "tag" },
error: "missing, moved, annotated, or bound to the wrong SHA",
},
{
name: "wrong SHA prefix",
env: {
...baseEnv,
MOCK_TAG_FULL_REF: `refs/tags/release-publish/${"b".repeat(12)}-123`,
WORKFLOW_FULL_REF: `refs/tags/release-publish/${"b".repeat(12)}-123`,
WORKFLOW_REF: `release-publish/${"b".repeat(12)}-123`,
},
error: "SHA prefix does not match",
},
{
name: "same-name branch",
env: { ...baseEnv, WORKFLOW_FULL_REF: `refs/heads/${workflowRef}` },
error: "exact tag full ref",
},
]) {
const result = runPluginNpmPreflightToolingGuard(rejected.env);
expect(result.status, rejected.name).toBe(1);
expect(result.stderr, rejected.name).toContain(rejected.error);
}
});
it("binds aggregate preflight consumption to the exact protected tooling tag and SHA", () => {
const workflowSha = "a".repeat(40);
const workflowTag = `release-publish/${workflowSha.slice(0, 12)}-123`;
const valid = runReleasePublishPreflightConsumerGuard({
currentRef: `refs/tags/${workflowTag}`,
currentWorkflowSha: workflowSha,
preflightHeadBranch: workflowTag,
preflightHeadSha: workflowSha,
});
expect(valid.status, valid.stderr).toBe(0);
for (const rejected of [
{
currentRef: `refs/tags/${workflowTag}`,
preflightHeadBranch: `${workflowTag}-wrong`,
preflightHeadSha: workflowSha,
},
{
currentRef: `refs/tags/${workflowTag}`,
preflightHeadBranch: workflowTag,
preflightHeadSha: "b".repeat(40),
},
{
currentRef: `refs/heads/${workflowTag}`,
preflightHeadBranch: workflowTag,
preflightHeadSha: workflowSha,
},
]) {
const result = runReleasePublishPreflightConsumerGuard({
...rejected,
currentWorkflowSha: workflowSha,
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("exact protected release-publish tag");
}
});
it("binds core npm preflight consumption to the exact protected tooling tag and SHA", () => {
const workflowSha = "a".repeat(40);
const workflowTag = `release-publish/${workflowSha.slice(0, 12)}-123`;
const valid = runOpenClawNpmPreflightConsumerGuard({
currentRef: `refs/tags/${workflowTag}`,
currentWorkflowSha: workflowSha,
preflightHeadBranch: workflowTag,
preflightHeadSha: workflowSha,
});
expect(valid.status, valid.stderr).toBe(0);
for (const rejected of [
{
currentRef: `refs/tags/${workflowTag}`,
preflightHeadBranch: `${workflowTag}-wrong`,
preflightHeadSha: workflowSha,
},
{
currentRef: `refs/tags/${workflowTag}`,
preflightHeadBranch: workflowTag,
preflightHeadSha: "b".repeat(40),
},
{
currentRef: `refs/heads/${workflowTag}`,
preflightHeadBranch: workflowTag,
preflightHeadSha: workflowSha,
},
]) {
const result = runOpenClawNpmPreflightConsumerGuard({
...rejected,
currentWorkflowSha: workflowSha,
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("exact protected release-publish tag");
}
});
it("rejects a protected tooling tag moved after request validation and environment approval", () => {
const workflowSha = "a".repeat(40);
const workflowTag = `release-publish/${workflowSha.slice(0, 12)}-123`;
const protectedRef = `refs/tags/${workflowTag}`;
const predecessor = runOpenClawNpmTrustedRefGuard({
MOCK_REMOTE_TAG_SHA: workflowSha,
WORKFLOW_REF: protectedRef,
WORKFLOW_SHA: workflowSha,
});
expect(predecessor.status, predecessor.stderr).toBe(0);
const consumer = runOpenClawNpmPreflightConsumerGuard({
currentRef: protectedRef,
currentWorkflowSha: workflowSha,
liveTagSha: "b".repeat(40),
preflightHeadBranch: workflowTag,
preflightHeadSha: workflowSha,
});
expect(consumer.status).toBe(1);
expect(consumer.stderr).toContain(
"Protected release-publish tag moved after npm-release approval",
);
});
it("uses the canonical tooling identity verifier for token-bootstrap evidence", () => {
const publishJob = workflowJob(PLUGIN_NPM_RELEASE_WORKFLOW, "publish_plugins_npm");
const evidenceStep = workflowStep(publishJob, "Consume immutable npm publication evidence");
expect(evidenceStep.run).toContain("^refs/tags/release-publish/([a-f0-9]{12})-[1-9][0-9]*$");
expect(evidenceStep.run).toContain(
'[[ "$WORKFLOW_REF" == "refs/heads/main" || "$sha_pinned_release_publish" == "true" ]]',
expect(evidenceStep.env?.RELEASE_PUBLISH_RUN_ID).toBe("${{ inputs.release_publish_run_id }}");
expect(evidenceStep.env?.RELEASE_PUBLISH_RUN_ATTEMPT).toBe(
"${{ inputs.release_publish_run_attempt }}",
);
expect(evidenceStep.run).toContain('git merge-base --is-ancestor "$WORKFLOW_SHA" origin/main');
expect(evidenceStep.env?.RELEASE_PUBLISH_PARENT_STATE_POLICY).toBe(
"${{ inputs.release_publish_run_id != '' && (github.actor == 'github-actions[bot]' && 'active' || 'manual-recovery') || '' }}",
);
expect(evidenceStep.run).toContain("node scripts/release-tooling-identity.mjs verify");
expect(evidenceStep.run).toContain('--workflow-ref "$WORKFLOW_HEAD_BRANCH"');
expect(evidenceStep.run).toContain('--workflow-full-ref "$WORKFLOW_REF"');
expect(evidenceStep.run).toContain('--workflow-sha "$WORKFLOW_SHA"');
expect(evidenceStep.run).toContain('--release-publish-run-id "$RELEASE_PUBLISH_RUN_ID"');
expect(evidenceStep.run).toContain(
'--release-publish-run-attempt "$RELEASE_PUBLISH_RUN_ATTEMPT"',
);
expect(evidenceStep.run).toContain(
'--release-publish-parent-state-policy "$RELEASE_PUBLISH_PARENT_STATE_POLICY"',
);
expect(evidenceStep.run).not.toContain("--allow-prevalidated-ref");
});
it("revalidates protected tooling immediately before every core and plugin npm publish", () => {
const corePublish = workflowStep(
workflowJob(OPENCLAW_NPM_RELEASE_WORKFLOW, "publish_openclaw_npm"),
"Publish",
);
expect(corePublish.env).toMatchObject({
GH_TOKEN: "${{ github.token }}",
RELEASE_PUBLISH_PARENT_STATE_POLICY:
"${{ inputs.release_publish_run_id != '' && (github.actor == 'github-actions[bot]' && 'active' || 'manual-recovery') || '' }}",
RELEASE_PUBLISH_RUN_ATTEMPT: "${{ inputs.release_publish_run_attempt }}",
RELEASE_PUBLISH_RUN_ID: "${{ inputs.release_publish_run_id }}",
WORKFLOW_FULL_REF: "${{ github.ref }}",
WORKFLOW_REF: "${{ github.ref_name }}",
WORKFLOW_SHA: "${{ github.workflow_sha }}",
});
expect(corePublish.run).toContain(
"node trusted-workflow/scripts/release-tooling-identity.mjs verify",
);
expect(corePublish.run).toContain("--allow-prevalidated-ref");
expect(corePublish.run).toContain(
'--release-publish-run-attempt "$RELEASE_PUBLISH_RUN_ATTEMPT"',
);
expect(corePublish.run).toContain(
'--release-publish-parent-state-policy "$RELEASE_PUBLISH_PARENT_STATE_POLICY"',
);
expect(corePublish.run).toMatch(
/verify_release_tooling_identity\s+bash scripts\/openclaw-npm-publish\.sh --publish "\.\/\$\{tarball_path\}"/u,
);
expect(corePublish.run).toMatch(
/verify_release_tooling_identity\s+bash scripts\/openclaw-npm-publish\.sh --publish "\$\{publish_target\}"/u,
);
const pluginPublishJob = workflowJob(PLUGIN_NPM_RELEASE_WORKFLOW, "publish_plugins_npm");
const oidcPublish = workflowStep(pluginPublishJob, "Publish with trusted publisher");
expect(oidcPublish.env).toMatchObject({
GH_TOKEN: "${{ github.token }}",
OPENCLAW_RELEASE_PUBLISH_RUN_ATTEMPT: "${{ inputs.release_publish_run_attempt }}",
OPENCLAW_RELEASE_PUBLISH_RUN_ID: "${{ inputs.release_publish_run_id }}",
OPENCLAW_RELEASE_PUBLISH_PARENT_STATE_POLICY:
"${{ inputs.release_publish_run_id != '' && (github.actor == 'github-actions[bot]' && 'active' || 'manual-recovery') || '' }}",
OPENCLAW_RELEASE_TOOLING_ALLOW_PREVALIDATED_REF: "true",
OPENCLAW_RELEASE_TOOLING_FULL_REF: "${{ github.ref }}",
OPENCLAW_RELEASE_TOOLING_IDENTITY_REQUIRED: "true",
OPENCLAW_RELEASE_TOOLING_REF: "${{ github.ref_name }}",
OPENCLAW_RELEASE_TOOLING_REPOSITORY: "${{ github.repository }}",
OPENCLAW_RELEASE_TOOLING_SHA: "${{ github.workflow_sha }}",
});
const bootstrapPublish = workflowStep(pluginPublishJob, "Publish approved bootstrap tarball");
expect(bootstrapPublish.env).toMatchObject({
GH_TOKEN: "${{ github.token }}",
RELEASE_PUBLISH_PARENT_STATE_POLICY:
"${{ inputs.release_publish_run_id != '' && (github.actor == 'github-actions[bot]' && 'active' || 'manual-recovery') || '' }}",
RELEASE_PUBLISH_RUN_ATTEMPT: "${{ inputs.release_publish_run_attempt }}",
RELEASE_PUBLISH_RUN_ID: "${{ inputs.release_publish_run_id }}",
WORKFLOW_FULL_REF: "${{ github.ref }}",
WORKFLOW_REF: "${{ github.ref_name }}",
WORKFLOW_SHA: "${{ github.workflow_sha }}",
});
const identityIndex =
bootstrapPublish.run?.indexOf("node scripts/release-tooling-identity.mjs verify") ?? -1;
const publishIndex = bootstrapPublish.run?.indexOf('npm publish "$TARBALL_PATH"') ?? -1;
expect(identityIndex).toBeGreaterThan(-1);
expect(publishIndex).toBeGreaterThan(identityIndex);
expect(bootstrapPublish.run?.slice(identityIndex, publishIndex)).not.toContain("npm view");
expect(bootstrapPublish.run).toContain(
'--release-publish-parent-state-policy "$RELEASE_PUBLISH_PARENT_STATE_POLICY"',
);
const pluginWrapper = readFileSync("scripts/plugin-npm-publish.sh", "utf8");
expect(pluginWrapper).toContain(
'--release-publish-parent-state-policy "${OPENCLAW_RELEASE_PUBLISH_PARENT_STATE_POLICY:-}"',
);
const distTagIndex = pluginWrapper.indexOf(
'npm dist-tag add "${package_name}@${package_version}"',
);
const distTagIdentityIndex = pluginWrapper.lastIndexOf(
"verify_release_tooling_identity",
distTagIndex,
);
expect(distTagIdentityIndex).toBeGreaterThan(-1);
expect(distTagIndex).toBeGreaterThan(distTagIdentityIndex);
});
it("binds release evidence validation to the exact trusted workflow ref", () => {
for (const [workflowPath, jobName, stepName] of [
[
RELEASE_PUBLISH_WORKFLOW,
"resolve_release_target",
"Validate full release validation manifest",
],
[
OPENCLAW_NPM_RELEASE_WORKFLOW,
"publish_openclaw_npm",
"Verify full release validation evidence",
],
] as const) {
const step = workflowStep(workflowJob(workflowPath, jobName), stepName);
expect(step.env).toMatchObject({
TRUSTED_WORKFLOW_FULL_REF: "${{ github.ref }}",
TRUSTED_WORKFLOW_REF: "${{ github.ref_name }}",
TRUSTED_WORKFLOW_SHA: "${{ github.workflow_sha }}",
});
expect(step.run).toContain("^refs/tags/release-publish/[a-f0-9]{12}-[1-9][0-9]*$");
expect(step.run).toContain('TRUSTED_MAIN_REF="${trusted_workflow_commit_ref}"');
expect(step.run).toContain('--trusted-workflow-ref "$TRUSTED_WORKFLOW_REF"');
expect(step.run).toContain('--trusted-workflow-full-ref "$TRUSTED_WORKFLOW_FULL_REF"');
expect(step.run).toContain('--trusted-workflow-sha "$TRUSTED_WORKFLOW_SHA"');
expect(step.run).toContain('--verifier-source-sha "$');
}
});
it("retries child environment approval when deployment propagation lags", () => {
@@ -5174,6 +5680,7 @@ describe("package artifact reuse", () => {
resolveTargetJob,
"Checkout target package manifest",
);
const toolingIdentity = workflowStep(resolveTargetJob, "Resolve trusted workflow identity");
const releaseInputValidation = workflowStep(resolveTargetJob, "Validate release inputs");
const evidenceReuseStep = workflowStep(evidenceReuseJob, "Find reusable validation evidence");
const releaseChecksDispatchStep = workflowStep(
@@ -5189,6 +5696,14 @@ describe("package artifact reuse", () => {
default: false,
type: "boolean",
},
trusted_workflow_json: {
default: "",
required: false,
type: "string",
},
});
expect(readWorkflow(FULL_RELEASE_VALIDATION_WORKFLOW).env).toMatchObject({
RELEASE_ISOLATION_TOOLING_CONTRACT: "2",
});
expect(workflow).toContain("CHILD_WORKFLOW_REF: ${{ github.ref_name }}");
expect(workflow).toContain('gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1');
@@ -5202,6 +5717,23 @@ describe("package artifact reuse", () => {
expect(resolveTargetSteps.indexOf(targetManifestCheckout)).toBeLessThan(
resolveTargetSteps.indexOf(releaseInputValidation),
);
expect(resolveTargetJob.outputs?.trusted_workflow_json).toBe(
"${{ steps.tooling_identity.outputs.json }}",
);
expect(toolingIdentity.env).toMatchObject({
GH_TOKEN: "${{ github.token }}",
REQUESTED_IDENTITY_JSON: "${{ inputs.trusted_workflow_json }}",
WORKFLOW_CONTRACT: "${{ env.RELEASE_ISOLATION_TOOLING_CONTRACT }}",
WORKFLOW_FULL_REF: "${{ github.ref }}",
WORKFLOW_REF: "${{ github.ref_name }}",
WORKFLOW_SHA: "${{ github.sha }}",
});
expectTextToIncludeAll(toolingIdentity.run, [
"node workflow/scripts/release-tooling-identity.mjs resolve",
'--workflow-contract "$WORKFLOW_CONTRACT"',
'--requested-identity-json "$REQUESTED_IDENTITY_JSON"',
'echo "json=${identity}"',
]);
expectTextToIncludeAll(releaseInputValidation.run, [
'target_version="$(jq -er',
"does not belong to release branch",
@@ -5227,6 +5759,7 @@ describe("package artifact reuse", () => {
NPM_TELEGRAM_PROVIDER_MODE: "${{ inputs.npm_telegram_provider_mode }}",
NPM_TELEGRAM_SCENARIO: "${{ inputs.npm_telegram_scenario }}",
SKIP_PACKAGE_TELEGRAM_E2E: "${{ inputs.skip_package_telegram_e2e }}",
TRUSTED_WORKFLOW_JSON: "${{ needs.resolve_target.outputs.trusted_workflow_json }}",
});
expectTextToIncludeAll(evidenceReuseStep.run, [
"npmTelegramPackageSpec: $npmTelegramPackageSpec",
@@ -5234,6 +5767,12 @@ describe("package artifact reuse", () => {
"npmTelegramScenario: $npmTelegramScenario",
"skipPackageTelegramE2e: $skipPackageTelegramE2e",
"allowUnreleasedChangelog: $allowUnreleasedChangelog",
'trusted_workflow_ref="$(jq -er',
'trusted_workflow_full_ref="$(jq -er',
'trusted_workflow_sha="$(jq -er',
'--trusted-workflow-ref "$trusted_workflow_ref"',
'--trusted-workflow-full-ref "$trusted_workflow_full_ref"',
'--trusted-workflow-sha "$trusted_workflow_sha"',
]);
expect(targetSummaryStep.env).toMatchObject({
SKIP_PACKAGE_TELEGRAM_E2E: "${{ inputs.skip_package_telegram_e2e }}",
@@ -6214,14 +6753,14 @@ describe("package artifact reuse", () => {
expect(trustedTooling.env?.WORKFLOW_SHA).toBe("${{ github.sha }}");
expect(validateManifest.env).toMatchObject({
RUN_JSON_FILE: "${{ runner.temp }}/full-release-validation-run.json",
TRUSTED_MAIN_REF: "refs/remotes/origin/main",
TRUSTED_WORKFLOW_FULL_REF: "${{ github.ref }}",
TRUSTED_WORKFLOW_REF: "${{ github.ref_name }}",
VALIDATOR_FILE:
"${{ runner.temp }}/release-validation-tooling/validate-full-release-validation-evidence.mjs",
STRICT_VALIDATOR_FILE: "${{ runner.temp }}/release-validation-tooling/release-ci-summary.mjs",
});
expect(validateManifest.run).toContain(
'MANIFEST_FILE="$manifest" node "$VALIDATOR_FILE" < "$RUN_JSON_FILE"',
);
expect(validateManifest.run).toContain('MANIFEST_FILE="$manifest"');
expect(validateManifest.run).toContain('node "$VALIDATOR_FILE" < "$RUN_JSON_FILE"');
expect(publishDownload.with?.name).toBe(
"full-release-validation-${{ inputs.full_release_validation_run_id }}-${{ needs.resolve_release_target.outputs.full_release_validation_run_attempt }}",
);
@@ -6661,6 +7200,28 @@ describe("package artifact reuse", () => {
contents: "read",
"id-token": "write",
});
expect(clawHubPublish.with?.trusted_tooling_identity_json).toBeUndefined();
const clawHubPreview = workflowJob(PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "preview_plugins_clawhub");
expect(
readWorkflow(PLUGIN_CLAWHUB_RELEASE_WORKFLOW).on?.workflow_dispatch?.inputs
?.release_publish_run_attempt,
).toBeUndefined();
expect(
readWorkflow(PLUGIN_CLAWHUB_RELEASE_WORKFLOW).on?.workflow_dispatch?.inputs
?.release_publish_full_ref,
).toBeUndefined();
expect(
readWorkflow(PLUGIN_CLAWHUB_RELEASE_WORKFLOW).on?.workflow_dispatch?.inputs
?.release_publish_workflow_sha,
).toBeUndefined();
expect(clawHubPreview.outputs?.trusted_tooling_identity_json).toBeUndefined();
const publishOrchestration = workflowStep(releasePublishJob, "Dispatch publish workflows");
expect(publishOrchestration.env?.PARENT_WORKFLOW_FULL_REF).toBeUndefined();
expect(publishOrchestration.run).toContain(
'wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${TARGET_SHA}"',
);
expect(publishOrchestration.run).not.toContain("release_publish_full_ref");
expect(publishOrchestration.run).not.toContain("release_publish_workflow_sha");
expect(clawHubBootstrapValidation.environment).toBe("clawhub-plugin-bootstrap");
expect(clawHubBootstrapPublish.environment).toBe("clawhub-plugin-bootstrap");
@@ -156,18 +156,32 @@ describe("plugin npm extended-stable workflow", () => {
const preview = workflow().jobs?.preview_plugins_npm;
const previewSteps = preview?.steps ?? [];
const trusted = step(preview, "Validate ref is on a trusted publish branch");
expect(previewSteps.slice(0, 4).map((candidate) => candidate.name)).toEqual([
expect(previewSteps.slice(0, 6).map((candidate) => candidate.name)).toEqual([
"Checkout",
"Checkout trusted preflight tooling",
"Resolve checked-out ref",
"Verify trusted preflight tooling identity",
"Validate ref is on a trusted publish branch",
"Setup Node environment",
]);
const trustedIndex = previewSteps.indexOf(trusted);
expect(trustedIndex).toBe(2);
expect(trustedIndex).toBe(4);
for (const candidate of previewSteps.slice(0, trustedIndex)) {
expect(candidate.uses?.startsWith("./"), candidate.name).not.toBe(true);
expect(candidate.run ?? "", candidate.name).not.toMatch(/\b(?:bun|npm|pnpm)\b/u);
}
const toolingIdentity = step(preview, "Verify trusted preflight tooling identity");
expect(toolingIdentity.env).toMatchObject({
WORKFLOW_FULL_REF: "${{ github.ref }}",
WORKFLOW_REF: "${{ github.ref_name }}",
WORKFLOW_SHA: "${{ github.workflow_sha }}",
});
expect(toolingIdentity.run).toContain(
"node .release-tooling/scripts/release-tooling-identity.mjs verify",
);
expect(toolingIdentity.run).toContain('--workflow-ref "$WORKFLOW_REF"');
expect(toolingIdentity.run).toContain('--workflow-full-ref "$WORKFLOW_FULL_REF"');
expect(toolingIdentity.run).toContain('--workflow-sha "$WORKFLOW_SHA"');
expect(step(preview, "Setup Node environment").uses).toBe("./.github/actions/setup-node-env");
expect(trusted.env).toMatchObject({
PREFLIGHT_ONLY:
@@ -176,6 +190,8 @@ describe("plugin npm extended-stable workflow", () => {
"${{ github.event_name == 'workflow_dispatch' && inputs.trusted_publisher_preflight || false }}",
RELEASE_PUBLISH_RUN_ID:
"${{ github.event_name == 'workflow_dispatch' && inputs.release_publish_run_id || '' }}",
RELEASE_PUBLISH_RUN_ATTEMPT:
"${{ github.event_name == 'workflow_dispatch' && inputs.release_publish_run_attempt || '' }}",
SOURCE_REF: "${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}",
WORKFLOW_REF: "${{ github.ref }}",
WORKFLOW_SHA: "${{ github.workflow_sha }}",
@@ -184,13 +200,13 @@ describe("plugin npm extended-stable workflow", () => {
'[[ "${TRUSTED_PUBLISHER_PREFLIGHT}" == "true" && "${PREFLIGHT_ONLY}" != "true" ]]',
);
expect(trusted.run).toContain("trusted_publisher_preflight requires preflight_only=true");
expect(trusted.run).toContain('[[ "${WORKFLOW_REF}" != "refs/heads/main" ]]');
expect(trusted.run).toContain('git merge-base --is-ancestor "${WORKFLOW_SHA}" origin/main');
expect(trusted.run).toContain('[[ ! "${SOURCE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]');
expect(trusted.run).toContain(
'[[ "$(git rev-parse HEAD)" != "$(git rev-parse "${SOURCE_REF}^{commit}")" ]]',
);
expect(trusted.run).toContain("preflight must not include release_publish_run_id");
expect(trusted.run).toContain(
"Plugin npm preflight must not include a release publish parent run tuple.",
);
const preflightBranchRejection = trusted.run?.indexOf(
"Plugin npm preflight target must be reachable from main or release/*.",
);
@@ -413,7 +429,7 @@ describe("plugin npm extended-stable workflow", () => {
.split("\n")
.filter((line) => line.includes('npm publish "$TARBALL_PATH"'));
expect(gitFetchLines).toHaveLength(6);
expect(gitFetchLines).toHaveLength(5);
expect(
gitFetchLines.every((line) => line.includes("timeout --signal=TERM --kill-after=10s 120s")),
).toBe(true);
@@ -468,18 +484,11 @@ describe("plugin npm extended-stable workflow", () => {
expect(consume.run).toContain("--connect-timeout 10");
expect(consume.run).toContain("--max-time 120");
expect(consume.run).toContain("actions/artifacts/${artifact_id}/zip");
expect(consume.run).toContain("sha_pinned_release_publish=false");
expect(consume.run).toContain(
'[[ "$WORKFLOW_REF" =~ ^refs/tags/release-publish/([a-f0-9]{12})-[1-9][0-9]*$ ]]',
);
expect(consume.run).toContain(
'[[ "$WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ && "${WORKFLOW_SHA:0:12}" == "$workflow_sha_prefix" ]]',
);
expect(consume.run).toContain("sha_pinned_release_publish=true");
expect(consume.run).toContain(
'[[ "$WORKFLOW_REF" == "refs/heads/main" || "$sha_pinned_release_publish" == "true" ]]',
);
expect(consume.run).toContain('git merge-base --is-ancestor "$WORKFLOW_SHA" origin/main');
expect(consume.run).toContain("node scripts/release-tooling-identity.mjs verify");
expect(consume.run).toContain('--workflow-ref "$WORKFLOW_HEAD_BRANCH"');
expect(consume.run).toContain('--workflow-full-ref "$WORKFLOW_REF"');
expect(consume.run).toContain('--workflow-sha "$WORKFLOW_SHA"');
expect(consume.run).toContain('--release-publish-run-id "$RELEASE_PUBLISH_RUN_ID"');
expect(
step(parsed.jobs?.publish_plugins_npm, "Checkout trusted publication tooling").with?.ref,
).toBe("${{ github.workflow_sha }}");
+26 -1
View File
@@ -1,6 +1,6 @@
// Plugin NPM Publish tests cover publish wrapper argument safety.
import { spawnSync } from "node:child_process";
import { chmodSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { delimiter, join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
@@ -41,6 +41,31 @@ function makePackage(version: string): { packageDir: string; path: string; root:
}
describe("plugin npm publish wrapper", () => {
it("revalidates release tooling after preparation and immediately before npm publish", () => {
const source = readFileSync(scriptPath, "utf8");
const buildIndex = source.indexOf("build_package_runtime");
const identityIndex = source.indexOf("\n verify_release_tooling_identity", buildIndex);
const publishIndex = source.indexOf(
'run_with_manifest_overlay "${publish_cmd[@]}"',
identityIndex,
);
expect(buildIndex).toBeGreaterThan(-1);
expect(identityIndex).toBeGreaterThan(buildIndex);
expect(publishIndex).toBeGreaterThan(identityIndex);
expect(source.slice(identityIndex, publishIndex)).not.toContain("npm view");
});
it("revalidates release tooling immediately before every npm dist-tag mutation", () => {
const source = readFileSync(scriptPath, "utf8");
const distTagIndex = source.indexOf('npm dist-tag add "${package_name}@${package_version}"');
const identityIndex = source.lastIndexOf("verify_release_tooling_identity", distTagIndex);
expect(identityIndex).toBeGreaterThan(-1);
expect(distTagIndex).toBeGreaterThan(identityIndex);
expect(source.slice(identityIndex, distTagIndex)).not.toContain("npm view");
});
it("prints help before package or npm checks", () => {
const result = runPluginPublishWrapper(["--help"]);
@@ -13,6 +13,7 @@ import {
candidateCumulativeShippedPullRequests,
candidateParallelsArgs,
candidateParallelsShellCommand,
fullReleaseTrustedWorkflowFields,
githubApi,
isDirectReleaseCandidateExecution,
parseArgs,
@@ -1372,6 +1373,59 @@ describe("release candidate checklist", () => {
).toThrow("refusing to guess from recent workflow_dispatch runs");
});
it("keeps contract 1 callers compatible and sends identity for contract 2", () => {
const workflowSha = "a".repeat(40);
const source = (contract: string, declareIdentity: boolean) => `env:
RELEASE_ISOLATION_TOOLING_CONTRACT: "${contract}"
on:
workflow_dispatch:
inputs:
expected_sha: {}
${declareIdentity ? " trusted_workflow_json: {}\n" : ""}`;
expect(
fullReleaseTrustedWorkflowFields({
workflowRef: "main",
workflowSha,
workflowSource: source("1", false),
}),
).toEqual({});
const fields = fullReleaseTrustedWorkflowFields({
workflowRef: "main",
workflowSha,
workflowSource: source("2", true),
});
expect(JSON.parse(fields.trusted_workflow_json ?? "{}")).toEqual({
ref: "main",
fullRef: "refs/heads/main",
sha: workflowSha,
});
expect(() =>
fullReleaseTrustedWorkflowFields({
workflowRef: "main",
workflowSha,
workflowSource: source("2", false),
}),
).toThrow("contract 2 requires trusted_workflow_json");
for (const contract of ["3", "4"]) {
expect(() =>
fullReleaseTrustedWorkflowFields({
workflowRef: "main",
workflowSha,
workflowSource: source(contract, true),
}),
).toThrow("supported release tooling contract");
}
});
it("threads the selected tooling identity into direct full validation dispatch", () => {
const source = readFileSync("scripts/release-candidate-checklist.mts", "utf8");
expect(source).toContain("const trustedWorkflowFields = fullReleaseTrustedWorkflowFields({");
expect(source).toContain("workflowSha: toolingSha");
expect(source).toContain("...trustedWorkflowFields");
});
it("falls back to a single compatible artifact from the same run", () => {
expect(
resolveArtifactName(
+108
View File
@@ -1196,6 +1196,114 @@ describe("release CI summary child correlation", () => {
});
});
it("accepts canonical SHA-pinned v3 evidence exactly bound to a protected tooling tag", () => {
const workflowSha = "7".repeat(40);
const workflowRef = `release-ci/${workflowSha.slice(0, 12)}-1783705000000`;
const trustedWorkflowRef = `release-publish/${workflowSha.slice(0, 12)}-123`;
const fixture = trustedMainPackageFixture({
manifestVersion: 3,
targetSha: "8".repeat(40),
workflowFullRef: `refs/heads/${workflowRef}`,
workflowRef,
workflowSha,
});
fixture.manifest.targetRef = fixture.targetSha;
expect(
validateReleaseRunEvidence(
{
repository: "openclaw/openclaw",
runId: fixture.runId,
trustedWorkflowFullRef: `refs/tags/${trustedWorkflowRef}`,
trustedWorkflowRef,
trustedWorkflowSha: workflowSha,
verifierSourceContent: readFileSync(SCRIPT),
verifierSourceSha: "c".repeat(40),
},
fixture.client,
),
).toMatchObject({
producerOnTrustedMainLineage: false,
trustedWorkflowFullRef: `refs/tags/${trustedWorkflowRef}`,
trustedWorkflowRef,
root: {
workflowRef,
workflowRefProof: "manifest-v3-protected-tag-exact-sha",
workflowSha,
},
});
});
it("rejects protected-tag evidence from a same-name branch or older ancestor", () => {
const trustedWorkflowSha = "7".repeat(40);
const trustedWorkflowRef = `release-publish/${trustedWorkflowSha.slice(0, 12)}-123`;
const validFixture = trustedMainPackageFixture({
manifestVersion: 3,
workflowSha: trustedWorkflowSha,
});
expect(() =>
validateReleaseRunEvidence(
{
repository: "openclaw/openclaw",
runId: validFixture.runId,
trustedWorkflowFullRef: `refs/heads/${trustedWorkflowRef}`,
trustedWorkflowRef,
trustedWorkflowSha,
verifierSourceContent: readFileSync(SCRIPT),
verifierSourceSha: "c".repeat(40),
},
validFixture.client,
),
).toThrow("must be a protected tag");
const olderWorkflowSha = "6".repeat(40);
const olderWorkflowRef = `release-ci/${olderWorkflowSha.slice(0, 12)}-1783705000000`;
const olderFixture = trustedMainPackageFixture({
manifestVersion: 3,
targetSha: "8".repeat(40),
workflowFullRef: `refs/heads/${olderWorkflowRef}`,
workflowRef: olderWorkflowRef,
workflowSha: olderWorkflowSha,
});
olderFixture.manifest.targetRef = olderFixture.targetSha;
expect(() =>
validateReleaseRunEvidence(
{
repository: "openclaw/openclaw",
runId: olderFixture.runId,
trustedWorkflowFullRef: `refs/tags/${trustedWorkflowRef}`,
trustedWorkflowRef,
trustedWorkflowSha,
verifierSourceContent: readFileSync(SCRIPT),
verifierSourceSha: "c".repeat(40),
},
olderFixture.client,
),
).toThrow("does not match trusted tooling");
const sameNameFixture = trustedMainPackageFixture({
manifestVersion: 3,
workflowFullRef: `refs/heads/${trustedWorkflowRef}`,
workflowRef: trustedWorkflowRef,
workflowSha: trustedWorkflowSha,
});
expect(() =>
validateReleaseRunEvidence(
{
repository: "openclaw/openclaw",
runId: sameNameFixture.runId,
trustedWorkflowFullRef: `refs/tags/${trustedWorkflowRef}`,
trustedWorkflowRef,
trustedWorkflowSha,
verifierSourceContent: readFileSync(SCRIPT),
verifierSourceSha: "c".repeat(40),
},
sameNameFixture.client,
),
).toThrow("canonical release-ci branch");
});
it.each(["main", "refs/heads/main"])(
"accepts a REST workflow path qualified with %s",
(qualifiedRef) => {
@@ -0,0 +1,361 @@
import { describe, expect, it, vi } from "vitest";
import {
resolveReleaseToolingIdentity,
validateReleasePublishParentRun,
validateReleaseToolingIdentity,
verifyReleaseToolingIdentity,
} from "../../scripts/release-tooling-identity.mjs";
const SHA = "a".repeat(40);
const OTHER_SHA = "b".repeat(40);
const RUN_ID = "12345";
const PARENT_RUN_ID = "67890";
const PARENT_RUN_ATTEMPT = "2";
const REF = `release-publish/${SHA.slice(0, 12)}-${RUN_ID}`;
const FULL_REF = `refs/tags/${REF}`;
function protectedIdentity(
overrides: Partial<Parameters<typeof verifyReleaseToolingIdentity>[0]> = {},
) {
return {
repository: "openclaw/openclaw",
workflowFullRef: FULL_REF,
workflowRef: REF,
workflowSha: SHA,
...overrides,
};
}
describe("release tooling identity", () => {
it.each([
["1", "main", "refs/heads/main"],
["2", "release/2026.8.1", "refs/heads/release/2026.8.1"],
["2", "tideclaw/alpha/2026-08-21-1200Z", "refs/heads/tideclaw/alpha/2026-08-21-1200Z"],
])("derives contract %s identity for safe direct workflow ref %s", (contract, ref, fullRef) => {
expect(
resolveReleaseToolingIdentity({
workflowContract: contract,
workflowFullRef: fullRef,
workflowRef: ref,
workflowSha: SHA,
}),
).toEqual({ fullRef, ref, sha: SHA });
});
it("rejects unsupported contract 3 even with explicit identity", () => {
expect(() =>
resolveReleaseToolingIdentity({
requestedIdentityJson: JSON.stringify({
ref: "main",
fullRef: "refs/heads/main",
sha: SHA,
}),
workflowContract: "3",
workflowFullRef: "refs/heads/main",
workflowRef: "main",
workflowSha: SHA,
}),
).toThrow("release tooling contract 3 is not supported");
});
it.each([
[
"release-ci ref",
{
workflowContract: "2",
workflowFullRef: `refs/heads/release-ci/${SHA.slice(0, 12)}-123`,
workflowRef: `release-ci/${SHA.slice(0, 12)}-123`,
},
],
[
"protected tag",
{
workflowContract: "2",
workflowFullRef: FULL_REF,
workflowRef: REF,
},
],
])("requires explicit identity for $0", (_label, overrides) => {
const { workflowContract, workflowFullRef } = overrides;
const workflowRef = "workflowRef" in overrides ? overrides.workflowRef : "main";
expect(() =>
resolveReleaseToolingIdentity({
workflowContract,
workflowFullRef,
workflowRef,
workflowSha: SHA,
}),
).toThrow(/requires explicit trusted workflow identity|require explicit trusted workflow/u);
});
it("accepts explicit main identity for a matching release-ci workflow", () => {
const releaseCiRef = `release-ci/${SHA.slice(0, 12)}-123`;
expect(
resolveReleaseToolingIdentity({
requestedIdentityJson: JSON.stringify({
ref: "main",
fullRef: "refs/heads/main",
sha: SHA,
}),
workflowContract: "2",
workflowFullRef: `refs/heads/${releaseCiRef}`,
workflowRef: releaseCiRef,
workflowSha: SHA,
}),
).toEqual({ ref: "main", fullRef: "refs/heads/main", sha: SHA });
});
it("rejects explicit identity that does not match a direct workflow", () => {
expect(() =>
resolveReleaseToolingIdentity({
requestedIdentityJson: JSON.stringify({
ref: "main",
fullRef: "refs/heads/main",
sha: OTHER_SHA,
}),
workflowContract: "2",
workflowFullRef: "refs/heads/main",
workflowRef: "main",
workflowSha: SHA,
}),
).toThrow("must match the executing workflow ref and SHA");
});
it("accepts only the live exact lightweight protected tag", () => {
const runGh = vi.fn(() =>
JSON.stringify({
ref: FULL_REF,
object: { sha: SHA, type: "commit" },
}),
);
expect(verifyReleaseToolingIdentity({ ...protectedIdentity(), runGh })).toEqual({
fullRef: FULL_REF,
ref: REF,
route: "protected-tag",
sha: SHA,
});
expect(runGh).toHaveBeenCalledWith([
"api",
`repos/openclaw/openclaw/git/ref/tags/${REF}`,
"--method",
"GET",
]);
});
it.each([
[
"moved tag",
{
runGh: () =>
JSON.stringify({
ref: FULL_REF,
object: { sha: OTHER_SHA, type: "commit" },
}),
},
"missing, moved, annotated, or bound to the wrong SHA",
],
[
"deleted tag",
{
runGh: () => {
throw new Error("HTTP 404");
},
},
"missing or unreadable",
],
[
"annotated tag",
{
runGh: () =>
JSON.stringify({
ref: FULL_REF,
object: { sha: OTHER_SHA, type: "tag" },
}),
},
"missing, moved, annotated, or bound to the wrong SHA",
],
[
"wrong SHA prefix",
{
workflowRef: `release-publish/${OTHER_SHA.slice(0, 12)}-${RUN_ID}`,
workflowFullRef: `refs/tags/release-publish/${OTHER_SHA.slice(0, 12)}-${RUN_ID}`,
},
"SHA prefix does not match",
],
["same-name branch", { workflowFullRef: `refs/heads/${REF}` }, "exact tag full ref"],
])("rejects $0", (_label, overrides, expectedError) => {
expect(() =>
verifyReleaseToolingIdentity({
...protectedIdentity(),
...overrides,
}),
).toThrow(expectedError);
});
it.each(["ahead", "identical"])(
"accepts main tooling reachable from current main: %s",
(status) => {
const runGh = vi.fn(() => JSON.stringify({ status }));
expect(
verifyReleaseToolingIdentity({
repository: "openclaw/openclaw",
runGh,
workflowFullRef: "refs/heads/main",
workflowRef: "main",
workflowSha: SHA,
}),
).toMatchObject({ route: "main", sha: SHA });
},
);
it("rejects main tooling outside current main ancestry", () => {
expect(() =>
validateReleaseToolingIdentity({
mainComparisonStatus: "diverged",
workflowFullRef: "refs/heads/main",
workflowRef: "main",
workflowSha: SHA,
}),
).toThrow("not reachable from current main");
});
it("preserves explicitly prevalidated non-main branch routes", () => {
const runGh = vi.fn(() =>
JSON.stringify({
ref: "refs/heads/release/2026.8.1",
object: { sha: SHA, type: "commit" },
}),
);
expect(
verifyReleaseToolingIdentity({
allowPrevalidatedRef: true,
repository: "openclaw/openclaw",
runGh,
workflowFullRef: "refs/heads/release/2026.8.1",
workflowRef: "release/2026.8.1",
workflowSha: SHA,
}),
).toMatchObject({ route: "prevalidated-branch" });
expect(runGh).toHaveBeenCalledWith([
"api",
"repos/openclaw/openclaw/git/ref/heads/release/2026.8.1",
"--method",
"GET",
]);
});
it("rejects a prevalidated branch moved after approval", () => {
expect(() =>
verifyReleaseToolingIdentity({
allowPrevalidatedRef: true,
repository: "openclaw/openclaw",
runGh: () =>
JSON.stringify({
ref: "refs/heads/release/2026.8.1",
object: { sha: OTHER_SHA, type: "commit" },
}),
workflowFullRef: "refs/heads/release/2026.8.1",
workflowRef: "release/2026.8.1",
workflowSha: SHA,
}),
).toThrow("branch is missing or moved");
});
it("binds a distinct current parent run independently from tag provenance", () => {
const calls: string[][] = [];
const runGh = vi.fn((args: string[]) => {
calls.push(args);
if (args[1]?.includes("/git/ref/tags/")) {
return JSON.stringify({
ref: FULL_REF,
object: { sha: SHA, type: "commit" },
});
}
return JSON.stringify({
id: Number(PARENT_RUN_ID),
run_attempt: Number(PARENT_RUN_ATTEMPT),
repository: { full_name: "openclaw/openclaw" },
path: `.github/workflows/openclaw-release-publish.yml@${FULL_REF}`,
event: "workflow_dispatch",
head_branch: REF,
head_sha: SHA,
status: "in_progress",
conclusion: null,
});
});
expect(
verifyReleaseToolingIdentity({
...protectedIdentity(),
releasePublishParentStatePolicy: "active",
releasePublishRunAttempt: PARENT_RUN_ATTEMPT,
releasePublishRunId: PARENT_RUN_ID,
runGh,
}),
).toMatchObject({ route: "protected-tag", sha: SHA });
expect(PARENT_RUN_ID).not.toBe(RUN_ID);
expect(calls).toContainEqual([
"api",
`repos/openclaw/openclaw/actions/runs/${PARENT_RUN_ID}`,
"--method",
"GET",
]);
});
it.each([
["active", "in_progress", null, true],
["active", "completed", "success", false],
["active-or-success", "in_progress", null, true],
["active-or-success", "completed", "success", true],
["active-or-success", "completed", "failure", false],
["manual-recovery", "in_progress", null, true],
["manual-recovery", "completed", "success", true],
["manual-recovery", "completed", "failure", true],
["manual-recovery", "completed", "cancelled", false],
] as const)(
"enforces parent state policy %s for %s/%s",
(releasePublishParentStatePolicy, status, conclusion, accepted) => {
const validate = () =>
validateReleasePublishParentRun({
identity: { ref: REF, fullRef: FULL_REF, sha: SHA },
releasePublishParentStatePolicy,
releasePublishRunAttempt: PARENT_RUN_ATTEMPT,
releasePublishRunId: PARENT_RUN_ID,
repository: "openclaw/openclaw",
run: {
id: Number(PARENT_RUN_ID),
run_attempt: Number(PARENT_RUN_ATTEMPT),
repository: { full_name: "openclaw/openclaw" },
path: `.github/workflows/openclaw-release-publish.yml@${FULL_REF}`,
event: "workflow_dispatch",
head_branch: REF,
head_sha: SHA,
status,
conclusion,
},
});
if (accepted) {
expect(validate).not.toThrow();
} else {
expect(validate).toThrow(`state is not allowed by ${releasePublishParentStatePolicy}`);
}
},
);
it("requires the parent state policy with the exact parent run tuple", () => {
expect(() =>
verifyReleaseToolingIdentity({
...protectedIdentity(),
releasePublishRunAttempt: PARENT_RUN_ATTEMPT,
releasePublishRunId: PARENT_RUN_ID,
runGh: () =>
JSON.stringify({
ref: FULL_REF,
object: { sha: SHA, type: "commit" },
}),
}),
).toThrow("run id, attempt, and parent state policy must be provided together");
});
});
@@ -125,6 +125,78 @@ describe("full release validation evidence", () => {
expect(isShaPinnedReleaseValidationBranch(pinnedBranch)).toBe(true);
});
it("accepts canonical SHA-pinned evidence exactly bound to a protected tooling tag", () => {
const isTrustedMainAncestor = vi.fn(() => false);
const trustedWorkflowRef = `release-publish/${workflowSha.slice(0, 12)}-123`;
const result = validateFullReleaseValidationEvidence({
run: releaseRun(),
manifest: releaseManifest(),
expectedRepository: "openclaw/openclaw",
expectedRunId: "123",
expectedTargetSha: targetSha,
expectedTrustedWorkflowFullRef: `refs/tags/${trustedWorkflowRef}`,
expectedTrustedWorkflowSha: workflowSha,
isTrustedMainAncestor,
});
expect(result.source).toBe("sha-pinned-protected-tag");
expect(isTrustedMainAncestor).not.toHaveBeenCalled();
});
it("rejects protected-tag evidence from a same-name branch or older ancestor", () => {
const trustedWorkflowRef = `release-publish/${workflowSha.slice(0, 12)}-123`;
expect(() =>
validateFullReleaseValidationEvidence({
run: releaseRun(),
manifest: releaseManifest(),
expectedRepository: "openclaw/openclaw",
expectedRunId: "123",
expectedTargetSha: targetSha,
expectedTrustedWorkflowFullRef: `refs/heads/${trustedWorkflowRef}`,
expectedTrustedWorkflowSha: workflowSha,
isTrustedMainAncestor: () => true,
}),
).toThrow("must be an exact protected tag");
const olderWorkflowSha = "c".repeat(40);
const olderBranch = `release-ci/${olderWorkflowSha.slice(0, 12)}-1783705000000`;
expect(() =>
validateFullReleaseValidationEvidence({
run: releaseRun({
head_branch: olderBranch,
head_sha: olderWorkflowSha,
}),
manifest: releaseManifest({
workflowFullRef: `refs/heads/${olderBranch}`,
workflowRef: olderBranch,
workflowSha: olderWorkflowSha,
}),
expectedRepository: "openclaw/openclaw",
expectedRunId: "123",
expectedTargetSha: targetSha,
expectedTrustedWorkflowFullRef: `refs/tags/${trustedWorkflowRef}`,
expectedTrustedWorkflowSha: workflowSha,
isTrustedMainAncestor: () => true,
}),
).toThrow("does not match trusted tooling");
expect(() =>
validateFullReleaseValidationEvidence({
run: releaseRun({ head_branch: trustedWorkflowRef }),
manifest: releaseManifest({
workflowFullRef: `refs/heads/${trustedWorkflowRef}`,
workflowRef: trustedWorkflowRef,
}),
expectedRepository: "openclaw/openclaw",
expectedRunId: "123",
expectedTargetSha: targetSha,
expectedTrustedWorkflowFullRef: `refs/tags/${trustedWorkflowRef}`,
expectedTrustedWorkflowSha: workflowSha,
isTrustedMainAncestor: () => true,
}),
).toThrow("canonical release-ci producer branch");
});
it.each([pinnedBranch, `refs/heads/${pinnedBranch}`])(
"accepts a REST workflow path qualified with %s",
(qualifiedRef) => {
@@ -15,6 +15,8 @@ function runApprovalScript(
CHILD_WORKFLOW_SHA?: string;
DIRECT_RELEASE_RECOVERY?: string;
EXPECTED_WORKFLOW_BRANCH?: string;
EXPECTED_WORKFLOW_FULL_REF?: string;
EXPECTED_WORKFLOW_SHA?: string;
EXPECTED_RUN_ATTEMPT?: string;
APPROVAL_PATH?: string;
GITHUB_REPOSITORY?: string;
@@ -34,6 +36,8 @@ function runApprovalScript(
CHILD_WORKFLOW_SHA: env.CHILD_WORKFLOW_SHA ?? "b".repeat(40),
DIRECT_RELEASE_RECOVERY: env.DIRECT_RELEASE_RECOVERY ?? "false",
EXPECTED_WORKFLOW_BRANCH: env.EXPECTED_WORKFLOW_BRANCH ?? "release/2026.6.21",
EXPECTED_WORKFLOW_FULL_REF: env.EXPECTED_WORKFLOW_FULL_REF ?? "",
EXPECTED_WORKFLOW_SHA: env.EXPECTED_WORKFLOW_SHA ?? "",
EXPECTED_RUN_ATTEMPT: env.EXPECTED_RUN_ATTEMPT ?? "",
APPROVAL_PATH: env.APPROVAL_PATH ?? "",
GITHUB_REPOSITORY: env.GITHUB_REPOSITORY ?? "openclaw/openclaw",
@@ -71,6 +75,7 @@ function approvalRun(overrides: Record<string, unknown> = {}) {
conclusion: null,
event: "workflow_dispatch",
headBranch: "release/2026.6.21",
repository: "openclaw/openclaw",
status: "in_progress",
url: "https://github.com/openclaw/openclaw/actions/runs/123",
workflowName: "OpenClaw Release Publish",
@@ -123,6 +128,27 @@ describe("scripts/validate-release-publish-approval.mjs", () => {
expect(result.stdout).toBe("");
});
it("binds the parent repository, workflow path, full ref, SHA, and attempt", () => {
const workflowSha = "d".repeat(40);
const fullRef = "refs/tags/release-publish/aaaaaaaaaaaa-111";
const result = runApprovalScript(
approvalRun({
headBranch: "release-publish/aaaaaaaaaaaa-111",
headSha: workflowSha,
path: `.github/workflows/openclaw-release-publish.yml@${fullRef}`,
runAttempt: 7,
}),
{
EXPECTED_RUN_ATTEMPT: "7",
EXPECTED_WORKFLOW_BRANCH: "release-publish/aaaaaaaaaaaa-111",
EXPECTED_WORKFLOW_FULL_REF: fullRef,
EXPECTED_WORKFLOW_SHA: workflowSha,
},
);
expect(result.status, result.stderr).toBe(0);
});
it("rejects completed runs for normal approval handoff", () => {
const result = runApprovalScript(approvalRun({ conclusion: "success", status: "completed" }));