diff --git a/CLAUDE.md b/CLAUDE.md index 8c73cc7..cb2f2a2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -204,34 +204,44 @@ Released to npm and mirrored as [GitHub Releases](https://github.com/colbymchenr ### Writing changelog entries -When asked for an entry for a new version: +**Default: write entries under `## [Unreleased]`** — that's the section reserved for work landing between releases. **Don't pre-create a `## [X.Y.Z]` block** for the next release: the Release workflow's first step is `scripts/prepare-release.mjs`, which automatically promotes everything under `[Unreleased]` into a new `## [X.Y.Z] - ` block at release time (or merges into a pre-existing `[X.Y.Z]` block if one exists — but you don't need one). Pre-staging is what caused the v0.9.5 sparse-release-notes incident: a sparse `[0.9.5]` block hand-added before the rest of the work landed got picked by the extractor over the much-larger `[Unreleased]` section above it. Don't do that. -1. Add a new `## [X.Y.Z] - YYYY-MM-DD` block at the **top** of `CHANGELOG.md` (under the intro, above the previous version). -2. Group under `### Added`, `### Changed`, `### Fixed`, `### Removed`, `### Deprecated`, `### Security` — omit empty sections. -3. Write from the **user's perspective**, not the implementation's. Lead with the observable symptom or capability; mention internals only if a user needs them (e.g., to work around an existing bad install). -4. Add the link reference at the bottom: `[X.Y.Z]: https://github.com/colbymchenry/codegraph/releases/tag/vX.Y.Z`. +Formatting rules for any entry (anywhere — `[Unreleased]` or otherwise): + +1. Group under `### Added`, `### Changed`, `### Fixed`, `### Removed`, `### Deprecated`, `### Security` — omit empty sections. The promote step merges matching sub-section headings, so writing under `### Added` in `[Unreleased]` lands under `### Added` in `[X.Y.Z]`. +2. Write from the **user's perspective**, not the implementation's. Lead with the observable symptom or capability; mention internals only if a user needs them (e.g., to work around an existing bad install). +3. Issue / PR references in entries are by number (`(#403)` etc.); the GitHub renderer auto-links them in the published release notes. +4. **Don't add a `[X.Y.Z]: https://...` link reference yourself** — `prepare-release.mjs` appends it automatically when it promotes the version (idempotent: a re-run is a no-op if it already exists). ### Release flow (the user runs these) Releases are built and published by the **GitHub Actions "Release" workflow** -(`.github/workflows/release.yml`). It bundles a Node runtime per platform -(`scripts/build-bundle.sh`) and publishes both the GitHub Release and the npm -thin-installer (`scripts/pack-npm.sh`: a shim package + per-platform packages). +(`.github/workflows/release.yml`). It runs `scripts/prepare-release.mjs` to +promote `[Unreleased]` into `[]` (and auto-commit + push that +CHANGELOG change back to `main` so on-disk truth matches the published +notes), then bundles a Node runtime per platform (`scripts/build-bundle.sh`) +and publishes both the GitHub Release and the npm thin-installer +(`scripts/pack-npm.sh`: a shim package + per-platform packages). Publishing manually is **wrong** now — a plain `npm publish` ships the root package (non-bundled), which breaks anyone on Node < 22.5. -After the changelog entry is written and `package.json` is bumped: +The release cut itself is: ```bash -git add package.json package-lock.json CHANGELOG.md -git commit -m "release: X.Y.Z ()" +# CHANGELOG already has entries under [Unreleased] — no manual moves needed. +# Just bump the version: +npm version --no-git-tag-version # or edit package.json manually +git add package.json package-lock.json +git commit -m "release: X.Y.Z" git push ``` -Then trigger **Actions → Release → Run workflow** (on `main`). It reads the -version from `package.json`, builds every platform bundle on one runner, creates -the GitHub Release with notes from the matching `CHANGELOG.md` section, and -publishes to npm. Requires the `NPM_TOKEN` repo secret. +Then trigger **Actions → Release → Run workflow** (on `main`). The workflow: + +1. Runs `prepare-release.mjs ` → promotes `[Unreleased]` → `[X.Y.Z] - ` in `CHANGELOG.md`, appends the link reference, commits + pushes the move with `[skip ci]`. +2. Builds every platform bundle on one runner, generates `SHA256SUMS`. +3. Creates the GitHub Release with notes from the freshly-promoted `[X.Y.Z]` block. +4. Publishes the npm shim + per-platform packages. Requires the `NPM_TOKEN` repo secret. **Do not run `npm publish`, `git push`, or `git tag` yourself** — these are publish actions on shared state. Write the files, hand the user the commands. diff --git a/__tests__/prepare-release.test.ts b/__tests__/prepare-release.test.ts index 7e0af6c..1491f88 100644 --- a/__tests__/prepare-release.test.ts +++ b/__tests__/prepare-release.test.ts @@ -165,6 +165,40 @@ describe('prepare-release.mjs', () => { }); }); + describe('link reference', () => { + it('appends a `[version]: https://...` link reference at EOF when promoting (Case A)', () => { + dir = setup(HEADER + `## [Unreleased]\n\n### Added\n- x\n\n## [1.2.2] - 2026-01-01\n`); + run(dir); + const result = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8'); + expect(result).toContain( + '[1.2.3]: https://github.com/colbymchenry/codegraph/releases/tag/v1.2.3', + ); + }); + + it('appends a link reference when merging into an existing [version] (Case B)', () => { + dir = setup( + HEADER + `## [Unreleased]\n\n### Added\n- new\n\n## [1.2.3] - 2026-02-02\n\n### Fixed\n- prior\n`, + ); + run(dir); + const result = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8'); + expect(result).toContain( + '[1.2.3]: https://github.com/colbymchenry/codegraph/releases/tag/v1.2.3', + ); + }); + + it('does not double-add an existing link reference', () => { + const ref = '[1.2.3]: https://github.com/colbymchenry/codegraph/releases/tag/v1.2.3'; + dir = setup( + HEADER + + `## [Unreleased]\n\n### Added\n- x\n\n## [1.2.2] - 2026-01-01\n\n${ref}\n`, + ); + run(dir); + const result = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8'); + const occurrences = result.split(ref).length - 1; + expect(occurrences).toBe(1); + }); + }); + describe('extractor integration', () => { it('the resulting [version] block is what extract-release-notes.mjs would surface', () => { // Run prepare, then extract — confirm the output contains all the diff --git a/scripts/prepare-release.mjs b/scripts/prepare-release.mjs index 0d1655e..f372579 100644 --- a/scripts/prepare-release.mjs +++ b/scripts/prepare-release.mjs @@ -195,7 +195,8 @@ function main() { body: ['', ''], // two blank lines for the next round of entries }; parsed.blocks.splice(unrelIdx, 1, emptied, promoted); - writeFileSync(CHANGELOG_PATH, joinChangelog(parsed)); + const next = joinChangelog(parsed); + writeFileSync(CHANGELOG_PATH, appendLinkRef(next, version)); console.log(`prepare-release: ${version} — renamed [Unreleased] to [${version}] - ${today}`); return; } @@ -234,10 +235,33 @@ function main() { // Empty out Unreleased. unrel.body = ['', '']; - writeFileSync(CHANGELOG_PATH, joinChangelog(parsed)); + const merged_text = joinChangelog(parsed); + writeFileSync(CHANGELOG_PATH, appendLinkRef(merged_text, version)); console.log(`prepare-release: ${version} — merged ${merged} Unreleased entries into existing [${version}] block`); } +/** + * Append a `[X.Y.Z]: https://github.com/colbymchenry/codegraph/releases/tag/vX.Y.Z` + * link reference at the end of the file IF one doesn't already exist. The + * link ref is what makes `## [X.Y.Z]` heading text auto-link to its tag in + * GitHub's renderer; without it the heading still renders, just unlinked. + * + * Idempotent. The existing CHANGELOG mixes link refs scattered through the + * file and a sorted block at the bottom — we just append at the very end, + * which CommonMark accepts regardless. + */ +function appendLinkRef(text, version) { + const refLine = `[${version}]: https://github.com/colbymchenry/codegraph/releases/tag/v${version}`; + // Already there? Look for a line that EQUALS this (anywhere in the file) + // to keep idempotency robust against the scattered-vs-block layout. + const lines = text.split('\n'); + if (lines.some((l) => l.trim() === refLine)) return text; + // Append, separated by a blank line from the prior content. Preserve a + // single trailing newline at EOF. + const trailingNewline = text.endsWith('\n') ? '' : '\n'; + return text + trailingNewline + refLine + '\n'; +} + try { main(); } catch (err) {