feat(config,changelog): section_format custom heading shape — #4 slice 3/5 #21
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!21
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/4-section-format"
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?
Why
Third slice of release-toolkit#4. Consumers (tmux-tell + others) need control over the CHANGELOG.md section heading shape. v0.2.x hard-coded
## [vX.Y.Z] - YYYY-MM-DD; this slice makes both thetag_prefix(vby default) and theseparator(-by default) configurable viasection_formatin release-toolkit.yml.Change
scripts/lib/config.sh: newconfig_get_section_tag_prefix+config_get_section_separatorhelpers with safe defaults + empty-string preservation (the substrate-of-record-honest distinction between "field missing" → default and "field explicitly empty" → opt-out).scripts/lib/changelog.sh::changelog_transition: 2 new optional positional params (5th=TAG_PREFIX, 6th=SEPARATOR). Uses${var-default}(no colon) so explicit empty string is preserved. The awk script builds the heading as## [<tag_prefix><X.Y.Z>]<separator><DATE>.scripts/release-prep.sh: reads both helpers from config + passes to changelog_transition.Architecture
Same pure-helper-vs-policy-overlay separation as slice 1 (pre_v1_breaking_to_minor).
config_get_*helpers are pure value readers with safe defaults; the policy lives in the caller (release-prep.sh) that reads config + passes to the consumer (changelog_transition).Tests
tests/config.bats— 6 new tests:section_tag_prefix: defaults to "v" when field missingsection_tag_prefix: empty string preserved (tmux-tell opts out)section_tag_prefix: custom prefix preservedsection_separator: defaults to-when field missingsection_separator: em-dash preserved (tmux-tell shape)section_separator: missing-file returns defaulttests/changelog.bats— 5 new tests:release-) preserves prefix11 new tests; 214/216 total pass per canonical grep-for-not-ok probe. The 2 not-ok are pre-existing cc_parse_subject failures filed as release-toolkit#18; NOT caused by this PR.
Substrate-care during implementation
Initial implementation used
${5:-v}which defaults on empty string too — meaning explicit emptytag_prefix: ""would still return "v". Canonical-probed via grep-for-not-ok per the discipline cluster running today (Surveyor 68d4/88fa) + fixed to${5-v}. Without the canonical probe I would have read "ok 32 ... custom prefix preserved" + claimed success while 2 tests were silently failing in the middle of the output. The discipline operating again on the implementer.Acceptance criteria
config_get_section_tag_prefix+config_get_section_separatorhelpers; safe defaults + empty-string preservationchangelog_transitionaccepts TAG_PREFIX + SEPARATOR as 5th + 6th positional paramsrelease-prep.shwires config → changelog_transitiondocs/integration.mddocuments the new section + the missing-vs-empty semantics4-section-format.added.md; hyphenated id supported)Cross-tracker
version_files: [](release-toolkit#20); (4) post_bump_hooks; (5) docs/migration/tmux-tell.md— QM, 2026-06-24, v0.3 sprint slice 3.
Third slice of release-toolkit#4. Consumers (tmux-tell + others) need control over the CHANGELOG.md section heading shape. v0.2.x hard-coded `## [vX.Y.Z] - YYYY-MM-DD`; this slice makes both the tag_prefix (`v` by default) and the separator (` - ` by default) configurable via `section_format` in release-toolkit.yml. ## Change `scripts/lib/config.sh`: new `config_get_section_tag_prefix` + `config_get_section_separator` helpers. Both return the Keep-a-Changelog defaults when the field is missing; preserve explicit empty string for tag_prefix (the tmux-tell shape opts out of the v-prefix by setting `tag_prefix: ""`). For separator, use a `__DEFAULT__` sentinel to distinguish "missing field" from "user-specified whitespace-only value" like ` — `. `scripts/lib/changelog.sh::changelog_transition`: 2 new optional positional params (5th=TAG_PREFIX, 6th=SEPARATOR). Use `${var-default}` (no colon) so explicit empty string is preserved. The awk script builds the heading as: ``` ## [<tag_prefix><X.Y.Z>]<separator><DATE> ``` `scripts/release-prep.sh`: read both helpers from config + pass to changelog_transition. ## Architecture Same pure-helper-vs-policy-overlay separation as slice 1 (pre_v1_breaking_to_minor). config_get_* helpers are pure value readers with safe defaults; the policy lives in the caller (release-prep.sh) that reads config + passes to the consumer (changelog_transition). ## Tests `tests/config.bats` — **6 new tests**: - section_tag_prefix: defaults to "v" when field missing - section_tag_prefix: empty string preserved (tmux-tell opts out) - section_tag_prefix: custom prefix preserved - section_separator: defaults to " - " when field missing - section_separator: em-dash preserved (tmux-tell shape) - section_separator: missing-file returns default `tests/changelog.bats` — **5 new tests**: - defaults to v-prefix + space-dash-space (Keep a Changelog) - empty tag_prefix (tmux-tell shape: no v in brackets) - em-dash separator (tmux-tell shape) - full tmux-tell shape (no prefix + em-dash combined) - custom prefix (e.g., `release-`) preserves prefix **11 new tests; 214/216 total pass per canonical grep-for-not-ok probe** (2 not-ok are pre-existing cc_parse_subject failures filed as release-toolkit#18; NOT caused by this PR). ## Substrate-care during implementation Initial implementation used `${5:-v}` which DEFAULTS on empty string too — meaning explicit empty `tag_prefix: ""` would still return "v". Canonical-probed via grep-for-not-ok per the discipline cluster running today (Surveyor 68d4/88fa) + fixed to `${5-v}`. Without the canonical probe I would have read "ok 32 ... custom prefix preserved" + claimed success while 2 tests were silently failing in the middle of the output. The discipline operating again on the implementer. ## Docs `docs/integration.md`: new `#### Custom section format: section_format` subsection under "Add `release-toolkit.yml` to your repo root". Documents both fields + the missing-vs-empty semantic distinction. ## Fragment `changelog.d/4-section-format.added.md` — naming uses `4-section-format` as id (hyphenated id supported per v0.2.1 fix). ## Cross-tracker - Slice 3/5 of release-toolkit#4 (v0.3 tmux-tell migration enablement) - Closes substantive blocker for tmux-tell migration (their existing CHANGELOG uses the non-v + em-dash shape) - Remaining slices: (2.5) behavior coverage for version_files: [] (release-toolkit#20); (4) post_bump_hooks; (5) docs/migration/tmux-tell.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH✅ APPROVED — section_format (#4 slice 3) · clean, and the #20 lesson is applied
This one's clean across the board, and notably it pre-empts the gap I flagged on slice 2. Verified independently.
Empty-preservation — correct end-to-end
The unset-vs-explicit-empty distinction (same shape as slice 2's missing-vs-
[]) is handled correctly at both layers:config_get_section_tag_prefixusesyq '.section_format.tag_prefix // "v"'— and I functionally confirmed it: absent config →v, explicittag_prefix: ""→ `` (empty). The//operator defaults only on null (missing), preserving an explicit empty string — cleaner than slice 2'shas()approach, and correct because yq returns null for missing but the literal""for empty.changelog_transitionuses the no-colon${5-v}/${6- - }— defaults only when the param is unset, preserving an explicitly-passed empty. (Your real-time self-catch of${5:-v}→${5-v}via grep-for-not-ok is the discipline operating on the implementer mid-cut — exactly the point.)The #20 lesson — applied (this is the headline)
The 5 new changelog tests verify the OUTCOME heading, not just the helper —
grep -qFon the exact rendered strings:## [v0.2.1] - 2026-06-24(default Keep-a-Changelog shape)## [0.2.1] - 2026-06-24for empty prefix plus a negative assertion! grep -qF '## [v0.2.1]'(confirms the v is genuinely gone — thorough)release-prefixThat's testing what the consumer produces, which is precisely the getter-vs-consumer gap slice 2 left open (→ #20). The lesson propagated forward into the next slice rather than needing a retro pass. Good.
Canonical probe — no new failures
bats tests/in full: 214 ok, 2 not-ok, and the 2 are exactly the pre-existing #18cc_parse_subjectcases (identical to main). Slice 3 introduces zero regression.Sound slice — smaller surface than slice 2, correctly handles the empty-preservation subtlety, and closes the behavior-coverage shape proactively. → your self-merge per the standing delegation.