fix(release): preserve frozen candidate identity

This commit is contained in:
Vincent Koc
2026-08-11 10:30:56 +08:00
parent abb856ece4
commit c3e3f276f2
9 changed files with 400 additions and 48 deletions
+72 -7
View File
@@ -7085,19 +7085,84 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
},
);
it("pins transient validation refs before trusted Telegram QA dispatch", () => {
it("keeps exact release validation identity separate from release context", () => {
const fullReleaseWorkflow = readWorkflow(".github/workflows/full-release-validation.yml");
const releaseWorkflow = readReleaseChecksWorkflow();
const telegramWorkflow = readWorkflow(".github/workflows/openclaw-release-telegram-qa.yml");
const fullReleaseDispatchStep = fullReleaseWorkflow.jobs.release_checks.steps.find(
(step: WorkflowStep) => step.name === "Dispatch and monitor release checks",
);
const dispatchStep = releaseWorkflow.jobs.qa_live_telegram_release_checks.steps.find(
(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",
);
const provenanceSteps = [
telegramWorkflow.jobs.build_candidate.steps.find(
(step: WorkflowStep) => step.name === "Validate candidate release provenance",
),
telegramWorkflow.jobs.run_telegram.steps.find(
(step: WorkflowStep) => step.name === "Revalidate candidate release provenance",
),
];
expect(dispatchStep.env.TARGET_REF).toBe("${{ needs.resolve_target.outputs.ref }}");
expect(fullReleaseWorkflow.on.workflow_dispatch.inputs.target_context_ref).toMatchObject({
required: false,
default: "",
type: "string",
});
expect(fullReleaseDispatchStep.run).toContain('-f ref="$TARGET_SHA"');
expect(fullReleaseDispatchStep.run).toContain('-f target_context_ref="$TARGET_CONTEXT_REF"');
expect(fullReleaseDispatchStep.run).not.toContain(
'release_checks_target_ref="${TARGET_CONTEXT_REF:-$TARGET_REF}"',
);
expect(releaseWorkflow.on.workflow_dispatch.inputs.target_context_ref).toMatchObject({
required: false,
default: "",
type: "string",
});
expect(telegramWorkflow.on.workflow_dispatch.inputs.target_context_ref).toMatchObject({
required: false,
default: "",
type: "string",
});
expect(dispatchStep.env.TARGET_SHA).toBe("${{ needs.resolve_target.outputs.revision }}");
expect(dispatchStep.run).toContain('telegram_target_ref="$TARGET_REF"');
expect(dispatchStep.run).toContain("validation/target-* | refs/heads/validation/target-*)");
expect(dispatchStep.run).toContain('telegram_target_ref="$TARGET_SHA"');
expect(dispatchStep.run).toContain('-f target_ref="$telegram_target_ref"');
expect(dispatchStep.run).not.toContain("release/* | refs/heads/release/*)");
expect(dispatchStep.env.TARGET_CONTEXT_REF).toBe("${{ inputs.target_context_ref }}");
expect(dispatchStep.run).toContain('-f target_context_ref="$TARGET_CONTEXT_REF"');
expect(dispatchStep.run).toContain('-f target_ref="$TARGET_SHA"');
expect(dispatchStep.run).not.toContain("telegram_target_ref=");
expect(identityStep.run).toContain(
"Telegram QA target context must be a canonical release branch or tag.",
);
expect(identityStep.run).toContain(
"Telegram QA release context requires an exact-SHA target ref.",
);
for (const provenanceStep of provenanceSteps) {
expect(provenanceStep.env.TARGET_CONTEXT_REF).toBe("${{ inputs.target_context_ref }}");
expect(provenanceStep.run).toContain("frozen-release-branch-head");
expect(provenanceStep.run).toContain(
'elif [[ "$candidate_version" =~ ^${release_version_pattern}-beta\\.[0-9]+$ ]]',
);
expect(provenanceStep.run).toContain(
'frozen_release_branch_pattern="^release/${candidate_version_pattern}-code-frozen(-r[1-9][0-9]*)?$"',
);
expect(provenanceStep.run).toContain('elif [[ -z "$frozen_release_branch_pattern" ]]; then');
expect(provenanceStep.run).toContain(
"Telegram candidate version ${candidate_version} does not belong to release ${release_version}.",
);
expect(provenanceStep.run).toContain(
"Telegram candidate version ${candidate_version} does not match context ${normalized_context_ref}.",
);
expect(provenanceStep.run).toContain('context_release_branch="$normalized_context_ref"');
expect(provenanceStep.run).toContain('context_release_tag="$normalized_context_ref"');
expect(provenanceStep.run).toContain(
"Frozen release candidate ${candidate_sha} requires a valid maintainer signature.",
);
expect(provenanceStep.run).toContain(
'select(.state == "OPEN" and .headRepository.nameWithOwner == $repo and',
);
}
});
it("keeps maturity scorecard release docs opt-in from release checks", () => {
+14
View File
@@ -2376,6 +2376,14 @@ docker_e2e_docker_run_cmd run demo
expect(publishedRunner).toContain(
'assert-prepublish-requests "$OPENCLAW_CLAWHUB_URL" "$prepublish_package" "$candidate_version"',
);
expect(publishedRunner).toContain(
'local tarball="$fixture_root/openclaw-brave-plugin-${candidate_version}.tgz"',
);
expect(publishedRunner).toContain('FIXTURE_PACKAGE_VERSION="$candidate_version"');
expect(publishedRunner).toContain("version,");
expect(publishedRunner).toContain(
'registry_args+=("@openclaw/brave-plugin" "$candidate_version" "$tarball")',
);
expect(publishedRunner).toContain('"$clawhub_security_mode"');
expect(publishedRunner.indexOf("phase assert-prepublish-requests node")).toBeLessThan(
publishedRunner.indexOf("phase doctor run_doctor"),
@@ -3328,6 +3336,7 @@ grep -Fxq preserved "$TMPDIR/caller-fd"
it("wires the Codex npm plugin live assertion boundary into Docker", () => {
const runner = readFileSync(CODEX_NPM_PLUGIN_LIVE_DOCKER_E2E_PATH, "utf8");
const assertions = readFileSync("scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs", "utf8");
expectTextToIncludeAll(runner, [
"docker_e2e_print_log /tmp/openclaw-codex-plugin-pack.log",
"scripts/e2e/lib/plugins/npm-registry-server.mjs",
@@ -3350,6 +3359,11 @@ grep -Fxq preserved "$TMPDIR/caller-fd"
expect(runner).not.toContain("trap 'openclaw_e2e_stop_process \"${registry_pid:-}\"' EXIT");
expect(runner).not.toContain("final=false");
expect(runner).not.toContain("--timeout 420");
expectTextToIncludeAll(assertions, [
'Requested agent harness "codex" is not registered',
"Unknown model: codex/",
'Agent harness runtime "codex" is not present in the prepared registry.',
]);
});
it("prints the OpenAI chat-tools gateway log when startup exits early", () => {
@@ -75,6 +75,7 @@ function runIdentityVerification(params: {
invocation?: "dispatch" | "reusable";
oidcJobWorkflowSha?: string;
oidcWorkflowSha?: string;
targetContextRef?: string;
workflowSha?: string;
}) {
const repository = "openclaw/openclaw";
@@ -143,7 +144,8 @@ function runIdentityVerification(params: {
workflow_sha: params.expectedTrustedWorkflowSha,
}),
PATH: `${fakeBin}:${process.env.PATH}`,
TARGET_REF: "refs/heads/release/2026.7.1",
TARGET_CONTEXT_REF: params.targetContextRef ?? "",
TARGET_REF: params.targetContextRef ? "a".repeat(40) : "refs/heads/release/2026.7.1",
TARGET_SHA: "a".repeat(40),
WORKFLOW_REF: workflowRef,
WORKFLOW_SHA: workflowSha,
@@ -210,11 +212,33 @@ function runAdvisoryStatus(overrides: Record<string, string> = {}) {
};
}
function runCandidateProvenance(params: { openPr?: boolean; unsignedWebFlow?: boolean } = {}) {
function runCandidateProvenance(
params: {
branchHead?: string;
candidateVersion?: string;
openPr?: boolean;
remoteSha?: string;
targetContextRef?: string;
unsignedWebFlow?: boolean;
} = {},
) {
const candidateSha = "a".repeat(40);
const targetContextRef = params.targetContextRef ?? "";
const normalizedContextRef = targetContextRef
.replace(/^refs\/heads\//u, "")
.replace(/^refs\/tags\//u, "");
const branchHead = params.branchHead ?? "release/2026.7.1-beta.3-code-frozen-r1";
const remoteRef = normalizedContextRef.startsWith("v")
? `refs/tags/${normalizedContextRef}`
: `refs/heads/${normalizedContextRef || "release/2026.7.1"}`;
const workdir = tempDirs.make("openclaw-telegram-provenance-");
const fakeBin = join(workdir, "bin");
mkdirSync(fakeBin);
mkdirSync(join(workdir, ".candidate"));
writeFileSync(
join(workdir, ".candidate", "package.json"),
JSON.stringify({ version: params.candidateVersion ?? "2026.7.1-beta.3" }),
);
const metadata = {
data: {
repository: {
@@ -256,6 +280,7 @@ function runCandidateProvenance(params: { openPr?: boolean; unsignedWebFlow?: bo
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$*" == *"api graphql"* ]]; then printf '%s\\n' "$FAKE_METADATA"; exit 0; fi
if [[ "$*" == *"/branches-where-head"* ]]; then printf '%s\\n' "$FAKE_BRANCH_HEAD"; exit 0; fi
if [[ "$*" == *"/compare/"* ]]; then printf '%s\\n' "behind"; exit 0; fi
if [[ "$*" == *"/collaborators/release-maintainer/permission"* ]]; then printf '%s\\n' '{"permission":"write","role_name":"maintain"}'; exit 0; fi
exit 64
@@ -267,7 +292,11 @@ exit 64
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$*" == *"rev-parse HEAD"* ]]; then printf '%s\\n' "$TARGET_SHA"; exit 0; fi
if [[ "$*" == *"ls-remote"* ]]; then printf '%s\\trefs/heads/release/2026.7.1\\n' "$TARGET_SHA"; exit 0; fi
if [[ "$*" == *"ls-remote"* ]]; then
if [[ "$*" == *"refs/tags/"* && "$FAKE_REMOTE_REF" != refs/tags/* ]]; then exit 0; fi
printf '%s\\t%s\\n' "$FAKE_REMOTE_SHA" "$FAKE_REMOTE_REF"
exit 0
fi
exit 64
`,
{ mode: 0o755 },
@@ -280,11 +309,15 @@ exit 64
encoding: "utf8",
env: {
...process.env,
FAKE_BRANCH_HEAD: branchHead,
FAKE_METADATA: JSON.stringify(metadata),
FAKE_REMOTE_REF: remoteRef,
FAKE_REMOTE_SHA: params.remoteSha ?? candidateSha,
GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN: "HTTP 5[0-9][0-9]",
GITHUB_REPOSITORY: "openclaw/openclaw",
PATH: `${fakeBin}:${process.env.PATH}`,
TARGET_REF: "refs/heads/release/2026.7.1",
TARGET_CONTEXT_REF: targetContextRef,
TARGET_REF: targetContextRef ? candidateSha : "refs/heads/release/2026.7.1",
TARGET_SHA: candidateSha,
},
},
@@ -346,6 +379,18 @@ describe("release Telegram QA workflow", () => {
it("accepts only the resolved trusted workflow identity", () => {
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",
"v2026.7.1",
"v2026.7.1-beta.3",
]) {
const accepted = runIdentityVerification({
expectedTrustedWorkflowSha: trustedSha,
targetContextRef,
});
expect(accepted.status, `${targetContextRef}: ${accepted.stderr}`).toBe(0);
}
expect(
runIdentityVerification({
expectedTrustedWorkflowSha: trustedSha,
@@ -373,6 +418,64 @@ describe("release Telegram QA workflow", () => {
expect(openPr.stderr).toContain("open same-repository PR head");
});
it("requires canonical signed frozen heads for beta release contexts", () => {
const matching = runCandidateProvenance({
candidateVersion: "2026.7.1-beta.3",
targetContextRef: "release/2026.7.1",
});
expect(matching.status, matching.stderr).toBe(0);
const unsigned = runCandidateProvenance({
candidateVersion: "2026.7.1-beta.3",
targetContextRef: "release/2026.7.1",
unsignedWebFlow: true,
});
expect(unsigned.status).toBe(1);
expect(unsigned.stderr).toContain("requires a valid maintainer signature");
const legacyFrozen = runCandidateProvenance({
branchHead: "release/2026.7.1-beta.3-frozen-r1",
candidateVersion: "2026.7.1-beta.3",
targetContextRef: "release/2026.7.1",
});
expect(legacyFrozen.status).toBe(1);
const alpha = runCandidateProvenance({
candidateVersion: "2026.7.1-alpha.1",
targetContextRef: "release/2026.7.1",
});
expect(alpha.status).toBe(1);
expect(alpha.stderr).toContain(
"Telegram candidate version 2026.7.1-alpha.1 does not belong to release 2026.7.1.",
);
});
it("binds every release context to candidate version and SHA", () => {
for (const [targetContextRef, candidateVersion] of [
["release/2026.7.1", "2026.7.1"],
["extended-stable/2026.7.33", "2026.7.33"],
["v2026.7.1", "2026.7.1"],
["v2026.7.1-alpha.2", "2026.7.1-alpha.2"],
["v2026.7.1-beta.3", "2026.7.1-beta.3"],
] as const) {
const accepted = runCandidateProvenance({ candidateVersion, targetContextRef });
expect(accepted.status, `${targetContextRef}: ${accepted.stderr}`).toBe(0);
const versionMismatch = runCandidateProvenance({
candidateVersion: "2026.8.1",
targetContextRef,
});
expect(versionMismatch.status, targetContextRef).toBe(1);
const shaMismatch = runCandidateProvenance({
candidateVersion,
remoteSha: "b".repeat(40),
targetContextRef,
});
expect(shaMismatch.status, targetContextRef).toBe(1);
}
});
it("writes terminal evidence only for complete successful producers", () => {
const success = runAdvisoryStatus();
expect(success.result.status, success.result.stderr).toBe(0);
@@ -489,11 +489,9 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => {
expect(normalCiScript).toContain('args+=(-f historical_target_tag="$TARGET_REF")');
expect(normalCiScript).toContain('args+=(-f historical_target_tag="$TARGET_CONTEXT_REF")');
expect(normalCiScript).toContain('args+=(-f release_candidate_ref="$TARGET_CONTEXT_REF")');
expect(releaseChecksScript).toContain(
'release_checks_target_ref="${TARGET_CONTEXT_REF:-$TARGET_REF}"',
);
expect(releaseChecksStep.env?.TARGET_CONTEXT_REF).toBe("${{ inputs.target_context_ref }}");
expect(releaseChecksScript).toContain('-f ref="$release_checks_target_ref"');
expect(releaseChecksScript).toContain('-f ref="$TARGET_SHA"');
expect(releaseChecksScript).toContain('-f target_context_ref="$TARGET_CONTEXT_REF"');
expect(releaseChecksScript).toContain("args+=(-f allow_frozen_target_scenario_omissions=true)");
expect(releaseWorkflowSource).toContain('--arg targetContextRef "$TARGET_CONTEXT_REF"');
expect(releaseWorkflowSource).toContain("targetContextRef: $targetContextRef");