Two paired updates:
1. **`CLAUDE.md` § Releases** — rewritten to match the actual workflow now
that #436's auto-promote step lands the entries automatically.
The old text told Claude to 'Add a new `## [X.Y.Z] - YYYY-MM-DD`
block at the top of CHANGELOG.md' as the first step. That instruction
is the exact pattern that caused the v0.9.5 sparse-release-notes
incident — a hand-added sparse `[X.Y.Z]` block (one early fix
pre-staged) is what the extractor picked, ignoring everything under
`[Unreleased]` above it.
New default: write entries under `## [Unreleased]` during normal
work. The Release workflow promotes them at release time. The
formatting rules (sub-section grouping, user-perspective wording,
issue/PR refs) are preserved. The link-reference rule moves to 'don't
add it yourself' since `prepare-release.mjs` now appends it.
2. **`scripts/prepare-release.mjs`** — extended to also append a
`[X.Y.Z]: https://github.com/colbymchenry/codegraph/releases/tag/vX.Y.Z`
link reference at the end of CHANGELOG.md when promoting (idempotent
— no-op if one already exists, regardless of where in the file it
sits). This is what makes the `## [X.Y.Z]` heading text auto-link
to its release tag in GitHub's renderer; without it the heading still
renders, just unlinked. 3 new tests cover Case A append, Case B
append-when-merging, and no-double-add.
940/942 existing tests still pass (2 pre-existing skips); +3 new tests.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes the silent-sparse-release-notes failure mode that surfaced on
v0.9.5: the Release workflow used to do a literal
`extract-release-notes.mjs <version>` lookup with an `[Unreleased]`
fallback. The fallback only triggered when the `[<version>]` block
DIDN'T exist at all — and in practice maintainers sometimes had a
sparse `[<version>]` block pre-populated (e.g. one early fix
documented before the rest of the work landed). The workflow then
extracted that sparse block, ignoring the much-larger `[Unreleased]`
section above it. Result: the published v0.9.5 release notes were
missing the shared MCP daemon, the per-file staleness banner, the
Objective-C indexing, AND the Mixed iOS/RN/Expo bridging.
The fix is a new `scripts/prepare-release.mjs` step that runs at the
start of the workflow:
Case A — `[<version>]` does not yet exist:
Rename `[Unreleased]` → `[<version>] - <today>`. Add a fresh
empty `[Unreleased]` above. The common path.
Case B — `[<version>]` exists AND `[Unreleased]` has content:
Merge `[Unreleased]`'s sub-sections (### Added / ### Fixed /
### Changed / ### Removed / ### Deprecated / ### Security) into
the corresponding sub-sections of `[<version>]`. Unmatched
sub-sections are appended. Then empty `[Unreleased]`.
Case C — `[Unreleased]` is empty:
No-op. Re-runs of the workflow are safe.
After the script runs, the workflow auto-commits + pushes the
CHANGELOG.md change back to main (with a `[skip ci]` tag in the
commit body) so future runs and human eyes both see the same
on-disk truth.
9 unit tests (`__tests__/prepare-release.test.ts`) cover all three
cases, idempotency, version-source precedence, and an
extract-release-notes.mjs integration check.
Workflow comment header rewritten to reflect the new flow.
Trigger reminder going forward: bump package.json. CHANGELOG entries
can live under `[Unreleased]` — the workflow takes care of moving
them.
937/939 existing tests pass (2 pre-existing skips); +9 new tests.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>