From 13bf60f3d97a16a5f9fd76e578d36b5564fa1660 Mon Sep 17 00:00:00 2001 From: jiachengzhen Date: Mon, 17 Aug 2026 16:31:15 +0800 Subject: [PATCH] fix(release): validate compare target explicitly --- .github/workflows/github-draft-release-v2.yml | 13 +++++++-- tests/release-workflow.test.ts | 29 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-draft-release-v2.yml b/.github/workflows/github-draft-release-v2.yml index 2e4fed4b4..20869a889 100644 --- a/.github/workflows/github-draft-release-v2.yml +++ b/.github/workflows/github-draft-release-v2.yml @@ -307,6 +307,14 @@ jobs: echo "::error title=Release compare metadata failed::Could not validate ${base_sha}...${TARGET_SHA} against GitHub. Manual recovery: confirm both commits are reachable in the repository, then retry." >&2 exit 1 fi + # GitHub Compare metadata has no top-level head_commit field. Validate + # the remote target through the exact commit endpoint instead. + if ! gh api --method GET \ + "repos/${GITHUB_REPOSITORY}/commits/${TARGET_SHA}" \ + > release-assets/TARGET_COMMIT_METADATA.json; then + echo "::error title=Release target metadata failed::Could not validate target commit ${TARGET_SHA} against GitHub. Manual recovery: confirm the merged main commit is reachable in the repository, then retry." >&2 + exit 1 + fi node scripts/build-release-compare.mjs \ --base "$base_sha" \ @@ -316,7 +324,7 @@ jobs: api_base="$(jq -r '.base_commit.sha // empty' release-assets/COMPARE_METADATA.json)" api_merge_base="$(jq -r '.merge_base_commit.sha // empty' release-assets/COMPARE_METADATA.json)" - api_head="$(jq -r '.head_commit.sha // empty' release-assets/COMPARE_METADATA.json)" + api_target="$(jq -r '.sha // empty' release-assets/TARGET_COMMIT_METADATA.json)" api_status="$(jq -r '.status // empty' release-assets/COMPARE_METADATA.json)" api_ahead="$(jq -r '.ahead_by // -1' release-assets/COMPARE_METADATA.json)" api_behind="$(jq -r '.behind_by // -1' release-assets/COMPARE_METADATA.json)" @@ -325,7 +333,7 @@ jobs: local_files="$(jq -r '.files | length' release-assets/COMPARE.json)" local_head="$(jq -r '.head_commit.sha // empty' release-assets/COMPARE.json)" - if [[ "$api_base" != "$base_sha" || "$api_merge_base" != "$base_sha" || "$api_head" != "$TARGET_SHA" || "$local_head" != "$TARGET_SHA" ]]; then + if [[ "$api_base" != "$base_sha" || "$api_merge_base" != "$base_sha" || "$api_target" != "$TARGET_SHA" || "$local_head" != "$TARGET_SHA" ]]; then echo "::error title=Release comparison identity mismatch::GitHub and the trusted checkout do not agree on the base, merge base, or target. Manual recovery: refresh the target checkout and retry without changing the tag." >&2 exit 1 fi @@ -853,6 +861,7 @@ jobs: release-assets/RELEASE_NOTES_SOURCE.json release-assets/QUALITY_REPORT.json release-assets/COMPARE_METADATA.json + release-assets/TARGET_COMMIT_METADATA.json release-assets/COMPARE.json release-assets/PULL_REQUESTS.json release-assets/RELEASE_EVIDENCE.json diff --git a/tests/release-workflow.test.ts b/tests/release-workflow.test.ts index 18202cd93..48b4bd24a 100644 --- a/tests/release-workflow.test.ts +++ b/tests/release-workflow.test.ts @@ -359,6 +359,34 @@ describe("GitHub Draft Release v2 workflow", () => { expect(download).toContain("SHA256SUMS.txt"); }); + it("does not expect a nonexistent head_commit in GitHub Compare metadata", () => { + const realCompareMetadataShape = { + base_commit: { sha: "base-sha" }, + merge_base_commit: { sha: "base-sha" }, + status: "ahead", + ahead_by: 2, + behind_by: 0, + total_commits: 2, + commits: [{ sha: "first-commit" }], + files: [], + }; + expect(realCompareMetadataShape).not.toHaveProperty("head_commit"); + + const snapshot = draftScript("Build complete release change snapshot"); + expect(snapshot).not.toContain("api_head="); + expect(snapshot).not.toContain( + "'.head_commit.sha // empty' release-assets/COMPARE_METADATA.json", + ); + expect(snapshot).toContain('"repos/${GITHUB_REPOSITORY}/commits/${TARGET_SHA}"'); + expect(snapshot).toContain("TARGET_COMMIT_METADATA.json"); + expect(snapshot).toContain("Release target metadata failed"); + expect(snapshot).toContain("api_target"); + expect(snapshot).toContain('"$api_target" != "$TARGET_SHA"'); + expect(snapshot).toContain( + "'.head_commit.sha // empty' release-assets/COMPARE.json", + ); + }); + it("records independently auditable commits, PRs, files, versions, and assets", () => { for (const stepName of [ "Build complete release change snapshot", @@ -432,6 +460,7 @@ describe("GitHub Draft Release v2 workflow", () => { expect(JSON.stringify(uploadAudit)).toContain("RELEASE_NOTES_SOURCE.json"); expect(JSON.stringify(uploadAudit)).toContain("QUALITY_REPORT.json"); expect(JSON.stringify(uploadAudit)).toContain("COMPARE_METADATA.json"); + expect(JSON.stringify(uploadAudit)).toContain("TARGET_COMMIT_METADATA.json"); expect(JSON.stringify(uploadAudit)).toContain("COMPARE.json"); expect(JSON.stringify(uploadAudit)).toContain("PULL_REQUESTS.json"); expect(draftScript("Create draft release and upload every asset")).toContain(