From 32d63253518381e1650fbd39171b252c3bbd45f7 Mon Sep 17 00:00:00 2001 From: Brian Jin <35789537+kikashy@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:30:54 -0400 Subject: [PATCH] ci(release): gate publish, not the draft Move the `production` environment approval from the (reversible) draft job to a new `publish` job so the gate protects the irreversible action. Flow becomes: package -> smoke -> attest + create draft (automatic) -> production approval -> publish (`gh release edit --draft=false`). The maintainer reviews the fully-built draft, then approving publishes it. A prerelease tag is never marked latest. Runbook updated to match. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com> --- .github/workflows/release.yml | 25 ++++++++++++++++++++++++- docs/releasing.md | 18 +++++++++++------- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 638061a..1d3384a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -193,7 +193,6 @@ jobs: - smoke-unix - smoke-windows runs-on: ubuntu-latest - environment: production timeout-minutes: 10 permissions: contents: write @@ -241,3 +240,27 @@ jobs: fi gh release create "${release_args[@]}" + + publish: + name: Publish release + if: github.event_name == 'push' + needs: attest-and-draft + runs-on: ubuntu-latest + environment: production + timeout-minutes: 10 + permissions: + contents: write + steps: + - name: Publish the reviewed draft release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + # No checkout in this job; tell gh which repository to target. + GH_REPO: ${{ github.repository }} + run: | + publish_args=("${RELEASE_TAG}" --draft=false) + # A prerelease (tag contains a hyphen) must not be marked latest. + if [[ "${RELEASE_TAG}" != *-* ]]; then + publish_args+=(--latest) + fi + gh release edit "${publish_args[@]}" diff --git a/docs/releasing.md b/docs/releasing.md index 860896a..c8b4e39 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -69,18 +69,22 @@ binary. A release build must report the tag version without the leading `v`. without publishing them. Included file timestamps are normalized to the tagged commit for reproducible archives. The workflow uploads the six archives and `checksums.txt` as one short-lived workflow artifact. Native Linux, macOS, and Windows jobs verify and run archives - from that exact artifact. Only after all native smoke tests pass does the final job attest the - archives and create the draft GitHub release. No maintainer PAT or repository secret is used. - To diagnose or revalidate an existing tag without creating a release, manually dispatch the same - workflow; manual runs stop after the native smoke tests: + from that exact artifact. Only after all native smoke tests pass does the attest-and-draft job + attest the archives and create the draft GitHub release. The workflow then pauses at the + `production` environment gate, awaiting maintainer approval before the publish job runs. No + maintainer PAT or repository secret is used. To diagnose or revalidate an existing tag without + creating a release, manually dispatch the same workflow; manual runs stop after the native smoke + tests: ```bash gh workflow run release.yml --field tag= ``` -5. Review the successful workflow, generated notes, checksums, attestations, and draft assets. -6. Publish the draft. With Release Immutability enabled, publishing - locks the tag and assets. +5. Review the drafted release on the Releases page — generated notes, checksums, attestations, and + the six archives. +6. Approve the pending `production` deployment on the release run. The publish job then flips the + draft to public (a prerelease tag is never marked latest). With Release Immutability enabled, + publishing locks the tag and assets. Do not move or reuse a released tag. Fixes require a new version.