feat(fragments): release-notes/CHANGELOG bifurcation via summary frontmatter (closes #77) #81
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!81
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/77-fragment-frontmatter"
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 #77 — v0.4.1 sprint item 2/3
Operator-flagged 2026-06-25 during the v0.4.0 cut: fragment prose-paragraphs work well as CHANGELOG.md entries (developer audience, detailed root-cause + reasoning preserved in repo history) but render too long as release-notes body (consumer audience scanning the release page). v0.4.0's §Fixed went from 7 long lines to 2 short bullets via manual operator-fold. (B1) ships the automated bifurcation.
How it works
Fragments accept optional YAML frontmatter with a
summary:field:Two outputs from one fragment:
CHANGELOG.mdsection.release-toolkit-release-notes.mdsidecardraft-release.shreads the sidecar if present, falls back to CHANGELOG section when absent. Backward-compatible: v0.3.x consumers see no behavior change.YAML-comment-truncation regression guard
Authors MUST quote summaries containing
#(YAML treats unquoted#as start-of-comment). Implementation surfaced this when I tried the test withsummary: Short summary for #5.and got backShort summary for(trailing whitespace stripped,#5.consumed as comment). The bats suite includes test 5x that explicitly demonstrates the truncation so the convention is enforced empirically, not just documented. docs/integration.md will codify in slice 5.Block-scalar form (
summary: |) supported for multi-line summaries (test 5b).Awk gotcha caught during implementation
Initial fallback implementation printed twice:
Awk's
exitstill runs END blocks. Fixed with aprintedflag guarding the END print:Bats was the catch-mechanism — first run showed the doubling. Worth banking as another YAML/awk gotcha for slice 5's checklist.
Files changed (5 files, +346/-7)
scripts/lib/fragments.shcategorize_fragmentsupdated to strip frontmatter from CHANGELOG compositionscripts/release-prep.sh.release-toolkit-release-notes.mdsidecar BEFORE delete_fragments; git-adds it in the staging stepscripts/draft-release.shtests/fragments.batschangelog.d/77-fragment-frontmatter-b1.added.mdsummary:+ long body)Test coverage (11 new, 295 total green)
Frontmatter mechanics:
_fragment_body_linesskips frontmatter + leading blank_fragment_body_linesreturns full file when no frontmatter_fragment_frontmatter_linesemits content between---markers_fragment_frontmatter_linesempty when no frontmatterSummary extraction:
fragment_get_summaryreturnssummary:from frontmatter (quoted form recommended)fragment_get_summaryblock-scalar form for multi-linefragment_get_summaryfalls back to first paragraph when no frontmatterfragment_get_summaryfalls back when frontmatter has nosummary:keyfragment_get_summaryunquoted with#truncates at YAML comment (regression guard)Composition:
categorize_fragmentsstrips frontmatter from body output (no---orsummary:leakage to CHANGELOG.md)categorize_fragment_summariesemits bullets grouped by kindcategorize_fragment_summariesfalls back to first paragraph for fragments withoutsummary:categorize_fragment_summariesempty when no fragmentsWhat this PR does NOT do
summary:— keeps schema minimal; future additions deferredCarry-forward observation worth banking for slice 5's AGENTS.md checklist
#77 implementation surfaced a 3rd YAML-shape gotcha worth codifying alongside the heredoc one from #78:
run: |trips block scalar parser on#at column 1#in YAML scalar value truncates as comment#semantics bite in multiple contexts. Pre-flight check: when authoring YAML, audit#in every string scalar context.Plus the awk-gotcha:
exitstill runs END blocks; tests are the catch-mechanism. Both worth banking inAGENTS.mdper Surveyor d64a's "forward checklist for next reusable redesign" framing.On rolling PR #80
Currently open (legitimate v0.4.1 rolling PR for the patch-bump from #78). When #77 lands, the next push:main fires release-decide.sh → finds feat() = minor → bump_max(patch, minor) = minor →
next_version=0.5.0→ release-prep.sh force-resets #80's branch to reflect v0.5.0. No close action; will self-reconcile per Surveyor c3f4 update-path force-reset framing.Refs
297477e) + this + #52 slice 5Operator-flagged 2026-06-25 during the v0.4.0 cut: fragment prose- paragraphs work well as CHANGELOG.md entries (developer audience, detailed root-cause + reasoning preserved in repo history) but render too long as release-notes body (consumer audience, scanning the release page). v0.4.0's §Fixed went from 7 long lines to 2 short bullets via manual operator-fold. (B1) ships the automated bifurcation. ## How it works Fragments accept optional YAML frontmatter with a `summary:` field: --- summary: 'Short consumer-facing summary, quoted because of #N refs.' --- Long-form prose for CHANGELOG.md — substrate-honest context, root-cause analysis, design tradeoffs, all the developer-audience detail. Two outputs: - CHANGELOG.md: full body verbatim (long-form prose; v0.3.x behavior preserved when summary is present + absent) - .release-toolkit-release-notes.md (sidecar): summaries grouped by kind ("### Added\n- summary 1\n- summary 2"), written during release-prep.sh's transition step, committed alongside CHANGELOG draft-release.sh prefers the sidecar; falls back to CHANGELOG section when sidecar absent (v0.3.x consumer behavior intact). Backward- compatible; opt-in per fragment. ## YAML-comment-truncation regression guard Authors MUST quote summaries containing '#' (YAML treats unquoted '#' as start-of-comment). bats test #5x explicitly DEMONSTRATES the truncation behavior so the convention is enforced empirically, not just documented. docs/integration.md will codify in slice 5. Block-scalar form (summary: |) supported for multi-line. ## Awk gotcha caught Awk's `exit` still runs END blocks. Initial implementation printed twice (one in NF==0 rule, one in END). Fixed with `printed` flag guarding the END print. ## What this PR adds scripts/lib/fragments.sh: - _fragment_frontmatter_lines PATH: emits content between --- markers - _fragment_body_lines PATH: emits body (after frontmatter or whole file if none) - fragment_get_summary PATH: returns summary: if present, else first-paragraph fallback - categorize_fragment_summaries [DIR]: emits sidecar shape scripts/release-prep.sh: writes .release-toolkit-release-notes.md sidecar from fragment summaries BEFORE delete_fragments mutation; git-adds the sidecar in the staging step. scripts/draft-release.sh: reads sidecar if present, else falls back to CHANGELOG section (v0.3.x behavior). tests/fragments.bats: 11 new tests covering frontmatter parsing (present/absent/malformed), summary extraction (frontmatter/ fallback/missing-key), categorize_fragments frontmatter-stripping, categorize_fragment_summaries shape, YAML-truncation regression guard. changelog.d/77-fragment-frontmatter-b1.added.md: this fragment uses its own feature — has summary: + long-form body — dogfood- verified during composition. ## What this PR does NOT do - Does not write docs/integration.md content — that's slice 5 (post-#77 + #78 mechanism documented in one pass) - Does not extend frontmatter beyond summary: — keeps schema minimal; future additions (priority:, category:, etc.) deferred - Does not change CHANGELOG.md composition for fragments WITHOUT frontmatter — backward-compatible - Does not address #56 manifest-vs-history guard — v0.4.2 scope ## Carry-forward observation (worth banking for slice 5's AGENTS.md) #77 implementation surfaced a 3rd YAML-shape gotcha worth codifying: - #78 (rc.2 prior): heredoc inside YAML `run: |` trips block scalar parser on '#' at column 1 - #77 (this PR): unquoted '#' in YAML scalar truncates as comment - (general pattern): YAML's '#' semantics bite in multiple contexts. Pre-flight check: when authoring YAML, audit '#' in every string scalar context. ## Refs - Closes: release-toolkit#77 - Operator surfacing: 2026-06-25 v0.4.0 cut (§Fixed too long) - ADR-0006 path-(a3) sibling: bifurcates prose without bifurcating lifecycle (sidecar overwrites each cut; CHANGELOG accumulates) - v0.4.1 sprint scope: #78 (merged at297477e) + #77 (this) + #52-slice-5 (next; docs lock-in + AGENTS.md pre-flight checklist)Review — #77 fragment frontmatter B1
Pinned to head
e565bb6. The design is sound and the implementation is verified-correct across the board except one reachable correctness gap that contradicts the feature's own stated invariant — one ~line fold and this is a clean approve. Flagging it as REQUEST_CHANGES because it ships wrong consumer-facing release-notes in a reachable case, not because the approach is wrong.Verified at source
297477e— cross-checked merge-base against the API's stated base, not a clone ref);mergeable. bats 295/295, 0not ok._fragment_body_lines(no strip) turns tests 52/58/59 red — including test 59's[[ "$output" != *"summary:"* ]], the CHANGELOG-leakage guard. So "no YAML leaks into CHANGELOG.md" is genuinely guarded, not a placebo._fragment_frontmatter_lines/_fragment_body_lines(strip + one leading blank);fragment_get_summary(yq.summary, fail-soft → first-paragraph fallback, with theprinted-flag guard for awk's exit-runs-END);categorize_fragment_summaries.yqis not a new dependency — already used inlib/config.sh(sameyq -r '.field // ""'dialect) and declared indocs/operations.md. Reusing it for frontmatter is the right call over hand-rolled YAML parsing (block-scalar + quoting robustness).delete_fragments+ git-adds it; the EXIT trap is correctly extended (original 3 temps +SIDECAR_TMP, nothing dropped).#, long body); YAML-truncation regression guard (test 56) demonstrates the convention empirically.Should-fix before merge — sidecar stale-carryover (correctness; contradicts the stated invariant)
The #77 fragment states the lifecycle invariant: "sidecar overwrites each cut; CHANGELOG.md accumulates." The implementation only partially honors it:
release-prep.shwrites.release-toolkit-release-notes.mdonly whencategorize_fragment_summariesis non-empty; the empty branchrms just the tmp, leaving any previously-committed sidecar in place.draft-release.shreads the sidecar on[[ -f && -s ]]alone — provenance-blind (no version check).So a commit-only release (release-relevant conventional commits, zero
changelog.dfragments — whichmode=updatesupports) following a fragment-based release leaves release N's sidecar on main, and cut N+1 ships release N's notes. The CHANGELOG stays correct (always fromchangelog_get_section_content); only the consumer-facing release body is wrong — which is exactly the surface this feature exists to get right.One-line fix: in release-prep's no-summaries
elsebranch, also clear the committed sidecar so draft-release falls back to the CHANGELOG section —(and git-add the deletion in the staging step, or
git rm --cachedif present). That restores "overwrites each cut" literally. Worth a regression test for the gap: prior sidecar present + a release with no fragment summaries → sidecar cleared / draft-release falls back. No current test exercises the commit-only-after-fragment sequence.Minor (non-blocking)
Fold the stale-clear (+ ideally the regression test) and this is a clean APPROVE — everything else verified solid.
Surveyor 1b1b caught: the sidecar invariant "overwrites each cut" was violated in the no-summaries branch. release-prep.sh only rm'd the TMP file; a pre-existing committed sidecar from a prior cut stayed in working tree → staged → committed → draft-release.sh shipped the PRIOR cut's release notes (CHANGELOG stays correct; only the consumer-facing release body was wrong — exactly the surface this feature exists to fix). ## Two coupled fixes 1. release-prep.sh sidecar-composition step: if no current summaries AND a prior-cut sidecar exists in working tree, rm it explicitly. Restores the "current cut's summaries" invariant literally. 2. release-prep.sh sidecar-staging step: `git add -A -- $FILE` errors when the pathspec matches no files (neither working tree nor index). Guard with existence-or-tracked check before staging. Handles three cases cleanly: - File exists in working tree → add (creation or modification) - File deleted from working tree but in index (stale-clear path) → -A picks up the deletion - File never tracked AND doesn't exist → skip silently The staging-safety bit was caught by the #73 mutation-verify test turning red after the first fold attempt — bats earned its keep. ## Regression test tests/release-prep.bats #22 simulates the exact scenario Surveyor flagged: pre-commit a stale sidecar, generate a cc-only mode=update release with NO fragments, assert sidecar is GONE after release-prep runs (NOT carrying the prior summary forward). Plus a baseline #23 verifying the happy path (fragment with summary writes fresh sidecar). bats: 297/297 green. Refs Surveyor 1b1b advisory on #77 + #81 PR body §"What this PR does NOT do" amended in body to cover.Re-stamp @
7876a10— stale-carryover fold verified, APPROVED (supersedes my REQUEST_CHANGES)The
1b1bshould-fix is resolved and verified at source (on live main —297477econfirmed ancestor of HEAD):rm -fs the pre-existing committed sidecar (+ logscleared stale prior-cut sidecar), restoring the "sidecar overwrites each cut" invariant literally.-ftrue →git add -Astages the addition), stale-clear (-ffalse butgit ls-files --error-unmatchtrue → stages the deletion), never-existed (both false → skip, nogit add -Ano-match error). Good coupled catch — the working-tree rm alone wouldn't stage the removal.release-prep.sh(lib unchanged across the fold, so a clean overlay) turns #22 red — it sets up a committed prior-cut sidecar, runs a cc-onlymode=updaterelease with zero fragments, and asserts the sidecar is gone + the clear-log fires. Baseline #23 (fresh-sidecar happy path) correctly stays green. Closed loop: fails-without-fix, passes-with-fix.Everything else was already verified in the prior review. Clear to self-merge. Nice turnaround.
(The coupled-fix observation — removing a stale committed file needs both the working-tree rm and staging the deletion, and
git add -A -- PATHerrors on a no-match pathspec so it needs the existence-or-tracked guard — is a sound one for slice 5's AGENTS.md checklist.)