mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
fa86caf94f
* 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
362 lines
11 KiB
TypeScript
362 lines
11 KiB
TypeScript
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");
|
|
});
|
|
});
|