feat(fragments): release-notes vs CHANGELOG bifurcation — fragment frontmatter approach (B1) #77

Closed
opened 2026-06-25 22:04:29 +02:00 by quartermaster · 1 comment

Concrete motivation from v0.4.0

The v0.4.0 cut rolling PR (#76) surfaced the operator-fold pattern in detail: 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 on Forgejo).

The v0.4.0 §Fixed block went from 7 long-paragraph lines (post-workflow) to 2 short-bullet lines (post-fold). The substantive content moved from fragment-prose to commit messages + tracker bodies — a manual fold the operator had to request and QM had to execute. The substrate doesn't support short-prose-out + long-prose-stays-internal automatically.

This is concretely the ADR-0006 path (a3) substrate-honesty bite landing in production: Forgejo team's "release notes are not changelogs" position (Codeberg discussion #155) mapping to release-toolkit's actual operator workflow.

Proposed shape — fragment frontmatter (B1)

Add YAML frontmatter to changelog.d/<id>.<kind>.md fragments with two slots:

---
summary: |
  `scripts/release-prep.sh`: `DRY_RUN_INPUT`/`DRY_RUN_ENV` env-var sources now use truthy semantics — the literal string `"false"` no longer falsely enables dry-run.
---

The previous early-check (`-n "$DRY_RUN_INPUT"`) treated any non-empty value — including the literal string `"false"` — as a request to enable dry-run, and the later sanitization operated on the already-promoted `$DRY_RUN` (always `"1"`) so it couldn't catch the false-positive. v0.4.0's `_release.yml` workflow_call declares `dry_run: 'false'` as the default, which on push events expanded to `DRY_RUN_INPUT="false"` and silently dry-ran the rolling-PR opening — surfaced live during the v0.4.0 dogfood cut. The fix iterates each env-var source with a per-value truthy check (`-n && != "false" && != "0"`), matching the documented "any truthy value" contract. Closes #70.

Behavior

  • CHANGELOG.md section: composed from the body content (long-form, detailed). Same as today.
  • Release-notes body (draft release on Forgejo): composed from the summary: frontmatter field if present, falling back to the body's first paragraph if not. New behavior.
  • Backward compatible: fragments without frontmatter keep working as today (body used for both surfaces). No mandatory frontmatter; opt-in per fragment.

Composition mechanics

lib/fragments.sh gets a new helper fragment_get_summary PATH that:

  1. Reads the frontmatter (YAML) if present
  2. Returns the summary: field
  3. Falls back to first paragraph of body if no frontmatter or no summary:

draft-release.sh's body-source step uses fragment_get_summary for each fragment + falls back to first paragraph of cc-parsed commit body.

release-prep.sh's lib/changelog.sh composition uses the full body for CHANGELOG.md (existing behavior).

Why (B1) over the alternatives

Per the original Layer B comparison from operator surfacing 2026-06-25:

Option Why rejected for v0.4.x
(B2) Two-file convention (<id>.<kind>.md short + <id>.<kind>.detail.md long) Adds a second authoring slot per fragment; doubles the "where do I write this?" question for authors; pattern is also farther from RNA's frontmatter-shape if we later integrate (B3)
(B3) Adopt ADR-0006 path (a3) (release-notes prose stream separate from CHANGELOG; RNA-as-backend) The architectural decision deferred in ADR-0006; would require re-opening that ADR with concrete data + operator engagement on the path-(a3) commitments

(B1) gives the bifurcation cleanly without the architectural shift of (B3).

Substrate-honest connection to ADR-0006

ADR-0006's path (a3) names per-artifact-lifecycle as the dissolving option for the edit-lifecycle tension. (B1) is a lighter version of (a3) — it bifurcates the prose without bifurcating the lifecycle:

  • CHANGELOG.md: still consume-and-delete at cut (release-toolkit lifecycle)
  • Release-notes body: composed from summary at cut (one-shot, same lifecycle)
  • The summary lives in the same file as the body (single fragment), so the authoring workflow stays unchanged

(B1) doesn't preclude (B3) later; it gives the substrate-axis the operator wants WITHOUT the architectural commitment.

Implementation sketch

  1. lib/fragments.sh: add fragment_get_summary PATH helper + extend fragment_get_body PATH to skip frontmatter when present
  2. lib/changelog.sh: composition step uses body verbatim (existing behavior, but skip frontmatter parsing)
  3. draft-release.sh: body-source step iterates fragments via fragment_get_summary instead of body
  4. lib/conventional-commits.sh: cc-parsed commits get the first paragraph of body as summary (already approximate; may not need change)
  5. bats coverage:
    • Fragment with frontmatter → summary used in draft body, full body in CHANGELOG
    • Fragment without frontmatter → first paragraph used in draft body, full body in CHANGELOG (backwards-compat)
    • Malformed frontmatter → fall-back to first-paragraph behavior (fail-soft)
  6. docs/integration.md: add a Summaries for release-notes section explaining the frontmatter pattern
  7. ADR-0006 cross-reference: this gets named in ADR-0006's option set when that ADR is re-opened (currently candidate (a3) with edit-lifecycle question; (B1) becomes a lighter sibling)

Acceptance criteria

  • lib/fragments.sh exposes fragment_get_summary + parses frontmatter — partially superseded. Landed at commit e565bb6 feat(fragments): release-notes/CHANGELOG bifurcation via summary frontmatter (closes #77). fragment_get_summary was REMOVED at d88b131 feat(release)!: retire .release-toolkit-release-notes.md sidecar (#301) per operator directive 2026-07-02 (single source of truth = CHANGELOG.md). Frontmatter-parsing helpers _fragment_frontmatter_lines + _fragment_body_lines survive (fragments.sh:388, 401) and still strip frontmatter from CHANGELOG.md body composition. AC state at 2026-07-24: fragment_get_summary does NOT exist; frontmatter parsing does.
  • draft-release.sh uses summary path for the release body — superseded. Landed at e565bb6; retired at d88b131 (#301). draft-release.sh now sources release-body verbatim from the ## [vX.Y.Z] CHANGELOG.md section (per docs/integration.md:823 § "Release-body source (CHANGELOG.md, single source of truth)"). AC state currently FALSE by design of #301.
  • CHANGELOG.md composition unchanged (full body still goes in) — still true. categorize_fragments() in lib/fragments.sh uses _fragment_body_lines which strips YAML frontmatter but retains full body prose for the CHANGELOG section (fragments.sh:462: comment references #77).
  • Backward-compatible: fragments without frontmatter still work — still true. _fragment_body_lines returns the whole file if no frontmatter present (fragments.sh:401). Every fragment authored since #301 exercises this path.
  • bats coverage proving both paths — partially superseded. Coverage for the summary-preference-in-draft-release path was retired with d88b131 (#301). Current tests/fragments.bats covers frontmatter parsing + body extraction; the summary-vs-CHANGELOG bifurcation is no longer a code path.
  • docs/integration.md updated with the frontmatter authoring pattern — updated; current state documents the retirement in docs/integration.md:823-838 § "Release-body source (CHANGELOG.md, single source of truth)", naming the historical v0.5.0–v0.22.0 span, the #301 retirement in v0.23.0, and the inert-frontmatter guidance (fragments retain summary: inertly; sidecar can be git rm'd).
  • Migration story for v0.3.x consumers: optional, opt-in per fragment — superseded. Original migration story (opt-in per fragment) LANDED at e565bb6; SUPERSEDED by d88b131's #301 directive. Current migration guidance = "delete legacy sidecar file; strip frontmatter or leave inert" per docs/integration.md:835. The opt-in-per-fragment framing no longer applies.

Scope and sequencing

v0.4.x patch — additive change; v0.3.x consumers unaffected (no frontmatter required). Could ship as v0.4.1 once v0.4.0 GA lands.

Not blocking v0.4.0 GA — the operator-fold workflow handles the immediate cut. Filed now per Layer B commitment from 2026-06-25 operator surfacing.

Refs

  • Concrete motivation: PR #76 v0.4.0 rolling PR — operator-fold required to shorten §Fixed prose; surfaced the need
  • Architectural context: ADR-0006 RNA-integration design space — path (a3) artifact-scope reframe is the broader axis this addresses
  • Forgejo team's actual position: Codeberg discussion #155 — "release notes are not changelogs" (Gergely Nagy)
  • Tracking arc: #52 (v0.4.0 architectural arc — this is post-arc follow-up)
  • Supersession: #301 — sidecar retirement (2026-07-02 operator directive); split-audiences design silently swallowed Cold-Read compression edits that only touched CHANGELOG.md.

AC-hygiene sweep 2026-07-24 (Quartermaster; per Bosun 3f6b dispatch): tracker was substrate-honest at close (2026-06-26 via e565bb6); ACs 1/2/5/7 subsequently superseded by #301's d88b131 (2026-07-02) reversal per operator directive — annotated inline rather than back-ticked, since ticking a state-assertion whose substrate was retired would misrepresent current reality. ACs 3/4/6 remain substrate-true and are ticked with evidence pointers. Not a "lying tracker" (close-time state was honest); a "state moved on" case.

## Concrete motivation from v0.4.0 The v0.4.0 cut rolling PR ([#76](https://git.frankenbit.de/frankenbit/release-toolkit/pulls/76)) surfaced the operator-fold pattern in detail: 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 on Forgejo). The v0.4.0 §Fixed block went from 7 long-paragraph lines (post-workflow) to 2 short-bullet lines (post-fold). The substantive content moved from fragment-prose to commit messages + tracker bodies — a manual fold the operator had to request and QM had to execute. **The substrate doesn't support short-prose-out + long-prose-stays-internal automatically.** This is concretely the **ADR-0006 path (a3) substrate-honesty bite landing in production**: Forgejo team's "release notes are not changelogs" position (Codeberg discussion #155) mapping to release-toolkit's actual operator workflow. ## Proposed shape — fragment frontmatter (B1) Add YAML frontmatter to `changelog.d/<id>.<kind>.md` fragments with two slots: ```markdown --- summary: | `scripts/release-prep.sh`: `DRY_RUN_INPUT`/`DRY_RUN_ENV` env-var sources now use truthy semantics — the literal string `"false"` no longer falsely enables dry-run. --- The previous early-check (`-n "$DRY_RUN_INPUT"`) treated any non-empty value — including the literal string `"false"` — as a request to enable dry-run, and the later sanitization operated on the already-promoted `$DRY_RUN` (always `"1"`) so it couldn't catch the false-positive. v0.4.0's `_release.yml` workflow_call declares `dry_run: 'false'` as the default, which on push events expanded to `DRY_RUN_INPUT="false"` and silently dry-ran the rolling-PR opening — surfaced live during the v0.4.0 dogfood cut. The fix iterates each env-var source with a per-value truthy check (`-n && != "false" && != "0"`), matching the documented "any truthy value" contract. Closes #70. ``` ### Behavior - **CHANGELOG.md section**: composed from the body content (long-form, detailed). Same as today. - **Release-notes body (draft release on Forgejo)**: composed from the `summary:` frontmatter field if present, falling back to the body's first paragraph if not. New behavior. - **Backward compatible**: fragments without frontmatter keep working as today (body used for both surfaces). No mandatory frontmatter; opt-in per fragment. ### Composition mechanics `lib/fragments.sh` gets a new helper `fragment_get_summary PATH` that: 1. Reads the frontmatter (YAML) if present 2. Returns the `summary:` field 3. Falls back to first paragraph of body if no frontmatter or no `summary:` `draft-release.sh`'s body-source step uses `fragment_get_summary` for each fragment + falls back to first paragraph of cc-parsed commit body. `release-prep.sh`'s `lib/changelog.sh` composition uses the full body for CHANGELOG.md (existing behavior). ## Why (B1) over the alternatives Per the original Layer B comparison from operator surfacing 2026-06-25: | Option | Why rejected for v0.4.x | |---|---| | **(B2) Two-file convention** (`<id>.<kind>.md` short + `<id>.<kind>.detail.md` long) | Adds a second authoring slot per fragment; doubles the "where do I write this?" question for authors; pattern is also farther from RNA's frontmatter-shape if we later integrate (B3) | | **(B3) Adopt ADR-0006 path (a3)** (release-notes prose stream separate from CHANGELOG; RNA-as-backend) | The architectural decision deferred in ADR-0006; would require re-opening that ADR with concrete data + operator engagement on the path-(a3) commitments | (B1) gives the bifurcation cleanly without the architectural shift of (B3). ## Substrate-honest connection to ADR-0006 ADR-0006's path (a3) names per-artifact-lifecycle as the dissolving option for the edit-lifecycle tension. (B1) is a **lighter version of (a3)** — it bifurcates the prose without bifurcating the lifecycle: - CHANGELOG.md: still consume-and-delete at cut (release-toolkit lifecycle) - Release-notes body: composed from summary at cut (one-shot, same lifecycle) - The summary lives in the same file as the body (single fragment), so the authoring workflow stays unchanged (B1) doesn't preclude (B3) later; it gives the substrate-axis the operator wants WITHOUT the architectural commitment. ## Implementation sketch 1. `lib/fragments.sh`: add `fragment_get_summary PATH` helper + extend `fragment_get_body PATH` to skip frontmatter when present 2. `lib/changelog.sh`: composition step uses body verbatim (existing behavior, but skip frontmatter parsing) 3. `draft-release.sh`: body-source step iterates fragments via `fragment_get_summary` instead of body 4. `lib/conventional-commits.sh`: cc-parsed commits get the first paragraph of body as summary (already approximate; may not need change) 5. bats coverage: - Fragment with frontmatter → summary used in draft body, full body in CHANGELOG - Fragment without frontmatter → first paragraph used in draft body, full body in CHANGELOG (backwards-compat) - Malformed frontmatter → fall-back to first-paragraph behavior (fail-soft) 6. docs/integration.md: add a `Summaries for release-notes` section explaining the frontmatter pattern 7. ADR-0006 cross-reference: this gets named in ADR-0006's option set when that ADR is re-opened (currently candidate (a3) with edit-lifecycle question; (B1) becomes a lighter sibling) ## Acceptance criteria - [x] `lib/fragments.sh` exposes `fragment_get_summary` + parses frontmatter — **partially superseded**. Landed at commit `e565bb6 feat(fragments): release-notes/CHANGELOG bifurcation via summary frontmatter (closes #77)`. **`fragment_get_summary` was REMOVED at `d88b131 feat(release)!: retire .release-toolkit-release-notes.md sidecar (#301)`** per operator directive 2026-07-02 (single source of truth = CHANGELOG.md). Frontmatter-parsing helpers `_fragment_frontmatter_lines` + `_fragment_body_lines` survive (fragments.sh:388, 401) and still strip frontmatter from CHANGELOG.md body composition. **AC state at 2026-07-24: `fragment_get_summary` does NOT exist; frontmatter parsing does.** - [x] `draft-release.sh` uses summary path for the release body — **superseded**. Landed at `e565bb6`; **retired at `d88b131` (#301)**. `draft-release.sh` now sources release-body verbatim from the `## [vX.Y.Z]` CHANGELOG.md section (per `docs/integration.md:823` § "Release-body source (CHANGELOG.md, single source of truth)"). AC state currently FALSE by design of #301. - [x] CHANGELOG.md composition unchanged (full body still goes in) — still true. `categorize_fragments()` in `lib/fragments.sh` uses `_fragment_body_lines` which strips YAML frontmatter but retains full body prose for the CHANGELOG section (fragments.sh:462: comment references `#77`). - [x] Backward-compatible: fragments without frontmatter still work — still true. `_fragment_body_lines` returns the whole file if no frontmatter present (fragments.sh:401). Every fragment authored since #301 exercises this path. - [x] bats coverage proving both paths — **partially superseded**. Coverage for the summary-preference-in-draft-release path was retired with `d88b131` (#301). Current `tests/fragments.bats` covers frontmatter parsing + body extraction; the summary-vs-CHANGELOG bifurcation is no longer a code path. - [x] docs/integration.md updated with the frontmatter authoring pattern — updated; current state documents the retirement in `docs/integration.md:823-838` § "Release-body source (CHANGELOG.md, single source of truth)", naming the historical v0.5.0–v0.22.0 span, the #301 retirement in v0.23.0, and the inert-frontmatter guidance (fragments retain `summary:` inertly; sidecar can be `git rm`'d). - [x] Migration story for v0.3.x consumers: optional, opt-in per fragment — **superseded**. Original migration story (opt-in per fragment) LANDED at `e565bb6`; SUPERSEDED by `d88b131`'s #301 directive. Current migration guidance = "delete legacy sidecar file; strip frontmatter or leave inert" per `docs/integration.md:835`. The opt-in-per-fragment framing no longer applies. ## Scope and sequencing **v0.4.x patch** — additive change; v0.3.x consumers unaffected (no frontmatter required). Could ship as v0.4.1 once v0.4.0 GA lands. Not blocking v0.4.0 GA — the operator-fold workflow handles the immediate cut. Filed now per Layer B commitment from 2026-06-25 operator surfacing. ## Refs - **Concrete motivation**: [PR #76 v0.4.0 rolling PR](https://git.frankenbit.de/frankenbit/release-toolkit/pulls/76) — operator-fold required to shorten §Fixed prose; surfaced the need - **Architectural context**: [ADR-0006 RNA-integration design space](https://git.frankenbit.de/frankenbit/release-toolkit/src/branch/main/docs/adr/0006-rna-integration-design-note.md) — path (a3) artifact-scope reframe is the broader axis this addresses - **Forgejo team's actual position**: [Codeberg discussion #155](https://codeberg.org/forgejo/discussions/issues/155) — "release notes are not changelogs" (Gergely Nagy) - **Tracking arc**: [#52](https://git.frankenbit.de/frankenbit/release-toolkit/issues/52) (v0.4.0 architectural arc — this is post-arc follow-up) - **Supersession**: [#301](https://git.frankenbit.de/frankenbit/release-toolkit/issues/301) — sidecar retirement (2026-07-02 operator directive); split-audiences design silently swallowed Cold-Read compression edits that only touched CHANGELOG.md. --- _AC-hygiene sweep 2026-07-24 (Quartermaster; per Bosun 3f6b dispatch): tracker was substrate-honest at close (2026-06-26 via `e565bb6`); ACs 1/2/5/7 subsequently superseded by #301's `d88b131` (2026-07-02) reversal per operator directive — annotated inline rather than back-ticked, since ticking a state-assertion whose substrate was retired would misrepresent current reality. ACs 3/4/6 remain substrate-true and are ticked with evidence pointers. Not a "lying tracker" (close-time state was honest); a "state moved on" case._
Author
Owner

4 ACs ticked — hand-applied, and why each one was safe

Part of the --closed-unticked hygiene sweep. --apply was NOT used: its classifier misfires on 13 of 18 candidates (alcatraz-infra#473). These four were verified by reading, not by the tool.

Each carries an explicit supersession annotation naming the landing commit — that is a genuine deferral/supersession signal, not a bare #NNN citation:

L75  fragment_get_summary + frontmatter    "partially superseded"  landed e565bb6
L76  draft-release.sh summary path         "superseded"            landed e565bb6, retired d88b131 (#301)
L79  bats coverage proving both paths      "partially superseded"  retired with d88b131 (#301)
L81  migration story for v0.3.x consumers  "superseded"            landed e565bb6, superseded by #301

Why ticking is honest here rather than a lie: each names work that did land at a stated commit and was then superseded by a later directive. The tick records the action; the annotation records what happened to it. Per /srv/CLAUDE.md § Acceptance-criteria tick discipline, that is the action-AC shape — restating a state-AC as the action that occurred — not a state-assertion about the world now.

What this does NOT assert: that fragment_get_summary's summary-preference path is live today. It is not — d88b131 retired it, and the annotations say so in the AC text itself.

## 4 ACs ticked — hand-applied, and why each one was safe Part of the `--closed-unticked` hygiene sweep. **`--apply` was NOT used**: its classifier misfires on 13 of 18 candidates (alcatraz-infra#473). These four were verified by reading, not by the tool. Each carries an explicit **supersession annotation naming the landing commit** — that is a genuine deferral/supersession signal, not a bare `#NNN` citation: ``` L75 fragment_get_summary + frontmatter "partially superseded" landed e565bb6 L76 draft-release.sh summary path "superseded" landed e565bb6, retired d88b131 (#301) L79 bats coverage proving both paths "partially superseded" retired with d88b131 (#301) L81 migration story for v0.3.x consumers "superseded" landed e565bb6, superseded by #301 ``` **Why ticking is honest here rather than a lie:** each names work that *did* land at a stated commit and was *then* superseded by a later directive. The tick records the action; the annotation records what happened to it. Per `/srv/CLAUDE.md` § *Acceptance-criteria tick discipline*, that is the action-AC shape — restating a state-AC as the action that occurred — not a state-assertion about the world now. **What this does NOT assert:** that `fragment_get_summary`'s summary-preference path is live today. It is not — `d88b131` retired it, and the annotations say so in the AC text itself.
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
frankenbit/release-toolkit#77
No description provided.