forked from heavy-duty/rig
fix: the release label's two meanings part ways in a decide step
LABELS.md gives 'release' to release-flow WORK as well as to the ceremony PR — including every PR that improves this very workflow. The old assert pair turned each of those merges into a red run on main. The fused decide step reads the version against the PR base and answers all four 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. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4ce1ab50aa
commit
d08ec8c6f6
2 changed files with 59 additions and 20 deletions
59
.github/workflows/release.yml
vendored
59
.github/workflows/release.yml
vendored
|
|
@ -88,32 +88,57 @@ jobs:
|
|||
# base-side VERSION is readable for the interlock below.
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
fetch-depth: 0
|
||||
# Assert 1 — the merged tree says it is a release. A `-dev` VERSION
|
||||
# here means the label lied (or the ceremony PR forgot the bump).
|
||||
- name: assert the merged tree is a release (non-dev VERSION)
|
||||
run: |
|
||||
ver="$(cat VERSION)"
|
||||
case "$ver" in
|
||||
*-dev)
|
||||
echo "VERSION '$ver' is still -dev — a release PR ships a bare X.Y.Z; refusing to release a dev tree" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
# Assert 2 — THIS PR is the one that changed VERSION (base vs merge).
|
||||
# The `-dev` transition as a safety interlock: an ordinary PR someone
|
||||
# mislabels `release` fails here loudly instead of shipping main
|
||||
# under a version some earlier PR minted.
|
||||
- name: assert VERSION changed in this PR (the mislabel interlock)
|
||||
# The decide step — asserts 1+2 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 — this very PR included.
|
||||
# The version tells them apart. A `-dev` VERSION left UNTOUCHED by the
|
||||
# PR is release-flow work: a green no-op, not a red run on main every
|
||||
# time the flow itself is improved. Everything in between is a
|
||||
# half-ceremony and dies loudly:
|
||||
# -dev, unchanged → work under the label: NOTICE + green no-op
|
||||
# -dev, changed → a bump that forgot to leave -dev: refuse
|
||||
# bare, unchanged,
|
||||
# already released → work merged in the post-release window
|
||||
# (ceremony landed, the -dev bump has not):
|
||||
# NOTICE + green no-op
|
||||
# bare, unchanged,
|
||||
# never released → the label says ship, the tree names an
|
||||
# unshipped version this PR did not mint:
|
||||
# genuinely ambiguous, refuse
|
||||
# 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_ver="$(git show "$BASE_SHA:VERSION")"
|
||||
case "$ver" in
|
||||
*-dev)
|
||||
if [ "$base_ver" = "$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' -> '$ver') but is still -dev — half a ceremony; a release PR ships a bare X.Y.Z. Refusing." >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
if [ "$base_ver" = "$ver" ]; then
|
||||
echo "VERSION did not change in this PR ('$ver' before and after) — a 'release'-labeled PR must be the ceremony PR that bumps it; refusing to release" >&2
|
||||
if gh release view "$ver" -R "$GITHUB_REPOSITORY" >/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." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "ceremony=yes" >> "$GITHUB_OUTPUT"
|
||||
# Assert 3 — the changelog names exactly this version, and the one
|
||||
# extractor (shared with the tag job and test/release.sh) gets a
|
||||
# non-empty body out of it. The notes are kept for the publish.
|
||||
- name: assert the changelog section for this version extracts
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
run: |
|
||||
. .github/scripts/release-lib.sh
|
||||
ver="$(cat VERSION)"
|
||||
|
|
@ -126,6 +151,7 @@ jobs:
|
|||
# Assert 4 — nothing exists yet, tag or release: a re-run of this job
|
||||
# (or a manual tag that beat it) must refuse, not clobber.
|
||||
- name: assert no tag and no release exist yet (idempotent re-runs)
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
|
|
@ -142,6 +168,7 @@ jobs:
|
|||
# assert 3 extracted. (GITHUB_TOKEN-created tag: no recursive
|
||||
# workflow runs — see the job comment.)
|
||||
- name: tag the merge commit and publish the release
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -141,10 +141,22 @@ check "release.yml: only a MERGED PR releases (closed-unmerged never fires)" 0 "
|
|||
grep -qF "github.event.pull_request.merged == true" "$RY"
|
||||
check "release.yml: only the 'release' label carries the intent" 0 "" \
|
||||
grep -qF "contains(github.event.pull_request.labels.*.name, 'release')" "$RY"
|
||||
check "release.yml: assert 1 — a still-dev VERSION refuses" 0 "" \
|
||||
grep -qF "refusing to release a dev tree" "$RY"
|
||||
check "release.yml: assert 2 — an unchanged VERSION refuses (the mislabel interlock)" 0 "" \
|
||||
grep -qF "VERSION did not change in this PR" "$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): work under
|
||||
# the label is a green NOTICE no-op — in the -dev steady state and in the
|
||||
# post-release window (bare, unchanged, already released) — while every
|
||||
# half-ceremony refuses. Pin each verdict's message 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"
|
||||
# shellcheck disable=SC2016 # the $-refs are the inner bash -c's, deliberately
|
||||
check "release.yml: decide gates every later step on ceremony=yes" 0 "" \
|
||||
bash -c '[ "$(grep -cF "if: steps.decide.outputs.ceremony == '\''yes'\''" "$1")" -ge 3 ]' _ "$RY"
|
||||
check "release.yml: assert 3 — an empty section refuses to publish" 0 "" \
|
||||
grep -qF "refusing to publish an empty release" "$RY"
|
||||
check "release.yml: assert 4 — an existing tag or release refuses (idempotent)" 0 "" \
|
||||
|
|
|
|||
Loading…
Reference in a new issue