changelog: seed ## Unreleased with all three section headings, and let the -dev bump write them
#126
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:apply
scope:capture
scope:coolify-api
scope:fleet
scope:manifest
scope:secrets
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/cast#126
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
## Unreleasedshould always carry### Added,### Changedand### Fixed, even when empty — and the-devbump should be what seeds them.The problem
Every open PR writes its entry under
## Unreleased, and the section starts out with no subsection headings. So the first PR to need### Fixedcreates it, and any other PR that also needs### Fixedcreates it too — at the same position, in the same otherwise-empty region. Git sees two sides both inserting at one anchor and calls it a conflict.That makes the conflict rate a function of how many PRs are open, not of whether they have anything to do with each other. Two changes that touch entirely different files, in different subsystems, still collide in
CHANGELOG.md.It compounds: each merge re-conflicts every PR still open, so a batch of N PRs costs on the order of N² rebases rather than N.
Evidence from a real batch
A recent batch in heavy-duty/rig merged seven entries — 1
Added, 3Changed, 3Fixed— across eight PRs. Nearly every rebase in it hitCHANGELOG.md, and not one of those conflicts was a genuine disagreement about content. Two shapes showed up:### Changed, the other### Fixed. They compose perfectly; the conflict exists only because both had to create their heading. This proposal eliminates these entirely.### Fixedbullets landing at the same anchor. This proposal does not fix these — see Limits.The worst single case was shape 1 with an ordering twist: a PR adding
### Addedconflicted against### Fixed, and resolving it correctly meant hoisting the new section to the top, because this file ordersAdded → Changed → Fixed. Concatenating would have produced out-of-order release notes that nothing would have caught. With the headings pre-seeded, that judgment call does not exist — the entry goes under a heading that is already in the right place.Proposal
1.
## Unreleasedalways carries all three headings, even when empty:A PR then only ever appends a bullet under an existing heading. It never creates a heading, never chooses where a section goes, and never reorders anything.
2. The
-devbump commit is what seeds them. The release flow already re-armsVERSIONtoX.Y.(Z+1)-devautomatically after a release; that same commit should write the fresh## Unreleasedwith the three empty headings.This is the part that makes the whole thing safe. Release notes are extracted verbatim between
## <ver>and the next##(changelog_sectionin.github/scripts/release-lib.sh), so if empty headings lived in a stamped section they would ship into the published release body. Seeding them on the-devside means the template only ever exists in## Unreleased, and a stamped section only ever contains real entries.It also moves the changelog re-arm from a manual ceremony step into the automation that already re-arms
VERSION— which is a bonus, not a side effect: the re-arm exists because a forgotten manual one fails silently and misattributes entries to a shipped release.The guard this breaks, and how
The release flow asserts that the section it is about to publish is non-empty. With seeded headings, a section containing only three empty headings is textually non-empty — so a genuinely empty release would sail past that check.
The guard must test for at least one real entry (a
-bullet), not for bytes. Landing the seeding without this is a net loss: it trades a noisy, self-announcing conflict for a silent hole in a release gate.Limits — stated honestly
### Fixedstill insert at the same anchor. This converts "conflicts between nearly every pair" into "conflicts only within a section", and leaves only the trivial resolution shape (keep both bullets, no ordering decision).changelog.d/directory, one file per PR, assembled at release time), which makes conflicts structurally impossible because no two PRs touch the same file. That is a bigger change and deserves its own issue if the cheap fix proves insufficient. This one is a few lines and no new concepts.Acceptance
## Unreleasedcarries all three headings onmain-devbump commit seeds them, so a stamped section never contains an empty headingCONTRIBUTING.mdsays "append under the right heading" rather than "add an entry under## Unreleased"Filed in all three repos
box, rig and cast share this release flow and this failure. Whichever lands first should be the reference the other two follow, so the three do not drift.
Siblings: heavy-duty/rig#86, heavy-duty/box#135.
Closing — the mechanism this issue patches moved out of this repo, and the proposal already landed upstream.
Since #143, the
-devre-arm and the release-notes guard are heavy-duty/ceremony's shared machinery, consumed by pin (release.yml→ceremony@0.1.0). The scripts this issue names (release-lib.sh,changelog_section) no longer exist in cast to patch.Upstream, ceremony 0.2.0 implements exactly this proposal — "count entries instead of bytes, refuse dangling grouped headings, and seed grouped re-arms with Added/Changed/Fixed" (heavy-duty/ceremony#98) — and the same release ships the complete fix this issue's Limits section names: per-PR
changelog.d/fragments (heavy-duty/ceremony#112, #114, #115), which make heading conflicts unrepresentable rather than merely rarer.Cast receives all of it the day its ceremony pin advances past 0.1.0. That advance is a real decision — it swaps the contributor workflow from "append under
## Unreleased" to fragment files — and it belongs to the maintainer (@danmt), not to this spec. If changelog conflicts bite here before the pin moves, the right move is a fresh triage item for the pin advance, with this issue as prior art.Sibling: heavy-duty/box#135 remains open on box's board and is box's call.