docs: add CHANGELOG.md (back to v8.7.1) and auto-update it on release - #441
Merged
Conversation
…on release Adds a Keep a Changelog-style CHANGELOG.md covering every release from v8.7.1 through v9.0.38, built from tag history, GitHub Release notes, and merged PRs. Consecutive metadata-only releases are condensed into ranged entries; real code changes (bug fixes, new APIs, the strong-naming saga in v9.0.0/v9.0.1, TFM migrations, the Extensions package, the metadata-automation itself, etc.) get their own entries. github-actions-metadata-update.sh now appends a CHANGELOG.md entry for the release it's about to open a PR for, in the same commit as the metadata sync. The version number is already known at that point (it's the upstream tag being synced, copied verbatim), so there is nothing to guess. This was chosen over updating the changelog from finalize_metadata_release.yml because that workflow only tags an existing commit and calls the Releases API - it can't push a follow-up commit of its own, since main's branch-protection ruleset requires every push to go through a PR with no bypass for any actor, including this automation's own bot account (the same reason the sync switched from a direct push to a PR in the first place). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVGRJyQwtvBGXAmWswxyqW
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #441 +/- ##
==========================================
+ Coverage 87.26% 87.33% +0.07%
==========================================
Files 41 43 +2
Lines 3831 3885 +54
Branches 978 991 +13
==========================================
+ Hits 3343 3393 +50
- Misses 284 287 +3
- Partials 204 205 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The previous CHANGELOG.md draft bucketed large swaths of real work into "metadata-only" ranges by pattern-matching version numbers rather than checking what actually shipped in each tag. In particular v9.0.31-v9.0.37 was condensed to "6 metadata-only releases, no source changes" even though that stretch contains dotnet10 TFM support, AOT compatibility fixes, a full performance-optimization pass, a redesigned demo website, XXE hardening of the XML metadata parser, SECURITY.md/CODE_OF_CONDUCT/ CODEOWNERS, and deterministic-build work. This rebuild classifies all 212 consecutive tag-to-tag ranges from v8.7.1 to v9.0.38 by actually walking their commit logs and diffs, using `git log`/`git show` against the real history plus cross-referencing PR numbers where a commit subject alone was unclear. Ranges with zero hand-authored source/test/docs/dependency changes (metadata-only syncs, version-bump-only project-file touches, and mechanical LocaleData.cs regenerations) are condensed into ranged entries as before; every one of the 100+ ranges that touched real work gets its own entry, sized to what happened - a one-liner for a small fix, multiple ### subsections for a large release. Several misattributions in the previous draft were also corrected (e.g. net10.0 support actually landed in v9.0.32, not v9.0.30; the Extensions NuGet package and PhoneNumberToTimeZonesMapper both shipped in v8.13.2, not split across v8.13.1/v8.13.2). Every one of the 213 tags is covered by exactly one heading, verified against classification2.tsv with no gaps or duplicates, and headings are in strict descending version order. The `<!-- next-entry -->` marker automation in lib/github-actions-metadata-update.sh needed no changes - re-verified its `sed`-based insertion against the rebuilt file, and re-ran `bash -n` and shellcheck on the script. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVGRJyQwtvBGXAmWswxyqW
v9.0.10-v9.0.29 is 20 tags (10 through 19, then 20 through 29), not 19. v8.8.10-v8.9.2 is 5 tags (v8.8.10, v8.8.11, v8.9.0, v8.9.1, v8.9.2), not 6. Verified by cross-checking every ranged heading's stated count against the actual tag list; these were the only two mismatches. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVGRJyQwtvBGXAmWswxyqW
v8.12.44-v8.12.46 listed its start date as 2022-03-14 (actually v8.12.45's date); the real start (v8.12.44) is 2022-02-24. v8.12.40-v8.12.43 listed an end date of 2022-02-24 (actually v8.12.44's date, the tag right after the range); all four tags in that range share the same real date, 2022-02-14, so the range collapses to a single date per the file's own convention for same-day ranges. Verified by cross-checking every heading's date(s) against `git log` for its tag(s) - these were the only two mismatches across all 127 headings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVGRJyQwtvBGXAmWswxyqW
…LOG.md Every release always includes a metadata sync (that's the only thing that ever cuts a tag), but some releases also bundle other work merged to `main` in between. The AI-assisted push over the last few months made nearly every recent release substantive, but that's not the steady state - once things settle back into the normal cadence of mostly-routine upstream syncs, this keeps the file from growing one near-duplicate heading per release forever. github-actions-metadata-update.sh now checks whether anything outside resources/ changed on `main` since the last release (a targeted single-tag fetch plus a tree-level `git diff`, which works even from a shallow checkout). The new lib/update-changelog.js uses that to either extend the metadata-only range already at the top of the file - identified by a `<!-- changelog-run ... -->` marker only automated entries carry, never a hand-written or substantive one - or start a fresh single-release entry. A substantive release always gets its own standalone entry and can neither extend a prior run nor be reached past by a later one, since it carries no run marker of its own for anything to match against. Verified with a simulated release sequence (metadata-only, fold to 2, fold to 3, a substantive release breaking the chain, then a fresh metadata-only run afterward) confirming the fold/no-fold/reset behavior, correct spacing, and that no unrelated part of the file is touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVGRJyQwtvBGXAmWswxyqW
- Stop excluding CountryCodeToRegionCodeMap.cs from the metadata-only check. Despite its name, it's hand-maintained (its own header still says "todo make this file automatically generated"), so a change to it is real content a release could ship, not a byproduct of the resources/ sync - excluding it risked silently folding such a release away as "metadata-only". - Make warn() also emit a GitHub Actions ::warning:: annotation, not just a stderr line, so a missing/mangled CHANGELOG.md marker - which would otherwise silently stop every future changelog entry with nothing to notice it - shows up as a visible banner on the workflow run instead. - Replace the hardcoded 4-line splice in update-changelog.js's fold path with a real scan for where the existing block actually ends, so extending a run can't corrupt the file if the body template ever grows past one line. Re-verified with the same simulated release sequence as before (extended one step further) - fold, fold, fold, a substantive release breaking the chain, then a fresh run afterward - plus explicit checks for stray blank lines. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVGRJyQwtvBGXAmWswxyqW
lib/ never had a deliberate "use Node for tooling" decision - the existing
lib/*.js files (compare-benchmarks.js, fail-on-benchmark-regression.js,
format-benchmark-change.js) were added incidentally as an implementation
detail of unrelated CI work, and JavaScript was never intended anywhere
outside the demo site. Rather than add a fourth file to that pattern,
lib/update-changelog.sh reimplements the same fold/insert/reset logic in
bash: mapfile reads the file into an array, [[ =~ ]] with BASH_REMATCH
extracts the changelog-run marker's fields (from/first/start-date/count)
instead of a regex capture group, and the same block-boundary scan replaces
the JS version's array-slice splice.
Caught and fixed one real bug while porting: the blank-line-skip check used
`${LINES[$i]:-x}`, which can never distinguish "index unset" from "line is
actually blank" - both produce the fallback "x", so the check was always
false and folding could never trigger for *any* release. Fixed by bounds-
checking the index first, then comparing the element directly. Caught only
because the port was verified against the exact same 6-release simulation
used for the JS version and diffed byte-for-byte against its output -
worth noting since the JS version, despite passing that same simulation
seemingly successfully in an earlier pass, was actually never run through
a second consecutive fold in that verification the way this port's
regression surfaced it.
Also re-verified all argument-validation error paths (missing args, bad
metadata-only value, missing marker) produce identical messages and exit
codes to the JS version.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WVGRJyQwtvBGXAmWswxyqW
wmundev
approved these changes
Aug 31, 2026
- v9.0.37: "not a metadata-only release, despite the version bump looking routine" restates the file's own intro paragraph, which already establishes that every release is a metadata release and real work just rides along in whichever one comes next. - v9.0.33, v9.0.32, v9.0.30: dropped "the previous CHANGELOG draft called/attributed ..." - that's PR-review history about how this file's own content got sorted out, not release history. Kept the actual useful fact in the v9.0.30 entry (net10.0 landed the release after, not this one) without the meta-narrative around it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes
CHANGELOG.mdat the repo root, populated retroactively from v8.7.1 through the current v9.0.38, built from tag history, GitHub Release notes, and merged PR titles.lib/github-actions-metadata-update.shto append a newCHANGELOG.mdentry for each future release, in the same commit as the metadata sync.README.md's intro and "Metadata updates" section.Retroactive changelog
212 tags exist between v8.7.1 (2017) and today. Rather than 212 near-identical headers, consecutive metadata-only releases are condensed into ranged entries (e.g.
## [v8.11.4 – v8.12.34] - 2020-02-13 – 2021-10-07, "35 metadata-only releases..."), while releases with an actual code change get their own entry with real detail — thenet35/net6.0/net8.0/net9.0/net10.0target additions and removals, theShortNumberUtilandPhoneNumberToTimeZonesMapperadditions, thelibphonenumber-csharp.extensionspackage and itsSystem.Text.Jsonconverter and (most recently)TryParse/TypeConverter/ValidationAttributeadditions, several real bug fixes (leading zeroes, a+being stripped, MX area codes, a null-ref in normalization), the perf work in 2023 ("reduce regex abuse", skip internal builders, stackallocFormat()), the binary-metadata build pipeline, the strong-naming saga in v9.0.0/v9.0.1 (added, then reverted the very next release), and this release's own rework of the metadata-sync automation. Every version number still gets a matching entry — none are silently skipped, they're just grouped where nothing happened.Content came from
git tag/git log,list_releases/get_release_by_tag, andlist_commits/search_pull_requestsfor the PRs behind each interesting-looking commit message — not invented.Where the automation lives, and why
The task suggested two options: update the changelog in the metadata-update PR itself, or have
finalize_metadata_release.ymladd it once the release/tag actually exists. I went with the former:github-actions-metadata-update.shnames its branchmetadata-update/vX.Y.Zand commitsfeat: automatic upgrade to vX.Y.Zbefore opening the PR —UPSTREAM_GITHUB_RELEASE_TAGis already the exact version this port will publish under, copied verbatim from the upstream tag being synced (this port tracks upstream's version number 1:1). There's nothing to wait for.finalize_metadata_release.ymlcan't push a follow-up commit anyway. It only tags an existing commit and calls the Releases API — per its own comments andgithub-actions-metadata-update.sh's file-level comment,main's branch-protection ruleset requires a PR + passing checks for every push, with no bypass for any actor, including this automation's ownlibphonenumber-csharp-botPAT (that's the documented reason the metadata sync itself moved from a direct push to a PR). A direct push of a changelog commit from the finalize step would hit the exact same wall the metadata sync used to. I couldn't query the ruleset directly via the API in this sandbox (GitHub App access for that call isn't enabled here), so I relied on the two workflows' own comments, which state this as settled fact already —finalize_metadata_release.yml's only non-tag/release/dispatch action is callingPOST /releases, which creates a tag ref rather than writingrefs/heads/main, so it sidesteps the ruleset rather than needing an exemption from it.The entry itself is intentionally terse for the common case (a one-line "Metadata update to upstream vX.Y.Z" with a compare link) — anything worth more than that is something a human would want to word by hand anyway, and can still edit the PR before it auto-merges.
Testing
bash -n lib/github-actions-metadata-update.sh— syntax check.shellcheck lib/github-actions-metadata-update.sh— no new findings (both pre-existing warnings are unrelated to this change).sed-based insertion logic standalone against a copy of the realCHANGELOG.md, including two insertions back-to-back, to confirm new entries land immediately below the<!-- next-entry -->marker and stay in reverse-chronological order.UPSTREAM_TAG=v9.0.33 DEPLOYED_VERSION=9.0.32 bash lib/github-actions-metadata-update.sh --dry-runto confirm the rest of the script's flow is unaffected (it exits before reaching the new code, same as before, since the sandbox can't reach the GitHub API to go further).dotnet build/dotnet testweren't re-run for this change.