Repair release activation recovery contracts

This commit is contained in:
rcourtman
2026-08-21 17:43:01 +01:00
parent 1327dddad5
commit 1ef8797d28
4 changed files with 24 additions and 15 deletions
@@ -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}" \
@@ -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
@@ -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
@@ -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)