Skip to content

fix(release): consolidated CHANGELOG.md misses changesets without @autonomos/app - #244

Merged
aterrylu merged 1 commit into
mainfrom
terry/changelog-merge-all-packages
Jun 24, 2026
Merged

fix(release): consolidated CHANGELOG.md misses changesets without @autonomos/app#244
aterrylu merged 1 commit into
mainfrom
terry/changelog-merge-all-packages

Conversation

@aterrylu

Copy link
Copy Markdown
Owner

Problem

The consolidated root CHANGELOG.md — which becomes the GitHub Release body via scripts/release-notes.ts — was missing almost every change. The v0.3.0 release showed 1 entry (#208) instead of the ~21 changesets that fed it. nox-0x flagged this on #205 and again on #211; both shipped with it deferred.

Root cause

scripts/sync-changelog.ts promoted a single representative package (packages/app/CHANGELOG.md) into the root changelog, on the assumption that the changesets fixed group makes every package's CHANGELOG identical. It doesn't. The fixed group locks version numbers in lockstep, but @changesets/changelog-github writes each changeset's entry only into the CHANGELOG of the package(s) its frontmatter names. So every server-/dashboard-/core-/cli-only changeset landed in that package's CHANGELOG and was never read.

The smoking gun is still in the tree: root ## [0.2.0] is completely empty — that release's changesets named no @autonomos/app package at all.

flowchart TD
    subgraph CS["changeset version"]
        A["#208 lists @autonomos/app"] --> APP["packages/app/CHANGELOG.md"]
        B["#240 lists @autonomos/server"] --> SRV["packages/server/CHANGELOG.md"]
        C["#242 lists @autonomos/dashboard"] --> DASH["packages/dashboard/CHANGELOG.md"]
    end
    APP -->|"OLD: only source read"| ROOT["root CHANGELOG.md → Release body"]
    SRV -. "OLD: DROPPED" .-> X["(invisible)"]
    DASH -. "OLD: DROPPED" .-> X
    APP -->|"NEW: merge all"| ROOT
    SRV -->|"NEW: merge all"| ROOT
    DASH -->|"NEW: merge all"| ROOT
Loading

Solution

Rewrite sync-changelog.ts to merge every packages/*/CHANGELOG.md, deduplicate by PR (a PR seen in multiple packages collapses to one line, keeping the highest severity), and render one concise line per PR — the title pulled from the squash-merge commit subject (git log -1 --format=%s, trailing (#NNN) stripped), per Terry's preference for terse release bodies.

Output format:

### Minor Changes

- [#240](.../pull/240) `7c2f7f2` — fix(server): stop leaking host CLAUDE_CODE_* env into spawned agents

scripts/release-notes.ts is untouched — it faithfully extracts whatever section this writes; the bug was entirely upstream of it.

Hardening — the bug was itself a silent drop, so the fix is defensive

  • Signature-anchored entry detection (- [#NNN] / - [`sha`]) so a multi-paragraph changeset body's prose or sub-bullets can't become phantom entries.
  • Per-package contribution logging + a parsed-vs-consumed reconciliation warning that surfaces partial drops (the actual v0.3.0 shape — 1 of 21) in CI output. Empty (--empty) changesets are excluded from the count to avoid false alarms.
  • Hard-fail (exit 1) when a release section comes out empty while real changesets were consumed this run — converts the silent drop into a loud CI failure.
  • git error handlers now log instead of silently degrading the guardrail.

Testing

  • scripts/sync-changelog.test.ts — 21 unit tests (pure functions; injected title-resolver so no git/fs needed). Covers: cross-package merge, multi-package PR dedup, severity tie-break, PR-ascending order, title extraction, phantom-bullet prevention, orphan-bullet skipping, --empty classification, and the full guardDecision matrix.
  • Wired into CI: added scripts/*.test.ts to the make check tsx --test glob (Makefile). Test count in CI: 532 → 553.
  • Full CI gate run locally (bunx biome check packages/ && make check): green.
  • Real-world end-to-end: ran the actual script against fixtures referencing real merge SHAs — old behavior surfaced 1 PR, new surfaces all 4 across app/server/dashboard, with release-notes.ts extracting the section correctly. Verified the phantom-sub-bullet case produces no junk entry.

Why this matters now

This PR's own changeset is scoped to @autonomos/server (a non-app package) — so it's a live dogfood: under the old script it would have been dropped from v0.4.0's release body; under the new one (which main will carry once this merges) it must appear.

Risks & alternatives

  • Risk: title resolution depends on the squash-merge convention (enforced on this repo) and a full-history checkout (release.yml / version.yml both use fetch-depth: 0). On a miss, it falls back to the changeset body, then (no description) (with a warning) — never a crash.
  • Alternatives considered: generating from git log between tags (loses changeset-curated severity), parsing the changesets bot PR body (fragile, needs network at version time), reading raw .changeset/*.md pre-consumption (loses changelog-github's PR/author resolution). Merging the per-package CHANGELOGs keeps the curated data and needs no network.

🤖 Generated with Claude Code

…tonomos/app

The release changelog consolidator (scripts/sync-changelog.ts) sourced only
packages/app/CHANGELOG.md, so any changeset that didn't list @autonomos/app —
server-, dashboard-, core-, cli-only — was silently dropped from the root
CHANGELOG.md and the GitHub Release body (v0.3.0 showed 1 of ~21 changes).

The changesets `fixed` group locks version numbers in lockstep but does NOT
replicate changelog entries across per-package CHANGELOGs. Fix: merge every
packages/*/CHANGELOG.md, dedup by PR (highest severity wins), and render one
concise line per PR with the title taken from the squash-merge commit subject
(git log %s, trailing " (#NNN)" stripped). release-notes.ts is unchanged — it
faithfully extracts whatever section this writes.

Hardening (the bug was itself a silent drop — never recur):
- precise, signature-anchored entry detection so a body's prose or sub-bullets
  can't be misread as phantom entries
- per-package contribution logging + a parsed-vs-consumed reconciliation warning
  that surfaces PARTIAL drops (the actual v0.3.0 shape: 1 of 21) in CI output
- hard-fail when a release section is empty while real (non-empty) changesets
  were consumed this run
- git error handlers now log instead of silently degrading the guardrail

Tests: scripts/sync-changelog.test.ts (21 cases) wired into `make check`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HW8EE2YaKwmF6j5PRpeBt3
@aterrylu
aterrylu enabled auto-merge (squash) June 23, 2026 04:25

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving — the fix correctly merges every packages/*/CHANGELOG.md and dedupes by PR, with thoughtful guardrails (signature-anchored bullet detection, parsed-vs-consumed reconciliation, hard-fail on empty section when changesets were consumed). The pure/impure split makes the regression unit-testable, and the 21 tests cover the right paths. sync-changelog.ts runs inside version.yml which already sets fetch-depth: 0, so gitTitle will resolve. The PR's own changeset (@autonomos/server only) is a live dogfood — if the script is broken it would silently drop itself.

Minor follow-ups, not blocking:

  • Tests don't exercise the Major severity path. Trivial to add.
  • A single PR with multiple changesets of differing severities collapses to the highest one; the lower-severity note is dropped from the release body. Intentional per the dedup-by-PR design (per-package CHANGELOGs keep the full text), but worth a comment in dedupeEntries so a future reader doesn't try to "fix" it.
  • The PR description claims release.yml uses fetch-depth: 0; it doesn't. Doesn't matter because release-notes.ts only reads CHANGELOG.md, but the description is slightly off.

@aterrylu
aterrylu merged commit 49e35f8 into main Jun 24, 2026
5 checks passed
@aterrylu
aterrylu deleted the terry/changelog-merge-all-packages branch June 24, 2026 06:44
aterrylu added a commit that referenced this pull request Jun 24, 2026
…e files (#246)

Captures the rationale behind PR #244's fix to sync-changelog.ts: the consolidator
now reads every packages/*/CHANGELOG.md (not just app), dedups by PR (highest
severity wins), and renders one-line-per-PR using squash-merge commit titles.

Records the load-bearing invariant for future readers (add-a-6th-package case +
parser signature dependencies), the self-guards (empty-section hard-fail +
parsed-vs-consumed partial-drop warning), and the floor-vs-ceiling framing —
mechanical output is the correctness floor, a future friendly/themed rewrite
layer is a separate ceiling concern that doesn't change this contract.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
aterrylu added a commit that referenced this pull request Jun 24, 2026
feat(release): add /release skill for friendly release-body rewrite

A repo-committed Claude Code skill that rewrites a published GitHub Release's
mechanical, one-line-per-PR changelog body into autonomOS's friendly themed/emoji
format (the v0.3.0 style Terry approved). This is the "ceiling of friendliness"
that sits on top of #244's mechanical "floor of correctness" (ADR-044).

Why a skill, not a CI script: no ANTHROPIC_API_KEY in repo secrets (and none
wanted). The rewrite runs inside the invoking Claude Code session — Terry's
subscription, no API key, no SDK, no CI step. CI keeps publishing the mechanical
floor unchanged; `/release` enhances the live release body via `gh release edit`,
fully decoupled from the tag->build->publish path and reversible.

Load-bearing guardrail: the rewrite must preserve every PR. The skill diffs the
mechanical PR set against the friendly body's visible #NNN AND its /pull/NNN link
targets before applying — a dropped, invented, or mislinked PR hard-STOPs the edit.
This protects ADR-044's correctness floor through to the release body (the exact
1-of-21 silent-drop class #244 fixed at the floor must not reappear at the ceiling).

Files:
- .claude/skills/release/SKILL.md          — the procedure (repo's first committed skill)
- .claude/skills/release/canonical-example.md — the v0.3.0 mechanical->friendly few-shot anchor (the spec)

No package code changes; empty changeset marks intentional no-version-impact.


Claude-Session: https://claude.ai/code/session_016A1U8t4Cafxazbd5uGJZb5

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants