From 1ef8797d28746e102cae2ffe7ddb768d8cbfd38d Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 21 Aug 2026 17:43:01 +0100 Subject: [PATCH] Repair release activation recovery contracts --- .../workflows/recover-release-activation.yml | 19 +++++-------------- .github/workflows/release-convergence.yml | 1 + .../subsystems/deployment-installability.md | 11 ++++++++++- .../release_promotion_policy_test.py | 8 ++++++++ 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/.github/workflows/recover-release-activation.yml b/.github/workflows/recover-release-activation.yml index 2242aeb5c..c194ba7b9 100644 --- a/.github/workflows/recover-release-activation.yml +++ b/.github/workflows/recover-release-activation.yml @@ -74,23 +74,14 @@ jobs: gh api --paginate --slurp \ "repos/${GITHUB_REPOSITORY}/actions/runs/${SOURCE_RELEASE_RUN_ID}/jobs?per_page=100" \ | jq '[.[].jobs[]]' > "${source_jobs}" + # release_readiness is the canonical DAG join for every immutable + # gate. Do not duplicate reusable-workflow display names here: those + # names are presentation details and can change while the join stays + # authoritative. The surrounding all-jobs verdict below still + # rejects every failure outside the activation boundary. for required_job in \ prepare \ - "Windows PowerShell 5.1 Install Command Smoke" \ - docker_build \ - helm_smoke \ - frontend_checks \ - "Build Immutable Release Candidate / Verify Native Signing Configuration" \ - "Build Immutable Release Candidate / Sign and Notarize macOS Agent" \ - "Build Immutable Release Candidate / Build and Validate Release Candidate" \ - backend_tests \ - release_smoke \ create_release \ - stage_private_pro_runtime \ - "validate_release_assets / validate" \ - "publish_docker / publish" \ - "publish_helm_chart / Package and Push Helm Chart" \ - "install_sh_smoke / smoke" \ release_readiness \ dispatch_release_convergence; do jq -e --arg name "${required_job}" \ diff --git a/.github/workflows/release-convergence.yml b/.github/workflows/release-convergence.yml index 3a99a8e8b..09f402953 100644 --- a/.github/workflows/release-convergence.yml +++ b/.github/workflows/release-convergence.yml @@ -282,6 +282,7 @@ jobs: # already won admission for the rollback-prone customer surfaces. if: ${{ needs.acquire_customer_promotion_lease.result == 'success' }} permissions: + actions: read contents: write uses: ./.github/workflows/helm-pages.yml secrets: inherit diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 387051be9..1d71cc7c2 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -1183,7 +1183,10 @@ upgrade, update, release, or artifact-selection behavior. A failed activation after immutable readiness has passed may use the activation-only recovery workflow. Recovery must accept only a completed failed `create-release.yml` run whose failures are confined to activation, - require every immutable gate to be successful, revalidate GitHub's stored + require the successful `release_readiness` DAG join as the canonical proof + that every immutable gate succeeded, and reject every failure outside the + activation boundary. Recovery must not duplicate reusable-workflow display + names as a parallel gate catalog. It must revalidate GitHub's stored asset digests against that source run's unexpired candidate manifest, and require the same draft release ID, tag, target commit, and absent activation marker. It then dispatches a fresh durable convergence owner and repeats the @@ -1192,6 +1195,12 @@ upgrade, update, release, or artifact-selection behavior. name and display title are coherently indexed, both normal and recovered activation use a bounded metadata-propagation wait before rejecting the convergence owner; a terminal owner still fails immediately. + Every convergence job that calls a reusable workflow must explicitly grant + all permissions requested by that callee. In particular, Helm Pages + convergence requires both `actions: read` to retrieve the exact source-run + chart and `contents: write` to update the versioned Pages index; omitting a + required caller permission is a workflow startup failure, not retriable + customer-surface debt. One immutable-readiness join must cover the staged release packet, staged install smoke, exact public Docker images, exact Helm OCI chart, and (for v6) the exact Pro image and signed packet. After that join, activation must diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index c600ed369..e7d1fdf51 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -439,6 +439,9 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn('.path == ".github/workflows/create-release.yml"', job) self.assertIn("release_readiness", job) self.assertIn("dispatch_release_convergence", job) + self.assertIn("release_readiness is the canonical DAG join", job) + self.assertNotIn("docker_build", job) + self.assertNotIn("helm_smoke", job) self.assertIn("failure outside the recoverable activation boundary", job) self.assertIn("release-candidate-manifest-${source_sha}-${version}", job) self.assertIn("scripts/release_candidate_manifest.py verify-release", job) @@ -452,6 +455,11 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertNotIn("build-release-candidate.yml", recovery) self.assertNotIn("scripts/build-release.sh", recovery) + convergence = read(".github/workflows/release-convergence.yml") + helm_pages_caller = workflow_job_block(convergence, "publish_helm_pages") + self.assertIn("actions: read", helm_pages_caller) + self.assertIn("contents: write", helm_pages_caller) + marker_upload = job.index('gh release upload "${TAG}"') committed = job.index("committed=true", marker_upload) readback = job.index("curl -fsSL --retry 12", committed)