The release pipeline's CHANGELOG handling has two failure modes, both silent, and both have now bitten. Neither is a bug in the code as written — release.yml:173-176 does exactly what it says — but the surrounding conditions let work ship unrecorded or misattributed.
Failure mode 1: no [current] → the release records nothing
if grep -q '## \[current\]' CHANGELOG.md; then
sed -i "s/## \[current\]/## [${VERSION}]/" CHANGELOG.md
git add CHANGELOG.md
fi
When a merge adds no [current] section, the if is simply false and the release proceeds with no CHANGELOG section at all — no warning, no failure.
Observed: releases 0.506.0 through 0.509.0 all shipped with no section. VERSION read 0.509.0 while the newest heading was [0.505.0]. Four releases of real work — the Windows .exe install fix, the nightly install/tags fix, the fs.read error-detail work, contrib/avcodec — recorded nowhere. Backfilled in #1474.
Failure mode 2: the rename races the merge window
The release workflow branches from main when it starts. A feature PR that merges between that moment and the release merge is in the release but not in its CHANGELOG.
Observed, 18 seconds apart:
20:02:45 #1476 merged to main (feat: io.fd_read_into)
20:03:03 #1475 merged (chore: release 0.510.0)
The release branch was cut before #1476 landed, so grep -q '## [current]' correctly found nothing — the previous [current] had just been consumed by the backfill and the new entry did not yet exist. Net effect: 0.510.0 shipped fd_read_into while its entry sat under [current], about to be misattributed to 0.511.0. Corrected by hand.
Failure mode 3 (older, same family): the rename can create a duplicate
## [0.435.0] and ## [0.497.0] each appear twice on main today. A blind sed rename will happily create a heading that already exists, which is how those arose.
Suggested fix
Three guards, cheap and independent:
-
Fail the release when there is no [current] to rename. A release with nothing to say is nearly always an oversight, not an intent. If a genuinely empty release is wanted, make that explicit — an [skip changelog] token in the release-trigger commit, or a hand-written empty section.
-
Refuse to create a duplicate heading. Before the sed, check whether ## [${VERSION}] already exists and fail loudly if so.
-
Close the race. Either re-check for [current] immediately before the release PR merges (rather than only when the branch is cut), or have the release workflow rebase onto main and re-run the rename as a final step. A cheaper mitigation: a CI check on every non-release PR that fails when the diff touches std/, runtime/, compiler/ or contrib/ but not CHANGELOG.md — that at least guarantees a [current] exists to be renamed.
Guard 1 alone would have caught all four missing releases. Guard 3's CI check would have prevented them being missing in the first place.
Why this matters more than tidiness
The CHANGELOG is what a downstream consumer reads to decide whether to upgrade. Four consecutive releases of silence — including a Windows packaging fix that made installs unusable — is the kind of gap that erodes trust in the file entirely, at which point people stop reading it and stop maintaining it.
The release pipeline's CHANGELOG handling has two failure modes, both silent, and both have now bitten. Neither is a bug in the code as written —
release.yml:173-176does exactly what it says — but the surrounding conditions let work ship unrecorded or misattributed.Failure mode 1: no
[current]→ the release records nothingWhen a merge adds no
[current]section, theifis simply false and the release proceeds with no CHANGELOG section at all — no warning, no failure.Observed: releases 0.506.0 through 0.509.0 all shipped with no section.
VERSIONread0.509.0while the newest heading was[0.505.0]. Four releases of real work — the Windows.exeinstall fix, the nightly install/tags fix, thefs.readerror-detail work,contrib/avcodec— recorded nowhere. Backfilled in #1474.Failure mode 2: the rename races the merge window
The release workflow branches from
mainwhen it starts. A feature PR that merges between that moment and the release merge is in the release but not in its CHANGELOG.Observed, 18 seconds apart:
The release branch was cut before #1476 landed, so
grep -q '## [current]'correctly found nothing — the previous[current]had just been consumed by the backfill and the new entry did not yet exist. Net effect: 0.510.0 shippedfd_read_intowhile its entry sat under[current], about to be misattributed to 0.511.0. Corrected by hand.Failure mode 3 (older, same family): the rename can create a duplicate
## [0.435.0]and## [0.497.0]each appear twice onmaintoday. A blindsedrename will happily create a heading that already exists, which is how those arose.Suggested fix
Three guards, cheap and independent:
Fail the release when there is no
[current]to rename. A release with nothing to say is nearly always an oversight, not an intent. If a genuinely empty release is wanted, make that explicit — an[skip changelog]token in the release-trigger commit, or a hand-written empty section.Refuse to create a duplicate heading. Before the
sed, check whether## [${VERSION}]already exists and fail loudly if so.Close the race. Either re-check for
[current]immediately before the release PR merges (rather than only when the branch is cut), or have the release workflow rebase ontomainand re-run the rename as a final step. A cheaper mitigation: a CI check on every non-release PR that fails when the diff touchesstd/,runtime/,compiler/orcontrib/but notCHANGELOG.md— that at least guarantees a[current]exists to be renamed.Guard 1 alone would have caught all four missing releases. Guard 3's CI check would have prevented them being missing in the first place.
Why this matters more than tidiness
The CHANGELOG is what a downstream consumer reads to decide whether to upgrade. Four consecutive releases of silence — including a Windows packaging fix that made installs unusable — is the kind of gap that erodes trust in the file entirely, at which point people stop reading it and stop maintaining it.