Skip to content

docs: add CHANGELOG.md (back to v8.7.1) and auto-update it on release - #441

Merged
twcclegg merged 8 commits into
mainfrom
changelog/add-changelog-and-automation
Aug 31, 2026
Merged

docs: add CHANGELOG.md (back to v8.7.1) and auto-update it on release#441
twcclegg merged 8 commits into
mainfrom
changelog/add-changelog-and-automation

Conversation

@twcclegg

@twcclegg twcclegg commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Changes

  • Add CHANGELOG.md at 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.
  • Update lib/github-actions-metadata-update.sh to append a new CHANGELOG.md entry for each future release, in the same commit as the metadata sync.
  • Mention the changelog in 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 — the net35/net6.0/net8.0/net9.0/net10.0 target additions and removals, the ShortNumberUtil and PhoneNumberToTimeZonesMapper additions, the libphonenumber-csharp.extensions package and its System.Text.Json converter and (most recently) TryParse/TypeConverter/ValidationAttribute additions, 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, stackalloc Format()), 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, and list_commits/search_pull_requests for 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.yml add it once the release/tag actually exists. I went with the former:

  • The version number isn't actually a guess. github-actions-metadata-update.sh names its branch metadata-update/vX.Y.Z and commits feat: automatic upgrade to vX.Y.Z before opening the PR — UPSTREAM_GITHUB_RELEASE_TAG is 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.yml can't push a follow-up commit anyway. It only tags an existing commit and calls the Releases API — per its own comments and github-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 own libphonenumber-csharp-bot PAT (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 calling POST /releases, which creates a tag ref rather than writing refs/heads/main, so it sidesteps the ruleset rather than needing an exemption from it.
  • Doing it in the metadata-update PR keeps everything in the one PR that already goes through checks + auto-merge — no second PR-and-review cycle per release, and no new permissions needed.

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).
  • Unit-tested the sed-based insertion logic standalone against a copy of the real CHANGELOG.md, including two insertions back-to-back, to confirm new entries land immediately below the <!-- next-entry --> marker and stay in reverse-chronological order.
  • Ran UPSTREAM_TAG=v9.0.33 DEPLOYED_VERSION=9.0.32 bash lib/github-actions-metadata-update.sh --dry-run to 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).
  • No C# code touched, so dotnet build/dotnet test weren't re-run for this change.

…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

codecov Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.33%. Comparing base (0bf7478) to head (71fdb57).
⚠️ Report is 18 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

claude added 6 commits August 30, 2026 01:21
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
- 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.
@twcclegg
twcclegg merged commit 9041672 into main Aug 31, 2026
6 checks passed
@twcclegg
twcclegg deleted the changelog/add-changelog-and-automation branch August 31, 2026 21:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants