fix: the release suite accepts the ceremony's own tree #45

Merged
dan-claude-bot merged 1 commit from fix/release-suite-stamped-tree into main 2026-07-19 14:22:15 +00:00
dan-claude-bot commented 2026-07-19 13:48:45 +00:00 (Migrated from github.com)

Fixes #44.

What

test/release.sh:94-97 asserted the REAL changelog has a literal ## Unreleased heading whose section extracts non-empty and contains #32. On the release: X.Y.Z tree — the one the ceremony's own PR produces by stamping that heading into ## X.Y.Z — date — all three are false by construction, so the first real release: 0.1.0 PR turned CI red and the flow blocked itself. Post-release main (until the next feature PR re-adds the section) fails the same way; the -dev bump doesn't touch the changelog (box precedent 7044f8e). Both drill rehearsals missed it because a fork rehearsal tags a branch: release.yml runs, ci.yml never does.

How

The guard now asserts what its own comment says it is for — the shipped changelog fits the extractor: whatever the TOP ## section is (Unreleased between releases; the stamped X.Y.Z — date on and right after one), the exact changelog_section that release.yml runs must extract it non-empty. The hardcoded issue-number grep is gone — it added no format coverage and would have rotted on every release.

Tests

The changed checks are themselves the test; verified green on both legitimate tree states: main's shape (Unreleased present) and a stamped copy (## 0.1.0 — 2026-07-19). test/release.sh 41/0 on both, test/cli.sh 325/0, CI-exact shellcheck sweep rc=0.

Unblocks the release: 0.1.0 PR, which follows once this lands.

🤖 Generated with Claude Code

Fixes #44. ## What `test/release.sh:94-97` asserted the REAL changelog has a literal `## Unreleased` heading whose section extracts non-empty and contains `#32`. On the `release: X.Y.Z` tree — the one the ceremony's own PR produces by stamping that heading into `## X.Y.Z — date` — all three are false by construction, so the first real `release: 0.1.0` PR turned CI red and the flow blocked itself. Post-release main (until the next feature PR re-adds the section) fails the same way; the `-dev` bump doesn't touch the changelog (box precedent `7044f8e`). Both drill rehearsals missed it because a fork rehearsal tags a branch: `release.yml` runs, `ci.yml` never does. ## How The guard now asserts what its own comment says it is for — the shipped changelog fits the extractor: whatever the TOP `## ` section is (`Unreleased` between releases; the stamped `X.Y.Z — date` on and right after one), the exact `changelog_section` that `release.yml` runs must extract it non-empty. The hardcoded issue-number grep is gone — it added no format coverage and would have rotted on every release. ## Tests The changed checks are themselves the test; verified green on **both** legitimate tree states: main's shape (Unreleased present) and a stamped copy (`## 0.1.0 — 2026-07-19`). `test/release.sh` 41/0 on both, `test/cli.sh` 325/0, CI-exact shellcheck sweep rc=0. Unblocks the `release: 0.1.0` PR, which follows once this lands. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
danmt (Migrated from github.com) reviewed 2026-07-19 13:48:45 +00:00
codex-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-19 14:04:02 +00:00
codex-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: I agree with everything and have no additional feedback.

Verdict: I agree with everything and have no additional feedback.
claude-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-19 14:07:30 +00:00
claude-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: approve.

The diagnosis in #44 checks out against the code. The old guard (test/release.sh:92-97 pre-PR) demanded a literal ## Unreleased heading in the real CHANGELOG.md plus #32 inside it — both false by construction on the release: X.Y.Z tree the ceremony's own PR produces and on post-release main, so the first real release PR was unshippable. The fork drills tagging a branch run release.yml but never ci.yml, which is exactly why this surfaced only on the real PR.

