From 420e8e7f3216c4fa3b8dd867fc452ceb65807c07 Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sun, 19 Jul 2026 15:25:52 +0000 Subject: [PATCH] fix: the release label's two meanings part ways in a decide step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LABELS.md gives 'release' to release-flow WORK as well as to the ceremony PR — the PR that added the merge door included. The old assert pair turned every such merge into a red run on main. The fused decide step reads VERSION against the merge commit's first parent and answers all states: -dev unchanged = work, green NOTICE no-op; bare unchanged but already released = work in the post-release window, same no-op; -dev-but-changed and bare-unchanged-never-released = half-ceremonies, refused loudly; bare-and-changed = the ceremony. Later steps gate on its output. Five new pins in test/release.sh cover each verdict and the gating. Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 52 ++++++++++++++++++++++++++--------- test/release.sh | 16 +++++++++++ 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9047242..7621c95 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,32 +56,57 @@ jobs: # base.sha can be stale; the merge commit's first parent cannot.) ref: ${{ github.event.pull_request.merge_commit_sha }} fetch-depth: 2 - - name: VERSION at the merge commit must be a release, not -dev - run: | - ver="$(cat VERSION)" - case "$ver" in - *-dev) - echo "VERSION is '$ver' — still -dev, so this merge is not a release ceremony, whatever its label says — creating nothing." >&2 - exit 1 ;; - esac - - name: VERSION must have CHANGED in this PR — the -dev interlock - # Only the release PR moves VERSION off -dev. A mislabeled ordinary - # PR merged while main already carries a release version dies here, - # loudly, instead of re-releasing whatever VERSION says. + # The decide step — the version asserts fused, because the `release` + # label carries TWO legitimate meanings (LABELS.md: "release flow and + # version/packaging work"): the ceremony PR that ships a version, and + # ordinary work ON the release machinery — the PR that added this very + # job included. The version tells them apart, in four states: + # -dev, unchanged → work under the label: green NOTICE + # no-op, not a red run per infra PR + # -dev, changed → a bump that forgot to leave -dev: + # half a ceremony, refuse + # bare, unchanged, released → work merged in the post-release + # window (ceremony landed, the -dev + # bump has not): green NOTICE no-op + # bare, unchanged, UNreleased→ the label says ship but this PR did + # not mint the version: refuse to guess + # bare, changed → the ceremony: proceed + - name: 'decide: ceremony, or release-flow work under the label?' + id: decide + env: + GH_TOKEN: ${{ github.token }} run: | ver="$(cat VERSION)" base="$(git show HEAD^1:VERSION)" + case "$ver" in + *-dev) + if [ "$base" = "$ver" ]; then + echo "NOTICE: VERSION '$ver' is -dev and unchanged by this PR — release-flow work under the release label, not a ceremony. Nothing to publish." + echo "ceremony=no" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "VERSION changed ('$base' -> '$ver') but is still -dev — half a ceremony; a release PR ships a bare X.Y.Z — creating nothing." >&2 + exit 1 ;; + esac if [ "$base" = "$ver" ]; then - echo "VERSION '$ver' did not change in this PR (the base commit already carried it) — this is not the release PR — creating nothing." >&2 + if gh release view "$ver" --json name >/dev/null 2>&1; then + echo "NOTICE: VERSION '$ver' is already released and unchanged by this PR — release-flow work merged in the post-release window (before the -dev bump). Nothing to publish." + echo "ceremony=no" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "VERSION '$ver' is bare, unchanged by this PR, and never released — the label says ship but this PR did not mint the version. Refusing to guess — creating nothing." >&2 exit 1 fi + echo "ceremony=yes" >> "$GITHUB_OUTPUT" - name: release notes — the version's own CHANGELOG.md section + if: steps.decide.outputs.ceremony == 'yes' # release-notes.sh fails loudly on a missing/empty section, which # fails the release here — before anything is created. run: | bash .github/scripts/release-notes.sh "$(cat VERSION)" > "$RUNNER_TEMP/notes.md" cat "$RUNNER_TEMP/notes.md" - name: nothing may exist yet — no tag, no release (idempotency) + if: steps.decide.outputs.ceremony == 'yes' env: GH_TOKEN: ${{ github.token }} run: | @@ -95,6 +120,7 @@ jobs: exit 1 fi - name: tag the merge commit, then publish — one job, on purpose + if: steps.decide.outputs.ceremony == 'yes' # Same job as the asserts: the GITHUB_TOKEN-created tag triggers no # workflows (GitHub's anti-recursion), so the tag door cannot fire # off it — this step is the release's only chance to publish. diff --git a/test/release.sh b/test/release.sh index f5d7229..9db7c1d 100644 --- a/test/release.sh +++ b/test/release.sh @@ -150,6 +150,22 @@ check "release.yml: BOTH doors publish bound to an existing tag (--verify-tag)" grep -cF -- '--verify-tag' "$RY" check "release.yml: tag + publish share one job (the anti-recursion shape)" 0 "" \ grep -qF 'anti-recursion' "$RY" +# The decide step tells the label's two meanings apart (LABELS.md gives +# `release` to release-flow WORK as well as to the ceremony PR — the PR +# that added the merge door included): work under the label no-ops GREEN — +# in the -dev steady state and in the post-release window (bare, unchanged, +# already released) — while every half-ceremony refuses. Pin each verdict +# and the gating output. +check "release.yml: decide — dev-tree work no-ops green (not a red run per infra PR)" 0 "" \ + grep -qF "release-flow work under the release label, not a ceremony" "$RY" +check "release.yml: decide — a half-ceremony (-dev but changed) refuses" 0 "" \ + grep -qF "half a ceremony" "$RY" +check "release.yml: decide — post-release-window work no-ops green" 0 "" \ + grep -qF "release-flow work merged in the post-release window" "$RY" +check "release.yml: decide — bare, unchanged, never released refuses to guess" 0 "" \ + grep -qF "Refusing to guess" "$RY" +check "release.yml: decide gates every later merge-door step on ceremony=yes" 0 "3" \ + grep -cF "if: steps.decide.outputs.ceremony == 'yes'" "$RY" # --------------------------------------------------------------------------- # latest_release_tag — extracted from install.sh (the source-the-pure-function