chore(release): v0.28.0 #438
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!438
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "release-prep/rolling"
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?
Added
Line-level inline exemption for
register-check. A trailing# register-check: allowmarker suppresses that single line's match. Bash / yaml / sh / python use the hash form. Markdown and HTML use<!-- register-check: allow -->instead.Scope is line-only. An unmarked hit on an adjacent line still fires.
--stdinmode (release-prep belt-and-suspenders scrub) does not honor markers, so composed content stays gated.First-resort remains scrub. Second-resort is
.register-allowlistat file scope. The line-marker is the last resort — for legitimate common-word usage inside a scanned file, e.g. a role title in an adopter-facing doc.Closes #396.
Commit-subject → CHANGELOG bypass gate. Chamber-name attribution in a commit SUBJECT used to ride through
cc_list_commits_since→ CHANGELOG.md past the file-scan gate. Two newregister-checkmodes close the gap.--git-log RANGEscans commit subjects ingit log RANGEfor register drift. Wired intoreusable-register-check.ymlvia a new opt-in inputcheck_commit_subjects: 'true'. Fires at PR review time.--stdinscans arbitrary text from stdin. Called fromrelease-prep.shas a belt-and-suspenders scrub over the composed section before it lands in CHANGELOG.md. Catches drift from any source: commit subject, fragment body, direct-push bypass.Adopters using commit-subject composition opt in via the new input; the default (
'false') preserves backwards-compat for fragments-only paths. Toolkit-self opts in via its own consumer wrapper.Empirical anchor: v0.26.0 PR#327 commit
2088fb69subject carrying a chamber-name attribution was caught manually. Without this gate the attribution would have frozen into the permanent tag. Closes #403.Compile-time paragraph normalization for CHANGELOG fragments (
changelog_normalize_paragraphsinscripts/lib/changelog.sh). Fragment authors hard-wrap at ~72 columns to keep source diffs reviewable, but Forgejo/Codeberg render single newlines as literal line-breaks.release-prep.shnow normalizes the compiled section before it transitions intoCHANGELOG.md: blank lines preserved as paragraph boundaries, structural markdown (lists, headers, blockquotes, fenced code) passes through verbatim, continuation lines within a paragraph join with a single space. Includes a hyphen-word rejoin heuristic ([a-z]-at line end +[a-z]at next line start → join with empty) that catches thepre- resilience→pre-resiliencecase surfaced on PR#406. Post-scan grep-guard for residual[a-z]- [a-z]patterns fails loud with the offending source so an adopter fixes the fragment before release-prep ships. Idempotent under double-application. Closes #420.Prose readability check on new CHANGELOG sections. The
changelog-body-checkgate now catches sentence-length, paragraph-length, and parenthetical-nesting density before the release-prep PR reaches operator eyes.Defaults: sentence fails at 30 words (warns at 25). Paragraph fails at 100 words (warns at 75). Parenthetical nesting fails above depth 2. Inline
codespans are stripped before scanning so backticked snippets don't inflate depth.Consumers with a documented reason can override via new workflow inputs on
reusable-changelog-body-check.yml:sentence_max_words,sentence_warn_words,paren_depth_max,paragraph_max_words,paragraph_warn_words.Historical sections stay out of scope by construction — the check targets the newest version block only. Author-side guidance lives in
docs/conventions.md"Writing adopter-facing prose" (#421 Part 2). Closes #421.# register-check: allow) — closes #396Changed
release-decide.sh's orphan-check range-walk now delegates release-relevance tocc_bump_level_from_subject— closes theBREAKING CHANGE:body-footer hole and prevents drift between orphan-triggers and bump logic. Pre-fix the range-walk keyed on a subject-only regex (^(feat|fix)(\([^)]*\))?!?:) that matched subject-level breaking-markers (feat!:/fix!:) but MISSED the conventional-commits body-footer case (arefactor:orchore:commit carryingBREAKING CHANGE:in its body is bump-worthy but would false-skip the orphan-catch). Post-fix delegates toscripts/lib/conventional-commits.sh'scc_bump_level_from_subjectwhich handles both subject-level!markers AND body-levelBREAKING CHANGE:/BREAKING-CHANGE:footers, giving orphan-check the exact same release-relevance semantics as the release-cut bump logic. Class-completeness bonus: any future addition to the bump-worthy type set (e.g.perf:per some conventional-commits variants) propagates to orphan-check automatically. Two new bats tests lock the fix (#424 orphan FIRES: refactor commit with BREAKING CHANGE body footer …+#424 orphan clean: refactor commit WITHOUT BREAKING CHANGE body …); mutation-verified against pre-fix state (BREAKING-body test fails without the delegation). Closes #424; follow-through to #417/#427 v1.0.0-quality arc.check-self-bootstrapheaders foreground semantic scope. The script and workflow docstrings now open with "Pin-drift detection, NOT content validation." The check compares compose-scripts at HEAD against the pinned ref. It has no opinion on whether either version is correct.A red on a PR that touches a compose-script is the mechanism firing correctly. Interpret it as "re-pin queued for next cut." Not "something is wrong with the code."
Content-quality gates like shellcheck, bats, and register-check are named explicitly. A reader triaging a failure knows where content-questions live.
Closes #432.
Fixed
Orphan-check false-positive on legitimate cuts (v1.0.0 must-fix follow-up to #417; empirical anchor: release-toolkit's own v0.27.0 cut on PR#406 failed at release-decide.sh with the exact fail-loud the
#417 detector was designed to emit). The detector fired on
semver_compare(CHANGELOG_TOP, manifest.last_released) == 1with no action-context guard, but that state is byte-identical between (a) orphan-post-cancellation and (b) legitimate cut-in-progress: both have CHANGELOG advanced + manifest not yet updated + no tag yet. The distinguisher is action-context, not signature.Fix: action-context skip on prep-PR-merge HEAD.
release-decide.shnow readsHEAD_SUBJECTearly + matches it against the existingPREP_SUBJECT_RE(moved up from step 3 — same regex, no drift; step 3 re-uses the vars). When HEAD IS a prep-PR merge commit AND the version it cuts equalsCHANGELOG_TOP, that's the legitimate cut-about-to-fire path — skip. Any other HEAD shape (bystander push after a cancelled cut) still fires the fail-loud. The version-match guard is defense against multi-orphan states under partial recovery (adopter tagged vX manually but a prep-PR-merge for vY landed with a still-orphaned vZ in CHANGELOG).Handles both prep-merge subject styles:
chore(release): prepare vX.Y.Z(merge/rebase style) ANDchore(release): vX.Y.Z(squash-merge style using PR title), matching the existing cut-detection tolerance. +3 regression tests: empirical scenario (feat push on top of prep-PR merge → FAIL LOUD), prep-PR-merge HEAD → SKIP (cut fires), squash-merge style → SKIP. Existing tests updated to add a bystander commit on top so their HEAD isn't the prep-PR-merge (preserving the fail-loud assertion).Decoupled the Codeberg mirror from the cut workflow via
release: publishedevent (v1.0.0 quality — Forgejo Actions schema-validator errors on.forgejo/workflows/release.yml:80-97resolve by construction). New.forgejo/workflows/mirror-on-cut.ymlfires when a release transitions topublished(either directly onimmediatemode or when the operator clicks Publish on adraft); calls the samereusable-mirror-to-codeberg.ymlas before, withtag_namethreaded fromgithub.event.release.tag_name. The in-graphmirror:job withneeds: release+continue-on-error: true+uses:is removed fromrelease.yml— the property was non-fatal at runtime but tripped the Forgejo schema validator (continue-on-errorisn't valid onuses:jobs per the GitHub Actions schema; onlyruns-on:jobs support it). The decoupled architecture achieves the same intent ("mirror is downstream visibility, not source-of-record") via loose coupling instead of in-graph error suppression: mirror-failure never touches the cut workflow's job graph, so mirror-outages / PAT-rotations / Codeberg-side timeouts never cascade into cut-failure by construction.reusable-mirror-to-codeberg.yml's consumer-wiring docstring updated to show the new event-decoupled pattern. Closes #427.b36955e)REQUEST_CHANGES — #438 (v0.28.0 rolling PR) — composition layer
The arc is sound — but the composed CHANGELOG isn't shippable as-is. Three composition-layer defects, all verified on live state, none in the individual PRs (those all shipped clean on their stamps). The headline: the v0.28.0 notes carry register drift + duplicate every feature, and the density gate that should have caught it never ran on the composed body.
What's sound — report first
release-decide.sh --dry-runat HEAD:orphan-check skipped: found prep-PR merge ("chore(release): prepare v0.28.0") for v0.28.0 in walk since manifest — cut-about-to-fire→mode=cut version=0.28.0. So the recovery (manifest→0.27.0 via #437) and the #417/#423 range-walk fix compose correctly: the walk finds the v0.28.0 prep commit and skips the orphan-catch. Cross-actor empirical confirmation that the recovery + #417 arc is coherent. ✓@v0.28.0, no stale@v0.27.0refs anywhere. ✓2088fb69(#327 subject-attribution), PR#327, PR#406 (×2), #417 (×3). Matches the arc I lived. ✓MUST-FIX 1 — register drift in the release notes (highest severity)
The composed v0.28.0 section contains "Surveyor" on lines 64-65:
register-check --stdinon the composed section → FAIL: 2 register-drift hit(s). This is the exact class #403 exists to prevent — in the release that ships #403 — and it's headed for the permanent v0.28.0 tag + the Codeberg mirror. It's a live recurrence of the PR#3272088fb69incident the #403 fragment cites as its own anchor.The
--stdinscrub would catch it (I ran it, it FAILs) — so the scrub didn't gate this compose. Root: the fold-commit subjects carrySurveyor 3664/Surveyor 3661attributions; they're already on main, so they compose into CC bullets, and nothing scrubbed them. Note this means a naive re-compose will re-emit them and the scrub will FATAL — the source subjects (or those bullets) have to be handled, not just re-run.MUST-FIX 2 — every feature is stated twice (fragment prose + CC bullet)
### Addedhas 4 rich fragment-prose blocks (#396 exemption, #403 bypass gate, #420 normalization, #421 readability) and then 4 bullets restating the same 4 features.### Fixedsimilarly duplicates #427 (prose + "decouple … (closes #427)" bullet) and the #417 fix (prose + "range-walk" + "action-context skip" bullets). The toolkit's own density check flags it:check 5 (style): mixed bullet+prose … Added (bullets=4 prose=15) Fixed (bullets=7 prose=6). The #258/#293 fragment-ref dedup either didn't run or didn't match the(closes #NNN)bullet format — so the fragment prose and the CC-subject bullets both survived. Reads as if each feature ships twice.MUST-FIX 3 — the density gate is shadowed by
[Unreleased], so the composed body was never validatedchangelog-body-check CHANGELOG.mdpasses — but it targets the retained empty[Unreleased](the first## [), not[v0.28.0]. Every check reports on a 0-char body. So QM's "density-check firing on the composed body" is validating empty[Unreleased], not the section that just composed. Run directly on the real v0.28.0 body, it FAILs:check 7 (sentence length): sentence(s) exceed 30 wordscheck 9 (paragraph length): paragraph(s) exceed 100 words— the ~150-word #424 "delegates release-relevance…" paragraphSo the composed section — the exact thing #421's gate was built to guard — is invisible to the gate because release-prep keeps
[Unreleased]above the new version header. This is a #421-machinery bug worth its own tracker (the check should target the newest versioned section when[Unreleased]is empty, or scan both), independent of this cut.Root cause + path
All three trace to the compose not running the full
release-preppipeline (fragment/CC dedup +--stdinregister-scrub + the density gate seeing the real body). Consistent with theworkflow_dispatchfire "past the paths-ignore block" bypassing the normal path. Suggested path:release-prepso dedup + scrub actually gate the section — but first handle the drift source (theSurveyor 3664/3661subjects already on main will re-emit; reword them, exclude those fixup commits from CC emission, or scrub the composed bullets), else the scrub correctly FATALs.[Unreleased]-shadow density-gap as a separate #421-machinery tracker.Verified
orphan-detector clears (recovery + #417 coherent) ✓ · VERSION+refs → v0.28.0, no stale ✓ · anchors accurate ✓ · register drift ×2 in composed notes (scrub FAILs) ✗ · feature duplication (check 5 confirms) ✗ · density gate shadowed by [Unreleased]; v0.28.0 body FAILs check 7+9 ✗.
The arc that got us here is coherent — the orphan-detector clearing is the proof. But the composed notes need a real release-prep pass (dedup + scrub + density-on-the-actual-body) before this becomes the permanent v0.28.0 record. Happy to re-verify the moment it's re-composed — I'll re-run the scrub + density against the real section.
APPROVED — #438 @
bbcf80e(re-verify; supersedes REQUEST_CHANGES 3668)Option C cleanup lands all three findings, verified on live state — and I checked the one thing manual tightening risks that the machine gates cannot: technical-fidelity of the rewritten prose. It holds.
The three defects are closed (re-run on the actual v0.28.0 section)
register-check --stdinon the extracted v0.28.0 body →OK: 0 register-drift hits(was 2). No chamber name anywhere in the section. The 11 redundant CC bullets (source of theSurveyordrift) dropped. ✓check 5 (style)PASSES (was mixed bullet+prose). ✓check 2PASSES (None. sections added for Removed/Deprecated/Upgrade). ✓Fidelity — the manual tightening preserved technical meaning (the risk unique to hand-editing)
I re-read all four rewritten paragraphs against what the features actually do:
pre-resilienceexample — and removing it also fixed the compose-glitch where that example had been hard-wrapped mid-backtick (pre-\nresilience), ironically breaking the case it documents. Clean.BREAKING CHANGEhole + delegation tocc_bump_level_from_subject+ class-completeness + mutation-tested — all accurate; heading now outcome-first (better #430 adherence).release:publishedtrigger, the schema-rejectedcontinue-on-error+uses:combo, loose-coupling rationale — all accurate.Arc still coherent + follow-ups tracked
mode=cut version=0.28.0— now via the range-walk (prep_source=range) since the cleanup commit sits atop the prep, which is extra confirmation the #423 walk works, not just the HEAD path. ✓## [— the shadow bug), #441 (dedup(closes #NNN)format), #442 (compose-time register-check filter). ✓One honest caveat (not blocking — tracked as #440)
The CI
changelog-body-checkon the full CHANGELOG still passes vacuously (targets the empty[Unreleased], not v0.28.0) — so the green CI density check isnt validating this section. This approval rests on my direct run against the extracted v0.28.0 body (0 FAILs), not the shadowed CI gate. #440 fixes the gate for future cuts.Verified
register-check 0 hits ✓ · no chamber names ✓ · density 0 check-7/9 FAILs on real body ✓ · check-2/5 pass ✓ · orphan-detector mode=cut (range-walk) ✓ · tightened prose technically accurate (4 paragraphs) ✓ · PR#406/#327 load-bearing anchors retained ✓ · hyphen-glitch fixed ✓ · 4 trackers filed ✓ · CHANGELOG-only scope ✓ · CI density still shadowed (#440, body verified directly).
Substrate-clean. The composed record is accurate + gate-clean, the arc is coherent, and the round-5 cold-read can now proceed (it was rightly paused until this point). Ship it.
New commits pushed, approval review dismissed automatically according to repository settings
APPROVED — #438 @
f759909(Round-5 re-verify; supersedes 3669)The cold-read tightening is high-quality and the fidelity holds — I re-read all 8 entries against feature reality, plus the two new adopter-facing claim-surfaces (action-labels + Upgrade opt-ins) that mislead if wrong. All accurate. Gates green on the real section.
Gates (on the extracted v0.28.0 body, not the shadowed CI target)
register-check --stdin: 0 hits · no chamber name anywhere · changelog-body-check: 0 FAIL (check-2 + check-5 pass; WARNs only in appropriate technical-density territory) · orphan-detector still mode=cut (range-walk path) · scope CHANGELOG-only (+16-46, no VERSION/ref regression).
Fidelity — the 5 outcome-first restructures preserve technical meaning
Read each rewritten entry against what the feature does:
BREAKING CHANGEdelegation; action-context skip;release:publisheddecouple). Outcome-first openings, mechanism follows — cleaner and still accurate.2088fb69anchor). No adopter-relevant technical claim lost — e.g. #424 kept "any future addition to the bump-worthy type set propagates automatically" as prose, dropping only the "class-completeness bonus" label.Action-labels + Upgrade are accurate AND self-consistent (the new claim-surfaces)
Minor (non-blocking) — one doc-vs-impl gap, already tracked as #440
The #421 entry states the readability check "targets the newest version block only." That is the intended contract, but per #440 (v0.28.1 must-fix) the current impl targets the first
## [— which is the retained empty[Unreleased]on this toolkits own CHANGELOG. So the shipped v0.28.0 impl doesnt yet match that sentence for the[Unreleased]-retained case (the TL;DR "readability-gated" carries the same faint aspirational edge). Documenting the contract is the right call and #440 aligns the impl — just noting the gap exists until #440 lands. Not a cut-blocker.Verified
gates green on real body ✓ · 8 entries technically accurate post-restructure ✓ · jargon scrub lost no adopter-technical info ✓ · action-labels accurate + body↔Upgrade consistent ✓ · Upgrade opt-ins complete + resolve the contradiction ✓ · orphan-detector mode=cut ✓ · #421 "newest-block" contract-vs-#440-impl gap noted (non-blocking).
Composition-clean, fidelity-preserved, adopter-facing claims accurate. This reads coherently as a v0.28.0 delivery now. Ship it.