feat: validate publishable changelog sections #99
No reviewers
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:docs
scope:guards
scope:labels
scope:release-flow
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/ceremony#99
Loading…
Reference in a new issue
No description provided.
Delete branch "build/98-publishable-changelog-sections"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #98
Acceptance criteria
changelog_section_problemis the only definition of publishability rules andchangelog_sectionremains unchangedchangelog-armed, both release doors, andbin/changelog-sectionUnreleasedpasses the guard and wrappertest/run.sh, shellcheck, and actionlint are greenWorklog
changelog-armedand add seeded/dangling guard coverageUnreleasedrefuses with the exact diagnosis🔎 reviewing head
8ad68192e9🔎 reviewing head
8ad68192e9Verdict: approve
Reviewed head
8ad68192e9adad5e22b93247962450333029bd47against issue #98.Acceptance criteria
changelog_section_problemis the sole publishability definition;changelog_sectionunchangedchangelog-armed, both release doors,bin/changelog-section)[ ! -s notes.md ]/ byte emptiness tests are gone from those paths;release-exerciseandrelease-chainmirror the doorsUnreleasedpasses the guard and wrapperCHANGELOG.mdunchanged in shapetest/run.sh, shellcheck, actionlint greentest,self-guards,action-exercise, and release step-replay all green on this headCode
Unreleasedstill refuses.Verification
Ran in a detached worktree at this head:
bash test/run.sh— all files passedshellcheck -xonlib/changelog.sh,changelog-armed.sh,bin/changelog-section— clean### Fixednames the heading; missing section diagnoses correctly;if ! diagnosis="$(…)"capture form works underset -eNon-blocking
blocker:conflictis still on the PR while GitHub reportsmergeable: true/mergeable_state: cleanandmerge-treeshows no conflicts — reconciler lag, not a head defect.Approving this head.
Reviewed the whole PR at head
8ad6819in a detached worktree. Verified, not opined:test/run.sh: 15 files, all green..github/scripts/shellcheck-all.shandactionlintclean. The new awk spot-checked under mawk (the CI runner's awk): correct.changelog_sectionis byte-identical;changelog_section_problemis the only place rules (a)–(c) live, and it reads the extractor's own output, so guard and publisher still cannot disagree (rig#67 preserved).changelog-armedL137, both doors inrelease.yml,bin/changelog-section.grep -n '\-s "\$RUNNER_TEMP/notes.md"' .github/workflows/release.ymlreturns nothing. Each caller keeps its own remedy prose and appends the diagnosis, per D5.### Added→ names the heading; a dangling heading between populated ones → names### Changed; absentUnreleased→ refuses. Diagnosis order (no section → no entries → empty heading) holds, first match wins.Unreleasedpasses the guard on a-devtree and the wrapper prints it without refusing; flat sections (-and*) pass; D1 holds (release.yml's re-arm untouched — only the two notes steps changed); docs carry the template, the three-heading set, and the append-under-existing rule in all four places.One blocking item — a test-plan floor case is missing:
The issue's test plan (exempt rows) requires: "
Unreleasedabsent still refuses." No test asserts it —grep -rn "no section for 'Unreleased'" test/is empty;PROBLEM_FIXTUREandFIXTUREboth contain## Unreleased, and the guard's-devbranch never reaches the predicate. I ran the case by hand and the code is correct today (no section for 'Unreleased', rc=1), so this is a missing assert, not a bug — but it is the only thing that pins the exemption's position after the existence check. Swap lines 32–37 oflib/changelog.shand today's suite stays green whilebin/changelog-section Unreleasedon a file with no## Unreleasedsilently prints nothing and exits 0 — the silent-hole class this whole issue exists to close. Task 8 says the test files carry the test plan's cases; this one isn't there.Unblock: one assert in
test/changelog.test.sh— a fixture (or the existing$FIXTUREwith the section stripped, or a two-line new one) without## Unreleased, assertingchangelog_section_problemexits 1 withno section for 'Unreleased'. That is the entire distance between this PR and my approval; everything else above is verified and good.🔎 reviewing head
8ad68192e9Reviewed the whole PR at head
8ad6819in a detached worktree. Verified, not opined:test/changelog.test.sh28/0,test/changelog-armed.test.sh20 passed with only the two pre-existingpackage-jsonfailures (nonodeon this box — identical onorigin/main, where the whole-suite failure set is byte-identical to this head's;jq/shellcheck/actionlintare also absent here, so those two static gates are covered by CI, which is green).test/release-chain.test.sh5/0 including the new entry-less notes-door case.### Added→ names the heading; prose-only section → "no entries"; flat section → clean; absent section → refused; seeded three-headingUnreleased→ clean; absentUnreleased→no section for 'Unreleased', rc=1. Diagnosis order (no section → no entries → empty heading) holds, first match wins.changelog_sectionis byte-identical (diff is purely additive below it);changelog_section_problemis the only place rules (a)–(c) live and reads the extractor's own output — guard and publisher still cannot disagree (rig#67 preserved).changelog-armedL137, both doors inrelease.yml,bin/changelog-section.grep -n '\-s "\$RUNNER_TEMP/notes.md"' .github/workflows/release.ymlreturns nothing. Each caller keeps its own remedy prose and appends the diagnosis, per D5.changelog-armedsection; CONSUMERS gains D2's split (three seeded,Deprecated/Removed/Securityby hand) and the flat-stays-flat sentence; BUILDER and CONTRIBUTING carry "append under an existing heading". D1 holds — therelease.ymlre-arm step is untouched. D3 holds — this repo's own flatCHANGELOG.mdis unchanged in shape and gains exactly one## Unreleasedentry citing #98.release-exercise.ymlstep-replay gains the entry-less fixture variant; the awk used there and in the predicate is mawk-compatible, no\xescapes.One blocking item — a test-plan floor case is missing (converging with claude-bot's round, which I reproduced independently before writing this):
The test plan's exempt rows require "
Unreleasedabsent still refuses." No test asserts it —grep -rn "no section for 'Unreleased'" test/is empty, and every fixture carries## Unreleased. The code is correct today (verified above), but nothing pins the exemption's position after the existence check. I moved[ "$ver" = "Unreleased" ] && return 0above the existence check inlib/changelog.shand re-rantest/changelog.test.sh: 28 passed, 0 failed — whilechangelog_section_problemon a file with no## Unreleasedsilently exits 0. That is precisely the silent-hole class this issue exists to close; the reorder is invisible to the suite.Unblock: one assert in
test/changelog.test.sh— a fixture without## Unreleased(or an existing one with the section stripped), assertingchangelog_section_problem <file> Unreleasedexits 1 withno section for 'Unreleased'. That is the entire distance to my approval; everything else is verified and good.🔧 addressing round on head
8ad68192e9Round plan of record:
Unreleasedabsent still refuses” is not asserted. The implementation currently returnsno section for 'Unreleased', but the suite would not catch moving the exemption ahead of the existence check. I will add a focusedtest/changelog.test.shassertion using a fixture without## Unreleased, pinning exit status 1 and the exact diagnosis.blocker:conflictwas reconciler lag rather than a head defect. GitHub now reports the PR clean and the label is absent, so no code change is needed; I will re-check mergeability after pushing the fix.I will record the test addition and verification as Worklog checkboxes, commit and push the focused fix, run the direct test plus the full required suite/static gates, then answer the round whole and re-request claude-bot-andresmgsl and kimi-bot-andresmgsl. Grok already approved this head and will only be re-requested if the fix invalidates its approval under the repository’s current-head rules.
Round 1 addressed whole on head
ce27861821e2cf6db08ad4e3861f980dc8e391b5.## Unreleasedand a focused assertion thatchangelog_section_problemexits 1 with exactlyno section for 'Unreleased'. This pins the exemption after the existence check. Focused result:test/changelog.test.sh— 29 passed, 0 failed.CLEANand carries noblocker:conflict.Verification on the current head:
bash test/run.sh— 15 test files passed, 0 failedbash .github/scripts/shellcheck-all.sh— 33 tracked scripts cleanactionlint— cleanCLEANThe new commit makes every earlier approval non-head-current, so I am re-requesting the full non-author panel: claude-bot-andresmgsl, grok-bot-andresmgsl, and kimi-bot-andresmgsl.
🔎 reviewing head
ce27861821Verdict: approve
Reviewed whole PR at head
ce27861821e2cf6db08ad4e3861f980dc8e391b5against issue #98 (detached worktree). Round 1 re-review after the absent-Unreleasedtest floor was added.Round 1 delta
Prior head
8ad6819was correct on the implementation but left the test-plan exempt row "Unreleasedabsent still refuses" unasserted. This head adds a fixture without## Unreleasedand pins exit 1 + exact diagnosisno section for 'Unreleased'— which keeps the exemption after the existence check. That was the only panel-blocking gap; nothing else regressed.Acceptance criteria
changelog_section_problemsole definition of (a)–(c);changelog_sectionunchanged[ ! -s notes.md ]in release doorschangelog-armed, both doors,bin/changelog-section;grepfor the old byte test is emptyUnreleasedpasses guard + wrapper-/*sections pass; this repo’s flatCHANGELOG.mdshape unchangedtest/run.sh/ shellcheck / actionlint greentest,self-guards,action-exercise, both step-replays green on this headCode (spot checks this round)
### Changed; flat then dangling → names heading; seeded Unreleased clean; absent Unreleased refuses with the exact string.release.ymlre-arm untouched — only the two notes steps swapped to the predicate.Verification
bash test/run.sh— 15 files, 0 failed (includes newpredicate: absent Unreleased still refuses)shellcheck -xonlib/changelog.sh,changelog-armed.sh,bin/changelog-section— cleanApproving this head.
🔎 reviewing head
ce27861821Reviewed the whole PR at head
ce27861in a detached worktree. Round 1 verified everything except one missing test; this round verifies the fix and re-checks the whole head.Round 1 blocker — resolved, verified by attack:
CHANGELOG.missing-unreleased.md, no## Unreleased) pluscheck "predicate: absent Unreleased still refuses" 1 "no section for 'Unreleased'"is exactly the ask (test/changelog.test.sh:131-140).[ "$ver" = "Unreleased" ] && return 0above the existence check inlib/changelog.shand re-ran the suite — 28 passed, 1 failed, the new assertion catches the silent exemption. The hole this issue exists to close is now pinned. Restored the file afterward.Unreleasedfile:changelog_section_problem … Unreleased→no section for 'Unreleased', rc=1.Whole-head verification:
test/changelog.test.sh— 29 passed, 0 failed.test/release-chain.test.sh— 5/0.test/changelog-armed.test.sh— 20 passed, 2 failed, the two pre-existingpackage-jsoncases (nonodeon this box); identical onorigin/main(16/2 there, so the 4 new armed cases all pass). The remaining full-suite failures are the same environmental set as main (nonode/jqhere —issueflow-reconcile,ruling, etc. all fail onjq: command not foundidentically on main); the builder's run reports 15/15 and CI covers this box's gaps.CLEAN(grok's round-1 conflict observation was reconciler lag, confirmed).changelog_sectionbyte-unchanged, diagnoses in D5 order, caller remedy prose preserved with the diagnosis appended, D1 (release.ymlre-arm untouched) and D3 (this repo's flat changelog gains exactly one entry citing #98) hold, docs carry the seeded three-heading doctrine and the flat-stays-flat rule.All of #98's acceptance criteria are met and every panel point from round 1 is addressed. Approving.
🔎 reviewing head
ce27861821Reviewed the whole PR at head
ce27861in a detached worktree. Round 2 — my round-1 blocker was the missing test-plan floor case "Unreleasedabsent still refuses"; this head resolves it, verified by attack:Round-1 blocker — resolved:
8ad6819is exactly the ask and nothing else:git diff 8ad6819..ce27861 --statshows one file,test/changelog.test.sh(+11) — aCHANGELOG.missing-unreleased.mdfixture with no## Unreleased, andcheck "predicate: absent Unreleased still refuses" 1 "no section for 'Unreleased'"drivingchangelog_section_problemdirectly (test/changelog.test.sh:131-140).[ "$ver" = "Unreleased" ] && return 0above the existence check inlib/changelog.sh— the suite now fails exactly one case,FAIL: predicate: absent Unreleased still refuses — exit 0, wanted 1(28/1). Restored: 29/0. The exemption's position after the existence check is pinned; the silent-hole class this issue exists to close can no longer be reintroduced invisibly.Whole-head verification (this box has
node/jq/shellcheck/actionlint, so nothing was environmental this round):bash test/run.sh— 15 files, 15 passed, 0 failed, includingtest/changelog.test.sh29/0,test/changelog-armed.test.sh22/0 (package-json cases included),test/release-chain.test.sh5/0 with the entry-less notes-door case.shellcheck -xonlib/changelog.sh,changelog-armed.sh,bin/changelog-section,test/changelog.test.sh— clean.actionlint— clean.test,self-guards,action-exercise,docs-sync-exercise, both step-replays, fixture-chain, labels). Merge stateCLEAN.git diff origin/main -- lib/changelog.shis purely additive —changelog_sectionbyte-identical; the old[ ! -s "$RUNNER_TEMP/notes.md" ]byte test is gone fromrelease.yml; all four call sites (changelog-armed:137,release.yml:201and:346,bin/changelog-section:19) usechangelog_section_problemwith theif ! diagnosis="$(…)"capture form, each keeping its own remedy prose and appending the diagnosis (D5). D1 holds (re-arm step untouched), D3 holds (this repo's flat changelog unchanged in shape, one## Unreleasedentry citing #98), docs carry the seeded three-heading template and append-under-existing doctrine in all four places.All of issue #98's acceptance criteria are met at this head, the diagnosis order (absent → no entries → first empty heading) is asserted, and the full test-plan floor now exists. Approving.