diff --git a/scripts/release-telegram-provenance.sh b/scripts/release-telegram-provenance.sh index d6e4419292ae..c45fef584449 100644 --- a/scripts/release-telegram-provenance.sh +++ b/scripts/release-telegram-provenance.sh @@ -48,13 +48,17 @@ normalized_context_ref="${normalized_context_ref#refs/heads/}" normalized_context_ref="${normalized_context_ref#refs/tags/}" context_release_branch="" context_release_tag="" +frozen_release_branch_pattern="" if [[ "$normalized_context_ref" =~ ^release/([0-9]{4}\.[0-9]+\.[0-9]+)$ ]]; then release_version="${BASH_REMATCH[1]}" release_version_pattern="${release_version//./\.}" candidate_version="$(jq -er '.version' "${candidate_root}/package.json")" - if [[ "$candidate_version" == "$release_version" || - "$candidate_version" =~ ^${release_version_pattern}-beta\.[0-9]+$ ]]; then + if [[ "$candidate_version" == "$release_version" ]]; then context_release_branch="$normalized_context_ref" + elif [[ "$candidate_version" =~ ^${release_version_pattern}-beta\.[0-9]+$ ]]; then + context_release_branch="$normalized_context_ref" + candidate_version_pattern="${candidate_version//./\.}" + frozen_release_branch_pattern="^release/${candidate_version_pattern}-code-frozen(-r[1-9][0-9]*)?$" else echo "Telegram candidate version ${candidate_version} does not belong to release ${release_version}." >&2 exit 1 @@ -113,11 +117,13 @@ if [[ -n "$context_release_branch" ]]; then branch_sha="$( git -C "$remote_git_dir" ls-remote --exit-code --refs origin \ "refs/heads/${context_release_branch}" | - awk 'NR == 1 { print $1 } END { if (NR != 1) exit 1 }' + awk 'NR == 1 { print $1 } END { if (NR != 1) exit 1 }' || + true )" - [[ "$branch_sha" == "$candidate_sha" ]] - trusted_reason="release-branch-head" - trusted_release_branch="$context_release_branch" + if [[ "$branch_sha" == "$candidate_sha" ]]; then + trusted_reason="release-branch-head" + trusted_release_branch="$context_release_branch" + fi elif [[ -n "$context_release_tag" ]]; then tag_refs="$( git -C "$remote_git_dir" ls-remote --exit-code origin \ @@ -172,6 +178,22 @@ else fi fi fi + +if [[ -z "$trusted_reason" && -n "$frozen_release_branch_pattern" && + "$TARGET_REF" =~ ^[a-f0-9]{40}$ && "$TARGET_REF" == "$candidate_sha" ]]; then + matching_frozen_release_branches="$( + gh_with_retry api --paginate \ + "repos/${GITHUB_REPOSITORY}/commits/${candidate_sha}/branches-where-head" \ + --jq '.[].name' | + awk -v frozen="$frozen_release_branch_pattern" '$0 ~ frozen { print }' + )" + if [[ "$(wc -l <<<"$matching_frozen_release_branches" | tr -d ' ')" == "1" && + -n "$matching_frozen_release_branches" ]]; then + trusted_reason="frozen-release-branch-head" + trusted_release_branch="$matching_frozen_release_branches" + fi +fi + if [[ -z "$trusted_reason" ]]; then echo "Telegram candidate ${candidate_sha} is not trusted release provenance." >&2 exit 1 @@ -195,6 +217,11 @@ if [[ "$trusted_reason" != "main-ancestor" ]]; then exit 1 fi signer="$(jq -r '.data.repository.object.signature.signer.login // ""' <<<"$candidate_metadata_json")" + if [[ "$trusted_reason" == "frozen-release-branch-head" && + ( "$signature_status" != "valid" || "$signer" == "web-flow" ) ]]; then + echo "Frozen release candidate ${candidate_sha} requires a valid maintainer signature." >&2 + exit 1 + fi permission_actor="$signer" if [[ "$signature_status" == "missing" || "$signer" == "web-flow" ]]; then if [[ "$trusted_reason" != "release-branch-head" || -z "$trusted_release_branch" ]]; then diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 0fc41df842d6..c2de8d927f67 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -7352,10 +7352,20 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" ); } expect(telegramProvenanceHelper).toContain( - 'if [[ "$candidate_version" == "$release_version" ||', + 'if [[ "$candidate_version" == "$release_version" ]]; then', ); expect(telegramProvenanceHelper).toContain( - '"$candidate_version" =~ ^${release_version_pattern}-beta\\.[0-9]+$ ]]; then', + 'elif [[ "$candidate_version" =~ ^${release_version_pattern}-beta\\.[0-9]+$ ]]; then', + ); + expect(telegramProvenanceHelper).toContain( + 'frozen_release_branch_pattern="^release/${candidate_version_pattern}-code-frozen(-r[1-9][0-9]*)?$"', + ); + expect(telegramProvenanceHelper).toContain( + '"$TARGET_REF" =~ ^[a-f0-9]{40}$ && "$TARGET_REF" == "$candidate_sha"', + ); + expect(telegramProvenanceHelper).toContain('trusted_reason="frozen-release-branch-head"'); + expect(telegramProvenanceHelper).toContain( + '"$signature_status" != "valid" || "$signer" == "web-flow"', ); expect(telegramProvenanceHelper).toContain('context_release_branch="$normalized_context_ref"'); expect(telegramProvenanceHelper).toContain('context_release_tag="$normalized_context_ref"'); @@ -7378,8 +7388,6 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" expect(telegramProvenanceHelper).toContain( 'if [[ "$permission" != "admin" && "$role_name" != "maintain" ]]; then', ); - expect(telegramProvenanceHelper).not.toContain("code-frozen"); - expect(telegramProvenanceHelper).not.toContain("frozen-release-branch-head"); expect(telegramProvenanceHelper).not.toContain(".baseRefName =="); }); diff --git a/test/scripts/openclaw-release-telegram-qa-workflow.test.ts b/test/scripts/openclaw-release-telegram-qa-workflow.test.ts index 740483f05b57..044d217a5879 100644 --- a/test/scripts/openclaw-release-telegram-qa-workflow.test.ts +++ b/test/scripts/openclaw-release-telegram-qa-workflow.test.ts @@ -222,6 +222,7 @@ function runAdvisoryStatus(overrides: Record = {}) { function runCandidateProvenance( provenanceBlock: ProvenanceBlock, params: { + branchHeads?: string[]; candidateVersion?: string; mergedPullRequests?: Array<{ baseRefName?: string; @@ -234,6 +235,7 @@ function runCandidateProvenance( remoteSha?: string; signature?: "invalid" | "maintainer" | "missing" | "web-flow"; targetContextRef?: string; + targetRef?: string; } = {}, ) { const candidateSha = "a".repeat(40); @@ -301,7 +303,7 @@ function runCandidateProvenance( `#!/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' "release/2026.7.1"; exit 0; fi +if [[ "$*" == *"/branches-where-head"* ]]; then printf '%s\\n' "$FAKE_BRANCH_HEADS"; exit 0; fi if [[ "$*" == *"/compare/"* ]]; then printf '%s\\n' "behind"; exit 0; fi if [[ "$*" == *"/collaborators/"*"/permission"* ]]; then printf '%s\\n' "$FAKE_PERMISSION"; exit 0; fi exit 64 @@ -327,6 +329,7 @@ exit 64 encoding: "utf8", env: { ...process.env, + FAKE_BRANCH_HEADS: (params.branchHeads ?? ["release/2026.7.1"]).join("\n"), FAKE_METADATA: JSON.stringify(metadata), FAKE_PERMISSION: JSON.stringify({ permission: params.permission === "admin" ? "admin" : "write", @@ -342,7 +345,8 @@ exit 64 GITHUB_REPOSITORY: "openclaw/openclaw", PATH: `${fakeBin}:${process.env.PATH}`, TARGET_CONTEXT_REF: targetContextRef, - TARGET_REF: targetContextRef ? candidateSha : "refs/heads/release/2026.7.1", + TARGET_REF: + params.targetRef ?? (targetContextRef ? candidateSha : "refs/heads/release/2026.7.1"), TARGET_SHA: candidateSha, }, }); @@ -469,6 +473,99 @@ describe("release Telegram QA workflow", () => { ]); }); + it("accepts only strict signed frozen beta branch heads in both provenance blocks", () => { + for (const provenanceBlock of PROVENANCE_BLOCKS) { + const frozen = runCandidateProvenance(provenanceBlock, { + branchHeads: ["release/2026.7.1-beta.3-code-frozen-r13"], + candidateVersion: "2026.7.1-beta.3", + remoteSha: "b".repeat(40), + targetContextRef: "release/2026.7.1", + }); + expect(frozen.status, `${provenanceBlock.stepName}: ${frozen.stderr}`).toBe(0); + expect(frozen.stdout).toContain( + "Telegram candidate trust reason: frozen-release-branch-head", + ); + + const rejectedCases = [ + { + label: "stale frozen branch", + params: { + branchHeads: [] as string[], + }, + }, + { + label: "duplicate frozen branches", + params: { + branchHeads: [ + "release/2026.7.1-beta.3-code-frozen", + "release/2026.7.1-beta.3-code-frozen-r13", + ], + }, + }, + { + label: "wrong-version frozen branch", + params: { + branchHeads: ["release/2026.7.1-beta.2-code-frozen-r13"], + }, + }, + { + label: "non-exact target ref", + params: { + branchHeads: ["release/2026.7.1-beta.3-code-frozen-r13"], + targetRef: "refs/heads/release/2026.7.1-beta.3-code-frozen-r13", + }, + }, + { + label: "missing signature", + params: { + branchHeads: ["release/2026.7.1-beta.3-code-frozen-r13"], + signature: "missing" as const, + }, + }, + { + label: "invalid signature", + params: { + branchHeads: ["release/2026.7.1-beta.3-code-frozen-r13"], + signature: "invalid" as const, + }, + }, + { + label: "web-flow signature", + params: { + branchHeads: ["release/2026.7.1-beta.3-code-frozen-r13"], + signature: "web-flow" as const, + }, + }, + { + label: "low-permission signer", + params: { + branchHeads: ["release/2026.7.1-beta.3-code-frozen-r13"], + permission: "write" as const, + }, + }, + { + label: "same-repository PR head", + params: { + branchHeads: ["release/2026.7.1-beta.3-code-frozen-r13"], + openPr: true, + }, + }, + ]; + for (const testCase of rejectedCases) { + const rejected = runCandidateProvenance(provenanceBlock, { + candidateVersion: "2026.7.1-beta.3", + remoteSha: "b".repeat(40), + targetContextRef: "release/2026.7.1", + ...testCase.params, + }); + expect( + rejected.status, + `${provenanceBlock.stepName}: ${testCase.label}: ${rejected.stderr}`, + ).not.toBe(0); + } + } + }); + it("attributes web-flow release heads through a unique integration-base merge", () => { const results = PROVENANCE_BLOCKS.flatMap((provenanceBlock) => ["2026.7.1", "2026.7.1-beta.3"].map((candidateVersion) => ({