From 6479e82c986e9224695875d453cbc0a8b68859cb Mon Sep 17 00:00:00 2001 From: DevBot Date: Mon, 27 Jul 2026 09:43:16 +0800 Subject: [PATCH] fix(tools): teach evidence closure the two-phase patch-release tag flow The tag snapshot carries the patch-release record while the final record on main is publish-existing; different run ids across those two kinds are valid (0.41.2 is the first release that actually took this path). The validator now accepts the two-phase shape and still rejects unexplained id mismatches. --- .../lib/release-evidence-consistency.test.ts | 36 +++++++++++++++++++ tools/lib/release-evidence-consistency.ts | 10 +++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/tools/lib/release-evidence-consistency.test.ts b/tools/lib/release-evidence-consistency.test.ts index c9f08933d..8787ab38a 100644 --- a/tools/lib/release-evidence-consistency.test.ts +++ b/tools/lib/release-evidence-consistency.test.ts @@ -102,3 +102,39 @@ Deno.test('release evidence closure requires durable final commit and workflow r 'release note does not reference the GitHub release', ]); }); + +Deno.test('release evidence closure accepts the two-phase patch-release tag flow', () => { + // 0.41.2: the local patch-release tagged the version (tag snapshot carries + // the patch-release record), the CI publish-existing closed the release + // with its own record. Different ids are valid across the two kinds. + const failures = validateReleaseEvidenceClosure({ + version: '0.41.2', + record: { + tagCommit: 'tag-sha', + finalEvidenceCommit: 'final-sha', + successfulReleaseRun: 'https://github.com/example/actions/runs/2', + releaseUrl: 'https://github.com/example/releases/tag/v0.41.2', + }, + tagIsAncestorOfFinal: true, + finalIsAncestorOfHead: true, + tagEvidence: { + id: 'patch-run-id', + kind: 'patch-release', + targetVersion: '0.41.2', + status: 'running', + steps: [{ name: 'tag release', status: 'passed' }], + }, + finalEvidence: { + id: 'publish-run-id', + kind: 'publish-existing', + targetVersion: '0.41.2', + status: 'completed', + completedAt: '2026-07-27T01:45:00.000Z', + steps: [{ name: 'create GitHub release', status: 'passed' }], + }, + releaseNote: + 'Final evidence: final-sha\nRun: https://github.com/example/actions/runs/2\nRelease: https://github.com/example/releases/tag/v0.41.2', + }); + + assertEquals(failures, []); +}); diff --git a/tools/lib/release-evidence-consistency.ts b/tools/lib/release-evidence-consistency.ts index e7e13946c..a2c0004a0 100644 --- a/tools/lib/release-evidence-consistency.ts +++ b/tools/lib/release-evidence-consistency.ts @@ -7,6 +7,7 @@ export interface ReleaseClosureRecord { export interface ReleaseEvidenceSnapshot { id: string; + kind?: string; targetVersion: string; status: string; completedAt?: string; @@ -25,7 +26,14 @@ export interface ReleaseEvidenceClosureInput { export function validateReleaseEvidenceClosure(input: ReleaseEvidenceClosureInput): string[] { const failures: string[] = []; - if (input.tagEvidence.id !== input.finalEvidence.id) { + // Same-run flow: the tag snapshot and the final record share one run id. + // Two-phase flow: a local patch-release tagged the version and the CI + // publish-existing closed it, so the tag snapshot carries the + // patch-release record while the final record is publish-existing (0.41.2). + const sameRun = input.tagEvidence.id === input.finalEvidence.id; + const twoPhase = input.tagEvidence.kind === 'patch-release' && + input.finalEvidence.kind === 'publish-existing'; + if (!sameRun && !twoPhase) { failures.push('tag and final evidence ids differ'); } if (