From 097422837b3de8c1ba3db0003efb0abc7c0618ca Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 21:43:11 +0100 Subject: [PATCH] fix(release): preserve quarantined version identity Reject historical draft reuse before PATCH when its retained target is not the exact checkout SHA, including missing targets and moving refs. Preserve same-source recovery and activation guards. Depends on PR2056 qualification-first workflow and immutable tag checks. Executable absent-tag fixtures fail before repair and pass on PR head 9e5e18f0 plus this patch; 53 policy, 6 immutability and 42 trust tests pass. Update the deployment contract in the same commit. Change-source: pulse-maintainer --- .github/workflows/create-release.yml | 11 ++++ .../subsystems/deployment-installability.md | 17 ++++++ .../release_promotion_policy_test.py | 57 +++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 3be410c4d..723981b77 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -993,12 +993,14 @@ jobs: RELEASE_ID=$(echo "$EXISTING_RELEASE" | jq -r '.id // empty') RELEASE_URL=$(echo "$EXISTING_RELEASE" | jq -r '.html_url // empty') RELEASE_IS_DRAFT=$(echo "$EXISTING_RELEASE" | jq -r '.draft // false') + RELEASE_TARGET_COMMITISH=$(echo "$EXISTING_RELEASE" | jq -r '.target_commitish // empty') RELEASE_PUBLISHED_AT=$(echo "$EXISTING_RELEASE" | jq -r '.published_at // empty') RELEASE_ACTIVATION_COMMITTED=$(echo "$EXISTING_RELEASE" | jq -r 'any(.assets[]?; .name == "release-activation.json")') python3 scripts/write_github_output.py release_id "${RELEASE_ID}" python3 scripts/write_github_output.py release_url "${RELEASE_URL}" python3 scripts/write_github_output.py release_is_draft "${RELEASE_IS_DRAFT}" + python3 scripts/write_github_output.py release_target_commitish "${RELEASE_TARGET_COMMITISH}" python3 scripts/write_github_output.py release_published_at "${RELEASE_PUBLISHED_AT}" python3 scripts/write_github_output.py release_activation_committed "${RELEASE_ACTIVATION_COMMITTED}" @@ -1059,6 +1061,7 @@ jobs: WORKFLOW_OUTPUT_6: ${{ steps.existing_release.outputs.release_is_draft }} WORKFLOW_OUTPUT_7: ${{ steps.existing_release.outputs.release_published_at }} WORKFLOW_OUTPUT_8: ${{ steps.existing_release.outputs.release_activation_committed }} + WORKFLOW_OUTPUT_10: ${{ steps.existing_release.outputs.release_target_commitish }} WORKFLOW_OUTPUT_9: ${{ needs.prepare.outputs.version }} run: | set -euo pipefail @@ -1072,6 +1075,7 @@ jobs: IS_DRAFT="${WORKFLOW_OUTPUT_6}" PUBLISHED_AT="${WORKFLOW_OUTPUT_7}" ACTIVATION_COMMITTED="${WORKFLOW_OUTPUT_8}" + PREVIOUS_TARGET_COMMITISH="${WORKFLOW_OUTPUT_10}" RELEASE_PAYLOAD=$(mktemp) RELEASE_JSON_FILE=$(mktemp) ACTUAL_BODY_FILE=$(mktemp) @@ -1095,6 +1099,13 @@ jobs: if [ -n "$RELEASE_ID" ]; then if [ "$IS_DRAFT" = "true" ] && [ "$ACTIVATION_COMMITTED" != "true" ]; then if [ -n "$PUBLISHED_AT" ]; then + # Deleting a public tag does not make its version reusable. + # Only an exact retained commit proves same-identity recovery; + # a missing target or moving branch name is not such proof. + if [ "$PREVIOUS_TARGET_COMMITISH" != "$HEAD_SHA" ]; then + echo "::error::Previously published release ${TAG} identifies ${PREVIOUS_TARGET_COMMITISH:-unknown}, not HEAD (${HEAD_SHA}); use a new version." + exit 1 + fi echo "Resuming quarantined draft release for ${TAG}; GitHub retained historical published_at=${PUBLISHED_AT}." fi echo "Updating existing draft release for ${TAG}" diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 1db4acbd2..53ebb7dde 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -5444,3 +5444,20 @@ must not alter permissions, environments, source identity, rollback, signing backend selection or release qualification. Local contract success does not establish hosted action execution, signature acceptance, image publication or production deployment. + +### Quarantined release identity after tag deletion + +Draft preparation retains the existing release's target_commitish. A historical +published_at is evidence that the version was exposed even if its Git tag is +now absent. Before PATCH, such a draft must retain an exact target equal to the +admitted checkout SHA; changed, absent and branch-name targets fail closed. +Same-SHA quarantined recovery remains available unless the activation marker +has committed the packet. Never-published private drafts remain replaceable. +This guard complements, rather than replaces, PR2056's immutable public-tag +check and qualification-first Git, Docker and Helm writers. + +The executable release policy regression runs the actual absent-tag check and +draft shell with recording fake APIs. It must reject changed/unknown/branch +historical targets before any API mutation, admit same-identity recovery and +private replacement, and preserve activated/published refusals. This is local +workflow proof, not publication or installed acceptance. diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index e2e35011a..e0149dddc 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -303,6 +303,63 @@ STAGED_GOVERNANCE_INPUT_ERRORS = ( class ReleasePromotionPolicyTest(unittest.TestCase): + def test_quarantined_draft_identity_before_patch(self): + workflow = yaml.safe_load(read(".github/workflows/create-release.yml")) + steps = workflow["jobs"]["create_release"]["steps"] + create = next(step for step in steps if step.get("name") == "Create draft release") + locate = next(step for step in steps if step.get("id") == "existing_release") + self.assertIn(".target_commitish // empty", locate["run"]) + self.assertIn('write_github_output.py release_target_commitish "${RELEASE_TARGET_COMMITISH}"', + locate["run"]) + self.assertEqual(create["env"]["WORKFLOW_OUTPUT_10"], + "${{ steps.existing_release.outputs.release_target_commitish }}") + check = next(step for step in steps + if step.get("name") == "Check existing public tag without changing it") + self.assertLess(steps.index(check), steps.index(create)) + script = (check["run"] + "\n" + create["run"]).replace( + "${{ github.repository }}", "fixture/pulse") + # Execute the actual draft shell with an absent-tag fake Git and a + # recording API sentinel. No network or public writer is available. + head = "a" * 40 + cases = [ + ("different", "b" * 40, "2026-09-01T00:00:00Z", "true", "false", False), + ("unknown", "", "2026-09-01T00:00:00Z", "true", "false", False), + ("branch", "main", "2026-09-01T00:00:00Z", "true", "false", False), + ("same", head, "2026-09-01T00:00:00Z", "true", "false", True), + ("private", "b" * 40, "", "true", "false", True), + ("activated", head, "2026-09-01T00:00:00Z", "true", "true", False), + ("published", head, "2026-09-01T00:00:00Z", "false", "false", False), + ] + for label, target, published, draft, activated, allowed in cases: + with self.subTest(label=label), tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "notes").write_text("Fixture notes") + (root / "git").write_text( + "#!/bin/bash\n" + 'if [ "$*" = "rev-parse HEAD" ]; then echo ' + head + '; exit 0; fi\n' + 'if [ "$1" = "ls-remote" ]; then exit 0; fi\nexit 98\n' + ) + (root / "gh").write_text( + "#!/bin/bash\nprintf '%s\\n' \"$*\" >> \"$CALLS\"\nexit 73\n" + ) + for name in ("git", "gh"): + (root / name).chmod(0o755) + env = dict(os.environ, PATH=f"{root}:/usr/bin:/bin", TMPDIR=tmp, + CALLS=str(root / "calls"), TAG="v6.4.4-beta.4") + values = ["v6.4.4-beta.4", str(root / "notes"), "true", "123", + "https://example.invalid/release", draft, published, activated, + "6.4.4-beta.4", target] + env.update({f"WORKFLOW_OUTPUT_{i}": v for i, v in enumerate(values, 1)}) + result = subprocess.run(["bash", "-euo", "pipefail", "-c", script], + cwd=root, env=env, text=True, capture_output=True) + calls = (root / "calls").read_text() if (root / "calls").exists() else "" + if allowed: + self.assertEqual(result.returncode, 73, result.stdout + result.stderr) + self.assertIn("-X PATCH", calls) + else: + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertEqual(calls, "", "refusal must precede API mutation") + def test_reviewed_action_manifests_cover_all_release_consumers(self) -> None: # Snapshot is derived from each immutable upstream action.yml, not from # our consumers: unknown inputs therefore fail rather than being blessed.