Skip to content

Changelog fragments: stop concurrent PRs conflicting on CHANGELOG.md #3563

Closed

Description

@clabonte

Problem

Every writer of CHANGELOG.md appends at one insertion point (## [Unreleased] / ### Fixed), so any two open PRs conflict as soon as one merges. In clabonte/generator, 385 of the 570 commits on main since 2026-10-01 touch the file.

A merge=union attribute makes local rebases clean, but GitHub's merge button ignores merge drivers. The PR still shows as conflicting, and a conflicted PR gets no CI run. Rebasing each item onto current main (FEAT-2026-0200) only helps the agent at the moment it opens the PR: two PRs open at once still conflict when either merges.

Why upstream

An adopter can't move fully to fragments locally, because the writers and the enforcement live here:

  • /fix-bug's changelog step
  • close-discipline.md §3
  • changelog.py's append_entry and stamp_release
  • assert_changelog_entry_for_contract_changes (close-k) and specfuse lint --closing, which require a feature close's entry to be in CHANGELOG.md

This was first raised as a comment on #3451, a closed roadmap PR, so it has no tracking item of its own. This issue is that item.

Proposal

  • One entry per file under changelog.d/, carrying its class (added / changed / fixed / breaking) and its trace (FEAT-… or #N). Naming the file after the trace means PRs for different issues can't collide.
  • /fix-bug and the close step write a fragment.
  • close-k accepts an entry in either a fragment or CHANGELOG.md.
  • The release step collates fragments into the version section through the existing write path, deletes them in the same commit, and fails if parse_changelog reports a finding on the result.
  • Opt-in per project, so an adopter that keeps a single file sees no change.

Adopter status

clabonte/generator is building an interim hybrid in clabonte/generator#3283: bug PRs write fragments through a local override, feature closes keep appending to CHANGELOG.md to satisfy close-k, and its own release script collates. Once this lands, the local override can go. Design detail and measurements: clabonte/generator#1973, #1958.

Done when

  • Two open PRs that each add a changelog entry don't conflict when one merges, whether they come from /fix-bug or a feature close.
  • A feature close whose entry exists only as a fragment passes close-k and specfuse lint --closing.
  • A released section contains every fragment, parses cleanly, and leaves no consumed fragment behind.

🤖 Generated with Claude Code

Activity

  1. clabonte commented on Nov 3, 2026

    @clabonte
    ContributorAuthor

    Implemented in #3566 as an opt-in changelog.mode: fragments in .specfuse/layout.yml; projects on the default single mode keep the same read and write path.

    • Writer: python3 -m specfuse.loop.changelog_fragments add, with collate and check alongside it.
    • close-k, specfuse lint --closing and the terminal auto-close guard share one reader, so an entry counts in a fragment or in CHANGELOG.md.
    • scripts/bump_version.py folds fragments before stamping and deletes them. A custom release script must call collate before it stamps; docs/lifecycles.md says so, but no test covers an adopter's script.

    Not yet proven: GitHub's merge button itself. The oracle uses real git merge, so opening two real PRs that each add a fragment is the first post-merge check.

  2. clabonte commented on Nov 4, 2026

    @clabonte
    ContributorAuthor

    Post-merge check passed, on released 0.32.0 code. Two PRs that each add a fragment do not conflict on GitHub; direct CHANGELOG.md edits still do.

    Step Second PR's state afterward
    Fragment PR #3572 merged #3573 stayed MERGEABLE
    Fragment PR #3573 merged Merged cleanly, both fragments present on base
    Control PR #3574 (direct CHANGELOG.md edit) merged #3575 went CONFLICTING / DIRTY, and its merge was refused

    Limits: merges used gh pr merge --squash, not a click on the button, against a throwaway base rather than main, so branch protection on main was not exercised. The base branch is deleted.

    Decision: FEAT-LOOP-2026-0200's optional append-only re-apply is skipped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions