Skip to content

CHANGELOG: gate released sections against their tags, not just the presence of [current] #1695

Description

@paul-hammant

The [current] release-fold has now bitten at least seven times. The existing gate ("CHANGELOG entry present") catches one half of it. This proposes the check that catches the other, more damaging half — and argues against the fix that first suggests itself.

The failure

The release job renames the first ## [current] heading into the new version number. A branch open across a release therefore has its heading renamed underneath it on main. Merging main then lands the branch's edits inside an already-tagged section, usually with no git conflict, because the two sides are textually close.

Concretely, on PR #1693 last night:

  1. Branch writes ## [current] + entry.
  2. v0.564.0 cut on main — that heading becomes ## [0.564.0], carrying the branch's own text into the tagged release.
  3. Branch merges main. Git auto-merges. The branch's later edits now rewrite the released 0.564.0 section.

Two things were broken, and the gate saw only the second:

  • the tagged 0.564.0 section had been silently rewritten, and
  • there was no ## [current] left at all.

A consequence that could not be repaired: a factually wrong claim in that entry shipped in v0.564.0 and is in the tagged release for good. It is corrected under [current] rather than by rewriting a tag.

What I do NOT recommend

The obvious fix — have the release job leave an empty ## [current] stub behind when it renames — is a one-line sed change:

sed -i "0,/^## \[current\]$/s//## [current]\n\n## [${VERSION}]/" CHANGELOG.md

The 0,/re/s//repl/ form already limits to the first match, so it is genuinely easy to write. But it would not have caught this case. With the stub, main has an empty ## [current] followed by ## [0.564.0] + content; the branch has ## [current] + the same content. Because the content lines are identical on both sides, git still merges them without conflict. The stub helps only when the branch's entry differs from what was released — the easy case, which is already survivable.

It is also not free: every release commit would then contain an empty section, and [skip changelog] releases would need to not emit one. Small, but not nothing, for a fix that misses the failure that keeps happening.

What I do recommend

Extend the existing CHANGELOG workflow with a second assertion: every already-tagged section must still be byte-identical to its tag.

for tag in $(git tag -l 'v0.*' --sort=-v:refname | head -8); do
  v="${tag#v}"
  a=$(git show "$tag:CHANGELOG.md" 2>/dev/null | sed -n "/^## \[$v\]/,/^## \[0/p" | sed '$d')
  [ -z "$a" ] && continue
  b=$(sed -n "/^## \[$v\]/,/^## \[0/p" CHANGELOG.md | sed '$d')
  if [ "$(printf '%s' "$a" | cksum)" != "$(printf '%s' "$b" | cksum)" ]; then
    echo "::error::CHANGELOG.md section [$v] no longer matches tag $tag."
    echo "A release was cut while this branch was open and the merge"
    echo "folded this branch's entry into a tagged section. Move it back"
    echo "under a fresh '## [current]' and restore [$v] from the tag:"
    echo "  git show $tag:CHANGELOG.md"
    exit 1
  fi
done

Verified against the actual failure, not just reasoned about:

  • on main today: checked 8 tags, 0 differ
  • on 2d0ba9fd, the merge commit that shipped the corruption: DIFFERS: 0.564.0

Two implementation notes, both learned the hard way here:

The check needs fetch-depth: 0 or an explicit git fetch --tags in that workflow — worth confirming, as the existing step only needs the two SHAs.

Why this is the better trade

The stub prevents a class of merge that is already survivable. The tag check detects the specific corruption that keeps shipping, costs one loop in an existing workflow, needs no change to the release job, and — unlike the stub — is verifiable against a commit we know was broken.

They are not exclusive; the stub could still land. But if only one goes in, this is the one that would have caught every occurrence so far.

Happy to send a PR if the shape looks right.

🤖 Generated with Claude Code

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