Skip to content

Changelog fragments in ./new_changelogs/, collected daily by a [skip actions] job — kill the release-fold at the root #2002

Description

@paul-hammant

Supersedes the detect-the-fold approach in #2001. That issue proposed catching the release-fold after it happens; this one removes the fold's cause entirely.

The root cause (recap)

Every open PR edits the same shared lines — ## [current] in CHANGELOG.md. When a release is cut it renames those lines ([current][0.664.0]), so merging the new main back into any open PR silently folds that PR's entry INTO the released section (no conflict marker). It has bitten four PRs in a row (0.661→0.664 cut against open branches). The real problem is many writers, one shared file, one shared heading.

The design: one fragment file per change

Adopt the changelog.d / newsfragments pattern (towncrier, Cargo, GitLab):

  • A PR does not edit CHANGELOG.md. It drops a file in ./new_changelogs/, one per change, containing its bullet(s).
  • Because each PR writes its own file, PRs never touch shared lines → no conflicts, and a release cannot fold an entry that isn't in CHANGELOG.md yet.
  • A daily [skip actions] job collects settled fragments into CHANGELOG.md under ## [current], git rms them, and commits.

This deletes the whole failure class. The CI "CHANGELOG entry present" gate flips from "requires a ## [current] edit" to "requires a new_changelogs/ file (or [skip changelog])", and the fold-check gate (and #2001) can be removed.

The timestamp: filename = UTC time of the last push to the PR

Rather than derive age from git history or file mtime (mtime is rewritten by any checkout/clone, so it is useless here), encode the time in the filename — the UTC timestamp of the last push to the PR that introduced the fragment, e.g.:

new_changelogs/20260910T175500Z-1988-optional-library-linking.md

The daily collector folds a fragment only when its filename timestamp is older than a cooling-off window (≈4h). The window's purpose is to let a just-landed change settle (a revert or follow-up fix) before it becomes a permanent changelog line.

Semantics to ratify (the "last push" wrinkle)

Using last push (not first-authored) time means a branch that is force-pushed resets its fragment's clock — the change is treated as "not settled until pushes stop." That is arguably the intended behaviour (an actively-churning PR should not fold yet), but it should be a deliberate choice:

  • Chosen (proposed): last-push time → cooling-off restarts on every push. An abandoned/settled PR folds ~4h after its final push.
  • Alternative: first-landed-on-main time → folds ~4h after merge regardless of later branch activity.

Whichever is chosen, the timestamp must be written by the author/PR tooling into the filename, not inferred at collection time, so the collector needs no git archaeology.

Release interaction (also to ratify)

When a release is cut while fragments are still inside the cooling-off window:

  • Proposed: the release job collects all pending fragments regardless of age (cutting a release is a deliberate "ship what's here"); the ≈4h bar applies only to the daily job. This guarantees no merged change is ever absent from its release.
  • Alternative: the release respects the ≈4h bar too — safer against recording an unsettled note, but a change merged minutes before a release would miss it and need a catch-up next cut.

Fragment format (to ratify)

Section (### Added / ### Fixed / ### Changed / ### Performance) needs to be declarable per fragment. Options, in rough order of preference:

  • Filename prefix: 20260910T175500Z-fixed-1988-*.md — visible in ls, trivial to group, no in-file boilerplate.
  • In-file section: header line.
  • Raw bullet, defaulted to ### Fixed and re-sorted at collection.

Open questions for @nic

  1. last-push vs first-landed timestamp (the churn-reset tradeoff above).
  2. Does a release sweep all pending fragments, or respect the cooling-off bar?
  3. Cooling-off window: 4h as proposed, or another value.
  4. Who stamps the filename — a commit hook, a PR-template helper, or a tiny make add-changelog that writes new_changelogs/$(date -u +%Y%m%dT%H%M%SZ)-<n>-<slug>.md? (The daily/release jobs carry the [skip actions] token in their own commits only — never in a PR title — per the release-pipeline rule.)

Non-goals

  • Not auto-writing bullet prose — the author still writes the fragment.
  • Not changing what a changelog entry says, only where it lives until collection.

Acceptance

  • A PR with a new_changelogs/ fragment and no CHANGELOG.md edit passes the (reworked) changelog gate.
  • Two PRs adding fragments simultaneously never conflict on CHANGELOG.md.
  • A release cut against open PRs cannot fold any fragment (there is nothing in CHANGELOG.md to fold).
  • The daily job folds only fragments whose filename timestamp is older than the window, git rms them, and commits with [skip actions].

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions