Skip to content

chore(release): promote changelog into [4.0.4] (+ fix promote tooling) - #3219

Merged
bpamiri merged 5 commits into
developfrom
release/changelog-4.0.4
Jun 19, 2026
Merged

chore(release): promote changelog into [4.0.4] (+ fix promote tooling)#3219
bpamiri merged 5 commits into
developfrom
release/changelog-4.0.4

Conversation

@bpamiri

@bpamiri bpamiri commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Release prep for 4.0.4. Two commits:

  1. fix(build)tools/changelog-promote.sh (the #2994 fragment system, never used in a real cut) wrote ## [version] - date (two-hash, no tag link). But every released section, and release.yml's notes extraction (awk '/^# \[VERSION\]/,/^---$/'), uses a single-hash # [version](tag) => date. The mismatch meant the one-hash awk would extract zero lines → an empty GitHub Release. Boundary detection also matched the next ## heading, which on this changelog lands on a subheading inside the 3.0.0 section, pulling every release since into the promoted body. Fixed: match #/## version headers, drop stray ---/---- rules from the body, emit # [ver](tag) => date with explicit --- separators.

  2. chore(release) — assemble the 83 pending changelog.d/ fragments (+ prior [Unreleased] content) into the [4.0.4] section and clear the folder. Done on develop first per the playbook.

Verified locally: the [4.0.4] header is single-hash with a tag link, the separator is exactly ---, release.yml's awk extracts the section with no bleed into 4.0.3, and every prior version heading still appears exactly once.

🤖 Generated with Claude Code

Peter Amiri and others added 2 commits June 18, 2026 18:09
…rmat

The changelog.d fragment system (#2994) was never exercised by a real cut.
Its promote step wrote "## [version] - date" (two-hash, no tag link), but
every released section — and release.yml's notes extraction
(awk '/^# \[VERSION\]/,/^---$/') — uses a single-hash "# [version](tag) => date".
With the two-hash header the one-hash awk extracts zero lines, so the GitHub
Release would publish with EMPTY notes.

Also: boundary detection searched for the next "## " heading, but version
sections here are single-hash, so the first "## " match landed on a
subheading inside the 3.0.0 section — pulling every release since into the
promoted body. And a stray "----"/"---" rule in [Unreleased] could leak in.

- Match the next version heading at "#" OR "##" + " [" (correct tail boundary).
- Drop horizontal-rule lines from the promoted body.
- Emit "# [version](tag-url) => date" + explicit "---" separators around it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Amiri <petera@pai.com>
Promote the 83 pending changelog.d fragments (plus prior [Unreleased] content)
into the [4.0.4] release section via tools/changelog-promote.sh, and clear the
fragment folder. Done on develop first (per the release playbook) so develop's
[Unreleased] doesn't go stale and force a back-port after the cut.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Amiri <petera@pai.com>
@github-actions github-actions Bot added the docs label Jun 19, 2026
The first promote pass exposed a second defect: when [Unreleased] carries the
same heading twice (develop had two '### Performance' blocks), the merge
appended the fragment bullets to EACH same-named section — double-counting every
performance fragment (e.g. the $getStatusCodes entry appeared twice). It also
emitted sections in [Unreleased] file order with the duplicate intact and
fragment-only sections tacked on last.

merged_sections now combines all bullets for a heading into ONE section and
emits sections in canonical order (Added, Changed, Deprecated, Removed,
Performance, Fixed, Security), existing bullets before fragment bullets.

Regenerated the [4.0.4] section: 116 unique entries (was 124 with 8 dupes),
canonical order, no duplicate headings. Verified zero unique entries lost.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Amiri <petera@pai.com>

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer

TL;DR — Release-prep for 4.0.4: a fix(build) to tools/changelog-promote.sh (emit the single-hash, tag-linked, =>-dated section format release.yml's awk extraction expects) plus the chore(release) that promotes the 83 changelog.d/ fragments into the [4.0.4] section. The tooling fix is correct and well-reasoned, and the section boundaries are clean — release.yml's awk '/^# \[4.0.4\]/,/^---$/' extracts the section with zero bleed (verified below). Verdict: comment. No correctness / cross-engine / security / commitlint blockers. But the promoted [4.0.4] section ships duplicate changelog bullets and a duplicated ### Performance heading in the user-facing release notes, which should be cleaned up before the tag is cut.

Docs

1. The promoted [4.0.4] section has two ### Performance headings with duplicated bullets. CHANGELOG.md lines 44 and 154 are both ### Performance inside the same [4.0.4] section (lines 29–172), and several bullets appear verbatim in both:

  • line 47 and line 157 — both Request-lifecycle mixin injection ... $pluginObj() ... (#2897, Stage 3)
  • line 53 and line 163 — both Mixin-free apps no longer pay a throwaway wheels.Plugins ... (Stage 3 quick win)
  • line 54 and line 164 — both Database column metadata is now memoized per datasource+table ... schemaColumnCache ... (#3210)

The section order also ends up Added → Performance → Fixed → Security → Performance → Changed, not the canonical Added, Changed, Deprecated, Removed, Performance, Fixed, Security from tools/changelog-promote.sh:38.

Root cause: the pre-existing ## [Unreleased] body had a second, hand-added ### Performance block whose bullets overlapped the *.performance.md fragments; merged_sections() (tools/changelog-promote.sh:127) preserves existing-section file order and never coalesces two same-named existing sections, so both copies survive. The script faithfully promoted malformed input — but since this PR produces the final 4.0.4 release notes (and the GitHub Release is built from exactly this range), the fix belongs here: collapse the two ### Performance blocks into one (dropping the duplicate #2897 / #3210 bullets) and move ### Changed into canonical position before tagging. Worth a follow-up to make merged_sections() coalesce duplicate headings so a malformed [Unreleased] can't reintroduce this.

2. Stale usage header in tools/changelog-promote.sh. The fix changed the emitted format, but the top-of-file usage comment still advertises the old shape:

tools/changelog-promote.sh:8:  #                                                   # "## [<version>] - <date>" section,

The script now writes # [<version>](tag-url) => <date> (line 199). The summary print() at line 213 (f"[{version}] - {date} now carries ...") likewise still uses the old - form. Cosmetic, but the header comment is the contract a future maintainer reads — update it to match the single-hash, tag-linked, =>-dated format.

Correctness

No issues. The regex fix is sound: re.search(r"(?m)^#{1,2} \[", rest) (tools/changelog-promote.sh:100) correctly stops the [Unreleased] body at the next version heading — ### Added / ### Performance (three hashes) and ## Detailed Changes (no [) are both excluded, so the tail begins at # [4.0.3] rather than a ## subheading buried in the 3.0.0 section. The horizontal-rule strip (re.fullmatch(r"-{3,}", line.strip()), line 114) correctly drops the original four-dash ---- separator that sat below [Unreleased] and replaces it with the explicit --- (line 204) — I confirmed line 173 of the result is exactly ---, so the release.yml four-dash-bleed guard (.github/workflows/release.yml:121-136) passes. The awk '/^# \[4.0.4\]/,/^---$/' extraction yields exactly one # [ heading, i.e. zero bleed into 4.0.3.

Commits

Both conform to commitlint.config.js: fix(build): ... and chore(release): ... — valid types, scopes, subjects ≤ 100 chars, DCO sign-off present. Messages explain the "why" (the empty-GitHub-Release failure mode) well.

Address the Reviewer-A nit: the top-of-file usage comment still advertised the
old "## [version] - date" shape and the summary print() used " - " — both now
reflect the single-hash, tag-linked, "=>"-dated section the script emits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Amiri <petera@pai.com>

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer

TL;DR — Release-prep for 4.0.4: a fix(build) pair on tools/changelog-promote.sh (single-hash tag-linked => date section format + per-heading section consolidation in canonical order) plus the chore(release) promotion that assembles the 83 changelog.d/ fragments into the [4.0.4] section. The generated CHANGELOG.md output is correct and the consolidation logic is sound — but commit b86e75f29 changed the emitted section format while leaving the companion dev test asserting the old format, so tools/test-changelog-promote.sh now fails, and the new consolidation behavior (the headline of commit f6646784a) ships untested. Verdict: request changes — update the test in this PR.

Tests

tools/test-changelog-promote.sh:90 asserts the pre-change format and now fails. Commit b86e75f29 changed the emitted version header from ## [{version}] - {date} to single-hash/tag-linked/=>:

# tools/changelog-promote.sh:226
version_section = f"# [{version}]({tag_url}) => {date}\n\n{render(merged)}\n"

producing e.g. # [1.2.3](https://github.com/wheels-dev/wheels/releases/tag/v1.2.3) => 2026-01-01. But Test 3 still greps for the two-hash/- form:

# tools/test-changelog-promote.sh:90
if grep -q '## \[1.2.3\] - 2026-01-01' "$tmp/CHANGELOG.md"; then
  ok "promote wrote the '## [1.2.3] - 2026-01-01' section"
else
  bad "CHANGELOG.md is missing the promoted version section"

That pattern cannot match the new output (# not ##, (url) => not -), so Test 3 reports FAIL: CHANGELOG.md is missing the promoted version section and exit $fail is non-zero. The prior format on develop (## [{version}] - {date}, changelog-promote.sh:181 there) is what the test was written against, so this PR's format change owns the breakage. The file is not in the diff — it was not updated.

Concrete fix — anchor the assertion to the new format the script (and release.yml's awk '/^# \[VERSION\]/,/^---$/') actually expects:

if grep -qE '^# \[1\.2\.3\]\(https://github.com/wheels-dev/wheels/releases/tag/v1\.2\.3\) => 2026-01-01$' "$tmp/CHANGELOG.md"; then
  ok "promote wrote the '# [1.2.3](tag) => 2026-01-01' section"
else
  bad "CHANGELOG.md is missing the promoted version section"
fi

No coverage for the new consolidation behavior. merged_sections (commit f6646784a) is the substantive change — it consolidates a heading that appears twice in [Unreleased] into one section and emits canonical order, existing-before-fragment bullets. None of the three existing tests exercise a duplicate-heading [Unreleased] fixture, so the exact regression this commit fixes (double-appended fragment bullets across two ### Performance blocks) would not be caught by the suite. Please add a fixture with two same-named ### blocks plus a matching fragment and assert the promoted section has a single heading with no duplicated bullets.

Correctness (verified clean)

For the record, the produced artifact checks out — these are confirmations, not findings:

  • CHANGELOG.md:29# [4.0.4](…/tag/v4.0.4) => 2026-06-18: single hash, tag link, => date. ✔
  • Exactly one --- separator on each side of the section (CHANGELOG.md:27 and :162), so release.yml's awk '/^# \[VERSION\]/,/^---$/' extracts the full section with no bleed into 4.0.3. ✔
  • 4.0.4 headings are Added → Changed → Performance → Fixed → Security — canonical order, no duplicates (merged_sections emits CANONICAL order, non-canonical headings preserved after). ✔
  • The defensive for heading in fragments: if heading not in CANONICAL loop (changelog-promote.sh:160) is unreachable given read_fragments only yields TYPES-mapped names — correctly noted as defensive in the comment, not a bug.

Commits

fix(build): consolidate changelog sections by heading in canonical order, chore(release): assemble changelog.d fragments into [4.0.4], and fix(build): changelog-promote emits release.yml-compatible section format all conform to commitlint (valid types/scopes, headers ≤ 100 chars, not ALL-CAPS) and carry Signed-off-by:. No issues.

Nothing else fires — the direct CHANGELOG.md edit here is the legitimate promote operation, not an [Unreleased]-anchor edit, and there is no CFML/cross-engine or security surface in this bash/python tooling.

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer

TL;DR — Release-prep for 4.0.4: a fix(build) trio on tools/changelog-promote.sh (single-hash tag-linked => date section format + per-heading consolidation in canonical order), a docs(build) sync of the usage header/summary text, and the chore(release) promotion assembling the 83 changelog.d/ fragments into the [4.0.4] section. The new head commit 0a3cd2352 correctly addressed the doc-comment/summary nit from the prior round — but the load-bearing finding is still open: the format change made tools/test-changelog-promote.sh assert output the script no longer produces, so that test now fails and the new consolidation path ships untested. Verdict: request changes — fix the test in this PR.

Tests

tools/test-changelog-promote.sh:90 still asserts the pre-change format and now fails. The script emits the single-hash/tag-linked/=> header:

# tools/changelog-promote.sh:228
version_section = f"# [{version}]({tag_url}) => {date}\n\n{render(merged)}\n"

producing # [1.2.3](https://github.com/wheels-dev/wheels/releases/tag/v1.2.3) => 2026-01-01. But Test 3 still greps for the two-hash/- form:

# tools/test-changelog-promote.sh:90
if grep -q '## \[1.2.3\] - 2026-01-01' "$tmp/CHANGELOG.md"; then
  ok "promote wrote the '## [1.2.3] - 2026-01-01' section"
else
  bad "CHANGELOG.md is missing the promoted version section"

# is not ## and (url) => is not -, so the grep can never match → badfail is incremented → exit $fail (:106) is non-zero. git diff origin/develop..0a3cd2352 -- tools/test-changelog-promote.sh is empty — the file was not touched in this PR, even though commit b86e75f29 here owns the format change that broke it. The head commit 0a3cd2352 ("Address the Reviewer-A nit") fixed only the usage comment (:7-11) and the summary print() (:242), not this assertion.

Concrete fix — anchor the assertion to the format the script (and release.yml's awk '/^# \[VERSION\]/,/^---$/') actually expects:

if grep -qE '^# \[1\.2\.3\]\(https://github.com/wheels-dev/wheels/releases/tag/v1\.2\.3\) => 2026-01-01$' "$tmp/CHANGELOG.md"; then
  ok "promote wrote the '# [1.2.3](tag) => 2026-01-01' section"
else
  bad "CHANGELOG.md is missing the promoted version section"
fi

No coverage for the new consolidation behavior. merged_sections (commit f6646784a) — consolidating a heading that appears twice in [Unreleased] into one section in canonical order — is the substantive change, but none of the three fixtures in tools/test-changelog-promote.sh use a duplicate-heading [Unreleased], so the exact regression it fixes (double-appended bullets across two same-named ### blocks) would not be caught. Please add a fixture with two same-named ### blocks plus a matching fragment and assert the promoted section has a single heading with no duplicated bullets.

Correctness (verified clean)

The generated artifact still checks out — confirmations, not findings:

  • CHANGELOG.md:29# [4.0.4](…/tag/v4.0.4) => 2026-06-18: single hash, tag link, => date. ✔
  • The summary print() change in 0a3cd2352 (changelog-promote.sh:242) is cosmetic stdout text; Test 4's grep -q 'Removed 1 fragment' keys off :243, which is unchanged, so that assertion still passes. ✔
  • Usage comment (:7-11) now matches the emitted shape. ✔

Commits

The new docs(build): sync changelog-promote usage header + summary to new format conforms to commitlint (valid type/scope, header ≤ 100 chars, not ALL-CAPS) and carries Signed-off-by:. No issues.

Nothing else fires — the direct CHANGELOG.md edit is the legitimate promote operation, not an [Unreleased]-anchor edit, and there is no CFML/cross-engine or security surface in this bash/python tooling.

Address Reviewer-A (CHANGES_REQUESTED): the format change broke Test 3, which
still asserted the old "## [ver] - date" header — re-anchor it to the
single-hash, tag-linked, "=>"-dated section the script now emits. Add Test 5
covering the merged_sections consolidation: a [Unreleased] body with two
"### Performance" blocks plus a matching *.performance.md fragment must collapse
to one heading in canonical order with each bullet appearing exactly once (the
double-count regression the 4.0.4 cut surfaced). Full suite green (11/11).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Amiri <petera@pai.com>

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer

TL;DR — Release-prep for 4.0.4. The new head commit 6e0b12e55 (test(build): cover new changelog-promote format + section consolidation) resolves both still-open blocking findings from the prior CHANGES_REQUESTED rounds: Test 3 is re-anchored to the single-hash/tag-linked/=> header the script now emits, and a new Test 5 exercises the merged_sections consolidation path. I traced both assertions against the actual script behavior and they hold. Verdict: approve — submitting --approve to supersede the earlier wheels-bot CHANGES_REQUESTED reviews (issue #3048; otherwise the stale block keeps the PR merge-wedged).

Resolved blocking findings (audit trail)

1. tools/test-changelog-promote.sh:90 asserted the pre-change format → fixed. The assertion now reads:

# tools/test-changelog-promote.sh:90
if grep -qE '^# \[1\.2\.3\]\(https://github.com/wheels-dev/wheels/releases/tag/v1\.2\.3\) => 2026-01-01$' "$tmp/CHANGELOG.md"; then

which matches the script's emitted header verbatim:

# tools/changelog-promote.sh:228
version_section = f"# [{version}]({tag_url}) => {date}\n\n{render(merged)}\n"

with tag_url = https://github.com/wheels-dev/wheels/releases/tag/v{version} (:227). Single hash, tag link, => date — Test 3 now passes.

2. No coverage for the new consolidation behavior → added (Test 5, :106-157). The fixture (:114-136) seeds a [Unreleased] body with two ### Performance blocks plus an ### Added block, and adds a matching speed.performance.md fragment (:137). I traced it through merged_sections (changelog-promote.sh:129-174): the two ### Performance blocks plus the fragment bullet collapse into one Performance section, emitted after Added in CANONICAL order (:169-171). The three assertions match that result:

  • nperf -eq 1 (:142) — one ### Performance heading ✔
  • each bullet (perf block one/two, perf from fragment, an added entry) appears exactly once (:148-152) ✔ — this is the precise double-append regression the commit fixes
  • canonical order, Added before Performance (:153) ✔

Correctness (verified clean)

  • Head commit 6e0b12e55 touches only tools/test-changelog-promote.sh (git diff 0a3cd2352..6e0b12e55 --stat); CHANGELOG.md and changelog-promote.sh are byte-identical to the prior round, where the promoted [4.0.4] artifact was already confirmed clean (single-hash tag-linked header at CHANGELOG.md:29, exactly one --- on each side for release.yml's awk '/^# \[VERSION\]/,/^---$/' extraction, canonical heading order). ✔
  • Test 5 runs the script in an isolated mktemp -d root (:111-138) — PROJECT_ROOT derives from the copied script's dirname, so it never touches the real changelog. ✔

Commits

test(build): cover new changelog-promote format + section consolidation conforms to commitlint (valid type/scope, header ≤ 100 chars, not ALL-CAPS) and carries Signed-off-by:. No issues.

Tests (one non-blocking nit)

The new test is labeled "Test 5" but the suite has no "Test 4" — the Removed 1 fragment check (:100-104) lives inside the Test 3 block rather than a standalone Test 4. Harmless comment-numbering gap; rename to "Test 4" at convenience. Not a merge blocker.

Nothing else fires — the direct CHANGELOG.md edit is the legitimate promote operation (not an [Unreleased]-anchor edit), and there is no CFML / cross-engine / security surface in this bash+python tooling.

@bpamiri
bpamiri merged commit 8c4b2f3 into develop Jun 19, 2026
6 checks passed
@bpamiri
bpamiri deleted the release/changelog-4.0.4 branch June 19, 2026 01:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant