feat(prep): a release without fragments gets its notes from feat/fix commit subjects #1529

Closed
opened 2026-10-03 01:07:57 +02:00 by claude · 1 comment
Member

Outcome (2026-10-04): landed via #1531 as e5fcf1d. The operator's review redesigned it (PR comments 114541 and 114651): there is no release_notes key. A release with no fragment and no authored [Unreleased] text gets its notes from its feat/fix subjects, plus breaking changes under Upgrade. Any other release works exactly as before. Mixed mode and its suppression rules were dropped. ADR-0012 is accepted. The text below is the issue as filed; each acceptance criterion says how it was met or what replaced it.

An adopter whose commit subjects are Conventional Commits cannot get release notes from them. Since #717 (ADR-0011), rt prep builds the version section only from changelog.d/ fragments and ### Kind blocks under ## [Unreleased], and a commit subject only sets the bump. No setting changes that, and an unknown key in release-toolkit.yml is refused.

frankenbit/storey wants exactly this from its next release on:

  • Its subjects are now type(area): the finding (#issue) (storey 109d985).
  • Its pending v0.2.0 release is built from five fragments (storey PR 20, "chore(release): v0.2.0").
  • After that cut it would rather write no fragments at all.

Measured at 8086700:

  • internal/prep/prep.go:216-217: renderCommits is a stub that returns nil, nil. The overview comment at :22 reads ccMd = nil // #717: CC is bump-only.
  • internal/config/config.go:150: KnownFields(true). With a v0.63.0 build on a Storey clone, a release_notes: key makes rt prep and manifest-check fail, while rt decide falls back to defaults and exits 0.
  • internal/prep/prep.go:186-199: recovery subtracts FragmentSections from a stranded section. Its comment says that if renderCommits ever returns notes, FragmentSections must render them too, or recovery folds them back and the re-prepare renders them again (#1490, review 7155).
  • cmd/rt/recover_pending_cut.go:400: recovery refuses a prepare that deleted no visible changelog.d/*.md. A cut whose notes came only from commits could therefore never be recovered.
  • docs/conventions.md:34-35: feat: and fix: are documented as "bump only".

A prototype already exists: +846/−72, written on dd871e8, and it applies cleanly to 8086700. It adds the key, restores the pre-#717 renderer from 2f78688^ behind it, and makes recovery subtract the stranded prepare's commit notes.

  • Without the key, its Storey output is byte-identical to main.
  • With the key, compose-verify passes on the rendered section.
  • All 12 mutants were caught, natively on Windows and in golang:1.25.

Why it matters

  • #717 stays in force for every adopter that does not opt in. This adds the explicit opt-in that ADR-0011 item 4 left open, for adopters whose subjects are written to be read.
  • Without it, an adopter must hand-write a fragment for every change even when the subject already says it, or ship "None." sections.

Decisions taken (for review; recorded as a decision tree in ADR-0012)

  • Key. release_notes: {source: fragments|commits}; leaving it out means fragments. commits still renders fragments and [Unreleased] prose, and a numeric fragment <N>.<kind>.md replaces every commit bullet that cites #N.
  • Rendered types in v1. feat goes to Added and fix to Fixed. perf and refactor stay bump-only, because rendering them would also need rt decide's release-relevance predicate to change, or a note could be lost in the cut race.
  • Declining a note. Put Release-Notes: skip in the commit's own body. For a subject that already landed, put Release-Notes: skip <sha> in a later commit's body. Neither needs a history rewrite.

Acceptance criteria

  • With no release_notes key, rt prep output is byte-identical for every adopter and for toolkit-self. The four ADR-0011 tests stay green, and a mutation-verified guard proves the default path makes no commit walk.

    — DONE @ #1531 (e5fcf1d, reviewed as e5fcf1d), restated for the per-release rule. There is no key. A release with a fragment or authored [Unreleased] text renders byte-identically to main. That was measured on Storey's pending v0.2.0, and on toolkit-self's first run of the new code: rolling PR #1523 (v0.65.0) renders exactly its five fragments. The four ADR-0011 tests stay green, adapted to the rule. TestCompose_noCommitWalkUnlessTheCutHasNoOtherSource and TestComposeFragmentsAndBumpOnlyCommits fail when such a release calls git, and mutants that walk anyway or ignore the fragments were killed.

  • With release_notes.source: commits, rt prep renders the feat/fix subjects since the last tag as - **scope**: description bullets under Added/Fixed. They are scrubbed by register.ScrubLine, and there is no blank-line split between fragment bullets and commit bullets.

    — DONE @ #1531 (e5fcf1d, reviewed as e5fcf1d), without the key: every release with no other note source renders this way, feat under Added and fix under Fixed, each bullet through register.ScrubLine. Breaking changes of any type also render, under Upgrade. The blank-line criterion no longer applies: with mixed mode dropped, fragment bullets and commit bullets never share a release.

  • These render no bullet:

    • a commit that adds a visible changelog.d/*.md;
    • a commit whose body names a fragment id;
    • a commit carrying Release-Notes: skip, or named by a later Release-Notes: skip <sha>.

    A numeric fragment suppresses the bullets that cite its number.

    — DONE in part @ #1531 (e5fcf1d, reviewed as e5fcf1d); the rest was superseded. Both Release-Notes: skip forms are implemented (TestCompose_commitNotesOwnSkipTrailer, TestCompose_commitNotesSkipNamedSHA). The three fragment-driven rules went with mixed mode: any fragment now makes the whole release a fragment release, so no commit bullet renders beside one. An empty fragment declines the notes of a whole release.

  • When opted in, rt recover-pending-cut neither duplicates commit notes nor refuses a prepare whose notes came only from commits. When the config cannot be loaded and may carry the key, it refuses instead of falling back.

    — DONE @ #1531 (e5fcf1d, reviewed as e5fcf1d). Recovery detects which source the stranded release used. It takes commit notes out instead of folding them back, and it accepts a prepare that consumed no fragment (TestRecoverPendingCutModeDetection, TestRecoverPendingCutCommitModePrepare). It refuses when release-toolkit.yml does not load (TestRealRecoverDepsCommitNotesNeedALoadedConfig). With no key, there is nothing more to guard.

  • Rendering refuses when the commit range cannot be anchored (no last tag, but the CHANGELOG has a prior cut). The last-tag match follows tag_format instead of a hard-coded v*.

    — DONE @ #1531 (e5fcf1d, reviewed as e5fcf1d), and extended. The window starts at the tag of the CHANGELOG's newest cut, matched through tag_format, and rendering refuses when that tag cannot be found. The operator accepted two more cases with ADR-0012:

    • a version that was prepared but never tagged anchors at its prepare commit, when the checkout has all its tags;
    • a first release walks the whole history, and warns if matching tags exist.
  • Documentation:

    • ADR-0012 amends ADR-0011 items 3 and 4 for opted-in adopters;
    • the C1 schema, C6, conventions.md, integration.md and fragment-style.md are updated, and so are the arc42 statements;
    • integration.md states that the key must never land before the pin that knows it.

    — DONE @ #1531 (e5fcf1d, reviewed as e5fcf1d), restated. ADR-0012 is accepted (2026-10-03) and supersedes ADR-0011 in part, for a release with no fragment and no [Unreleased] text. Its details are in docs/adr/0012-commit-subject-release-notes-details.md. C6, conventions.md, integration.md, fragment-style.md, the README and the arc42 statements are updated. The C1 schema is unchanged against main. The pin-before-key warning no longer applies: integration.md says adopters need only the pin bump.

  • The native Windows and golang:1.25 suites pass, and the PR shows a dry run on a Storey-shaped repository.

    — DONE @ #1531 (e5fcf1d, reviewed as e5fcf1d). All 30 packages pass natively on Windows and on Linux. The earlier rounds ran in golang:1.25. The last code round (b20b59e) ran in CI's own image (forgejo-ci-go:latest), where golangci-lint reports 0 issues. The PR description reports the Storey dry runs. On the landed commit, all 46 checks are green.

  • A changelog.d fragment.

    — DONE @ #1531 (e5fcf1d, reviewed as e5fcf1d): changelog.d/1529-commit-notes.changed.md, already consumed into v0.65.0's ### Changed by rolling PR #1523.

Out of scope

  • Rendering perf/refactor, a breaking-change marker on feat!, and PR-time subject grading.
    • Update: breaking changes now render under Upgrade (ADR-0012). perf/refactor and subject grading stay out of scope.
  • Storey's own adoption: one PR with the pin bump plus the key, after a toolkit tag ships this.
    • Update: no key is needed. Storey's adoption is the pin bump alone, after a toolkit tag ships this.

Anchor

The release-notes plan for frankenbit/storey, 2026-10-01 to 2026-10-03. Adoption tracker: #1478.

> **Outcome (2026-10-04):** landed via #1531 as `e5fcf1d`. The operator's review redesigned it (PR comments 114541 and 114651): there is no `release_notes` key. A release with no fragment and no authored `[Unreleased]` text gets its notes from its `feat`/`fix` subjects, plus breaking changes under Upgrade. Any other release works exactly as before. Mixed mode and its suppression rules were dropped. ADR-0012 is accepted. The text below is the issue as filed; each acceptance criterion says how it was met or what replaced it. An adopter whose commit subjects are Conventional Commits cannot get release notes from them. Since #717 (ADR-0011), `rt prep` builds the version section only from `changelog.d/` fragments and `### Kind` blocks under `## [Unreleased]`, and a commit subject only sets the bump. No setting changes that, and an unknown key in `release-toolkit.yml` is refused. frankenbit/storey wants exactly this from its next release on: - Its subjects are now `type(area): the finding (#issue)` (storey `109d985`). - Its pending v0.2.0 release is built from five fragments (storey PR 20, "chore(release): v0.2.0"). - After that cut it would rather write no fragments at all. Measured at `8086700`: - `internal/prep/prep.go:216-217`: `renderCommits` is a stub that returns `nil, nil`. The overview comment at `:22` reads `ccMd = nil // #717: CC is bump-only`. - `internal/config/config.go:150`: `KnownFields(true)`. With a v0.63.0 build on a Storey clone, a `release_notes:` key makes `rt prep` and `manifest-check` fail, while `rt decide` falls back to defaults and exits 0. - `internal/prep/prep.go:186-199`: recovery subtracts `FragmentSections` from a stranded section. Its comment says that if `renderCommits` ever returns notes, `FragmentSections` must render them too, or recovery folds them back and the re-prepare renders them again (#1490, review 7155). - `cmd/rt/recover_pending_cut.go:400`: recovery refuses a prepare that deleted no visible `changelog.d/*.md`. A cut whose notes came only from commits could therefore never be recovered. - `docs/conventions.md:34-35`: `feat:` and `fix:` are documented as "bump only". A prototype already exists: +846/−72, written on `dd871e8`, and it applies cleanly to `8086700`. It adds the key, restores the pre-#717 renderer from `2f78688^` behind it, and makes recovery subtract the stranded prepare's commit notes. - Without the key, its Storey output is byte-identical to `main`. - With the key, `compose-verify` passes on the rendered section. - All 12 mutants were caught, natively on Windows and in `golang:1.25`. ## Why it matters - #717 stays in force for every adopter that does not opt in. This adds the explicit opt-in that ADR-0011 item 4 left open, for adopters whose subjects are written to be read. - Without it, an adopter must hand-write a fragment for every change even when the subject already says it, or ship "None." sections. ## Decisions taken (for review; recorded as a decision tree in ADR-0012) - **Key.** `release_notes: {source: fragments|commits}`; leaving it out means `fragments`. `commits` still renders fragments and `[Unreleased]` prose, and a numeric fragment `<N>.<kind>.md` replaces every commit bullet that cites `#N`. - **Rendered types in v1.** `feat` goes to Added and `fix` to Fixed. `perf` and `refactor` stay bump-only, because rendering them would also need `rt decide`'s release-relevance predicate to change, or a note could be lost in the cut race. - **Declining a note.** Put `Release-Notes: skip` in the commit's own body. For a subject that already landed, put `Release-Notes: skip <sha>` in a later commit's body. Neither needs a history rewrite. ## Acceptance criteria - [x] With no `release_notes` key, `rt prep` output is byte-identical for every adopter and for toolkit-self. The four ADR-0011 tests stay green, and a mutation-verified guard proves the default path makes no commit walk. — **DONE @ #1531 (`e5fcf1d`, reviewed as `e5fcf1d`)**, restated for the per-release rule. There is no key. A release with a fragment or authored `[Unreleased]` text renders byte-identically to `main`. That was measured on Storey's pending v0.2.0, and on toolkit-self's first run of the new code: rolling PR #1523 (v0.65.0) renders exactly its five fragments. The four ADR-0011 tests stay green, adapted to the rule. `TestCompose_noCommitWalkUnlessTheCutHasNoOtherSource` and `TestComposeFragmentsAndBumpOnlyCommits` fail when such a release calls git, and mutants that walk anyway or ignore the fragments were killed. - [x] With `release_notes.source: commits`, `rt prep` renders the `feat`/`fix` subjects since the last tag as `- **scope**: description` bullets under Added/Fixed. They are scrubbed by `register.ScrubLine`, and there is no blank-line split between fragment bullets and commit bullets. — **DONE @ #1531 (`e5fcf1d`, reviewed as `e5fcf1d`)**, without the key: every release with no other note source renders this way, `feat` under Added and `fix` under Fixed, each bullet through `register.ScrubLine`. Breaking changes of any type also render, under Upgrade. The blank-line criterion no longer applies: with mixed mode dropped, fragment bullets and commit bullets never share a release. - [x] These render no bullet: - a commit that adds a visible `changelog.d/*.md`; - a commit whose body names a fragment id; - a commit carrying `Release-Notes: skip`, or named by a later `Release-Notes: skip <sha>`. A numeric fragment suppresses the bullets that cite its number. — **DONE in part @ #1531 (`e5fcf1d`, reviewed as `e5fcf1d`); the rest was superseded.** Both `Release-Notes: skip` forms are implemented (`TestCompose_commitNotesOwnSkipTrailer`, `TestCompose_commitNotesSkipNamedSHA`). The three fragment-driven rules went with mixed mode: any fragment now makes the whole release a fragment release, so no commit bullet renders beside one. An empty fragment declines the notes of a whole release. - [x] When opted in, `rt recover-pending-cut` neither duplicates commit notes nor refuses a prepare whose notes came only from commits. When the config cannot be loaded and may carry the key, it refuses instead of falling back. — **DONE @ #1531 (`e5fcf1d`, reviewed as `e5fcf1d`)**. Recovery detects which source the stranded release used. It takes commit notes out instead of folding them back, and it accepts a prepare that consumed no fragment (`TestRecoverPendingCutModeDetection`, `TestRecoverPendingCutCommitModePrepare`). It refuses when `release-toolkit.yml` does not load (`TestRealRecoverDepsCommitNotesNeedALoadedConfig`). With no key, there is nothing more to guard. - [x] Rendering refuses when the commit range cannot be anchored (no last tag, but the CHANGELOG has a prior cut). The last-tag match follows `tag_format` instead of a hard-coded `v*`. — **DONE @ #1531 (`e5fcf1d`, reviewed as `e5fcf1d`)**, and extended. The window starts at the tag of the CHANGELOG's newest cut, matched through `tag_format`, and rendering refuses when that tag cannot be found. The operator accepted two more cases with ADR-0012: - a version that was prepared but never tagged anchors at its prepare commit, when the checkout has all its tags; - a first release walks the whole history, and warns if matching tags exist. - [x] Documentation: - ADR-0012 amends ADR-0011 items 3 and 4 for opted-in adopters; - the C1 schema, C6, conventions.md, integration.md and fragment-style.md are updated, and so are the arc42 statements; - integration.md states that the key must never land before the pin that knows it. — **DONE @ #1531 (`e5fcf1d`, reviewed as `e5fcf1d`)**, restated. ADR-0012 is accepted (2026-10-03) and supersedes ADR-0011 in part, for a release with no fragment and no `[Unreleased]` text. Its details are in `docs/adr/0012-commit-subject-release-notes-details.md`. C6, conventions.md, integration.md, fragment-style.md, the README and the arc42 statements are updated. The C1 schema is unchanged against `main`. The pin-before-key warning no longer applies: integration.md says adopters need only the pin bump. - [x] The native Windows and `golang:1.25` suites pass, and the PR shows a dry run on a Storey-shaped repository. — **DONE @ #1531 (`e5fcf1d`, reviewed as `e5fcf1d`)**. All 30 packages pass natively on Windows and on Linux. The earlier rounds ran in `golang:1.25`. The last code round (`b20b59e`) ran in CI's own image (`forgejo-ci-go:latest`), where golangci-lint reports 0 issues. The PR description reports the Storey dry runs. On the landed commit, all 46 checks are green. - [x] A `changelog.d` fragment. — **DONE @ #1531 (`e5fcf1d`, reviewed as `e5fcf1d`)**: `changelog.d/1529-commit-notes.changed.md`, already consumed into v0.65.0's `### Changed` by rolling PR #1523. ## Out of scope - Rendering `perf`/`refactor`, a breaking-change marker on `feat!`, and PR-time subject grading. - *Update:* breaking changes now render under Upgrade (ADR-0012). `perf`/`refactor` and subject grading stay out of scope. - Storey's own adoption: one PR with the pin bump plus the key, after a toolkit tag ships this. - *Update:* no key is needed. Storey's adoption is the pin bump alone, after a toolkit tag ships this. ## Anchor The release-notes plan for frankenbit/storey, 2026-10-01 to 2026-10-03. Adoption tracker: #1478.
claude changed title from feat(prep): render Conventional Commit subjects as release notes for adopters who opt in (release_notes.source: commits) to feat(prep): a release without fragments gets its notes from feat/fix commit subjects 2026-10-04 00:18:46 +02:00
Author
Member

Closing by hand: every acceptance criterion is ticked in the description.

  • Landed: e5fcf1dd8c5afc80ea1f7601a64f34044750da1d on main, via #1531. It is a fast-forward rebase, and all 18 commits are identical to the reviewed head (git range-diff: 18 of 18 =). The landing-identity record (comment 114675) says the same.
  • Approval: alex, review 7211, at e5fcf1d.
  • Checks: all 46 statuses on e5fcf1d are green, the push run included.
  • Head branch: claude/1529-commit-notes was deleted on merge.
  • First run on toolkit-self: the push run rebuilt rolling PR #1523 (v0.65.0) with the new code. It renders its five fragments and no commit subject, as the per-release rule says for a release that has fragments.

The redesign changed several criteria during review: there is no key, mixed mode was dropped, and breaking changes render under Upgrade. The description has an outcome note at the top, and each criterion says what was done.

One correction to #1531's description: it says the companion document lists the mutants, and it does not. The mutation runs were done in the implementing session and are not recorded in the repository.

Closing by hand: every acceptance criterion is ticked in the description. - **Landed:** `e5fcf1dd8c5afc80ea1f7601a64f34044750da1d` on `main`, via #1531. It is a fast-forward rebase, and all 18 commits are identical to the reviewed head (`git range-diff`: 18 of 18 `=`). The landing-identity record (comment 114675) says the same. - **Approval:** alex, review 7211, at `e5fcf1d`. - **Checks:** all 46 statuses on `e5fcf1d` are green, the push run included. - **Head branch:** `claude/1529-commit-notes` was deleted on merge. - **First run on toolkit-self:** the push run rebuilt rolling PR #1523 (v0.65.0) with the new code. It renders its five fragments and no commit subject, as the per-release rule says for a release that has fragments. The redesign changed several criteria during review: there is no key, mixed mode was dropped, and breaking changes render under Upgrade. The description has an outcome note at the top, and each criterion says what was done. **One correction to #1531's description:** it says the companion document lists the mutants, and it does not. The mutation runs were done in the implementing session and are not recorded in the repository.
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#1529
No description provided.