The replacement is correct and stays narrow:

  • test/release.sh:106-107 derives the top section's key with the same field-2 rule the extractor matches on (release-lib.sh:19-24, $2 == ver), so both legitimate states pass and the assertion is against the exact function release.yml:42 runs — no parallel reimplementation.
  • Verified by execution, not just reading: a stamped copy (## 0.1.0 — 2026-07-19 as top) passes the new guard; an empty top section fails; a changelog with no ## heading at all fails (test/release.sh:103-104 catches that case). So the guard still fails closed on real format drift and only admits the ceremony's own tree state.
  • The fixture tests at test/release.sh:69-90 continue to pin changelog_section in both directions (stamped and bare headings extract; unknown version and date-match yield nothing), so dropping the rotting #32 grep loses no coverage.
  • Both shellcheck disable=SC2016 annotations (test/release.sh:102,105) are correctly scoped — the $-refs genuinely belong to the inner bash -c.

Two non-blocking observations:

  1. Because the test derives the section key from the heading itself, it is self-consistent with the extractor: a heading whose second field is not the bare tag name (e.g. ## [0.1.0] — date) would pass this suite yet make changelog_section CHANGELOG.md "$GITHUB_REF_NAME" extract empty at release time. That path still fails closed via the loud refusal pinned at release.yml:44 (itself asserted by test/release.sh:122-123), so nothing ships silently — acceptable.
  2. The relaxation means a feature PR landing right after a release without re-adding ## Unreleased no longer trips CI. Unavoidable: post-release main is a legitimate state and a static check cannot tell the two apart; the comment block at test/release.sh:92-101 documents this honestly.

Ran locally at 589854a: test/release.sh 41 passed / 0 failed, test/cli.sh 325 passed / 0 failed. The CHANGELOG entry matches the change.

automated review by claude-bot-andresmgsl · heavy-duty-review-bot

**Verdict: approve.** The diagnosis in #44 checks out against the code. The old guard (`test/release.sh:92-97` pre-PR) demanded a literal `## Unreleased` heading in the real `CHANGELOG.md` plus `#32` inside it — both false by construction on the `release: X.Y.Z` tree the ceremony's own PR produces and on post-release main, so the first real release PR was unshippable. The fork drills tagging a branch run `release.yml` but never `ci.yml`, which is exactly why this surfaced only on the real PR. The replacement is correct and stays narrow: - `test/release.sh:106-107` derives the top section's key with the same field-2 rule the extractor matches on (`release-lib.sh:19-24`, `$2 == ver`), so both legitimate states pass and the assertion is against the exact function `release.yml:42` runs — no parallel reimplementation. - Verified by execution, not just reading: a stamped copy (`## 0.1.0 — 2026-07-19` as top) passes the new guard; an empty top section fails; a changelog with no `## ` heading at all fails (`test/release.sh:103-104` catches that case). So the guard still fails closed on real format drift and only admits the ceremony's own tree state. - The fixture tests at `test/release.sh:69-90` continue to pin `changelog_section` in both directions (stamped and bare headings extract; unknown version and date-match yield nothing), so dropping the rotting `#32` grep loses no coverage. - Both `shellcheck disable=SC2016` annotations (`test/release.sh:102,105`) are correctly scoped — the `$`-refs genuinely belong to the inner `bash -c`. Two non-blocking observations: 1. Because the test derives the section key from the heading itself, it is self-consistent with the extractor: a heading whose second field is not the bare tag name (e.g. `## [0.1.0] — date`) would pass this suite yet make `changelog_section CHANGELOG.md "$GITHUB_REF_NAME"` extract empty at release time. That path still fails closed via the loud refusal pinned at `release.yml:44` (itself asserted by `test/release.sh:122-123`), so nothing ships silently — acceptable. 2. The relaxation means a feature PR landing right after a release without re-adding `## Unreleased` no longer trips CI. Unavoidable: post-release main is a legitimate state and a static check cannot tell the two apart; the comment block at `test/release.sh:92-101` documents this honestly. Ran locally at 589854a: `test/release.sh` 41 passed / 0 failed, `test/cli.sh` 325 passed / 0 failed. The CHANGELOG entry matches the change. — _automated review by `claude-bot-andresmgsl` · heavy-duty-review-bot_
grok-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-19 14:09:57 +00:00
grok-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: Approve — I agree with this as-is.

Why

  • Same disease as cast#109: literal ## Unreleased + #32 content check made the ceremony's own release PR fail CI by construction; fork tag rehearsals never run ci.yml, so drills missed it.
  • New guards use the exact changelog_section from release-lib.sh against the top section's field-2 token — same contract the workflow uses. Empty top / no ## still fail closed.
  • Rotting issue-number grep correctly removed. CI green.

No blockers.

**Verdict: Approve** — I agree with this as-is. ### Why - Same disease as cast#109: literal `## Unreleased` + `#32` content check made the ceremony's own release PR fail CI by construction; fork tag rehearsals never run `ci.yml`, so drills missed it. - New guards use the exact `changelog_section` from `release-lib.sh` against the top section's field-2 token — same contract the workflow uses. Empty top / no `## ` still fail closed. - Rotting issue-number grep correctly removed. CI green. No blockers.
dan-claude-bot commented 2026-07-19 14:14:19 +00:00 (Migrated from github.com)

@danmt — handing this one to you. Final state:

What it fixes: rig#44 — test/release.sh demanded the real CHANGELOG have a literal ## Unreleased section extracting non-empty and containing #32. All false by construction on the tree the ceremony's own release: X.Y.Z PR produces (it stamps that heading into ## X.Y.Z — date), so the first real release PR turned CI red and the flow blocked itself. Both drill rehearsals missed it: a fork rehearsal tags a branch, which runs release.yml and never ci.yml.

The fix: the guard now asserts its stated purpose — whatever the TOP ## section is (Unreleased between releases, the stamped version on and right after one), the exact changelog_section that release.yml runs extracts it non-empty. The rotting issue-number grep is gone. Verified green on both legitimate tree states; suites 41/0 and 325/0; CI-exact shellcheck clean.

Review history: one round — all three bots approved, no changes requested.

This PR gates the release: 0.1.0 PR, which I'll open the moment this merges. (cast's twin is heavy-duty/cast#109; box needed no fix — its suite pins already-released sections.)

🤖 Generated with Claude Code

@danmt — handing this one to you. Final state: **What it fixes:** rig#44 — `test/release.sh` demanded the real CHANGELOG have a literal `## Unreleased` section extracting non-empty and containing `#32`. All false by construction on the tree the ceremony's own `release: X.Y.Z` PR produces (it stamps that heading into `## X.Y.Z — date`), so the first real release PR turned CI red and the flow blocked itself. Both drill rehearsals missed it: a fork rehearsal tags a branch, which runs `release.yml` and never `ci.yml`. **The fix:** the guard now asserts its stated purpose — whatever the TOP `## ` section is (`Unreleased` between releases, the stamped version on and right after one), the exact `changelog_section` that `release.yml` runs extracts it non-empty. The rotting issue-number grep is gone. Verified green on both legitimate tree states; suites 41/0 and 325/0; CI-exact shellcheck clean. **Review history:** one round — all three bots approved, no changes requested. **This PR gates the `release: 0.1.0` PR**, which I'll open the moment this merges. (cast's twin is heavy-duty/cast#109; box needed no fix — its suite pins already-released sections.) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/rig#45
No description provided.