Skip to content

Generate release notes from merged pull requests - #986

Merged
TheGreatAxios merged 3 commits into
mainfrom
cl-7888-generate-release-notes-from-merged-pull-requests-instead-of
Sep 13, 2026
Merged

Generate release notes from merged pull requests#986
TheGreatAxios merged 3 commits into
mainfrom
cl-7888-generate-release-notes-from-merged-pull-requests-instead-of

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Collaborator

Closes CL-7888.

Summary

Cutting a release required hand-writing a ## [X.Y.Z] section in CHANGELOG.md first, and release.sh hard-failed without one (no ## [$VERSION] section in CHANGELOG.md — rename [Unreleased] first). That is a whole pull request per release whose only job is narrating the previous ones — written after the fact by whoever remembers, and free to drift from what actually merged.

release.sh now asks GitHub for the notes covering every pull request merged since the previous tag, each credited to its author. faremeter/interchange releases this way.

The runtime feature stays

This is not "delete the changelog" — it is "stop writing it by hand". src/changelog/index.ts still parses the file, /changelog still works, the post-upgrade what's-new still works, and CHANGELOG.md still ships in DOC_FILES so offline installs carry their notes. Only the source of the text changes.

Precedence: --notes <file>, then an existing hand-written section, then generated. A release that deserves narration can still have it.

Two things I got wrong first, and verified

The tag does not exist yet when notes are resolved. I flagged this as the likely awkward part on the issue. target_commitish anchors the range end at the commit being released — confirmed against the live API, which returned the full 0.3.24..HEAD PR list for an unborn v0.3.26.

Generated bodies open with their own h2s (## What's Changed, ## New Contributors). parseChangelogText ends a version section at the next line starting with ## , so writing the body in verbatim truncated the entry to its header — /changelog would have rendered an empty release. Caught by round-tripping real generated output through the actual parser, not by reading it. Headings are demoted to h3 on write.

After the fix, that round-trip gives:

top version:       0.3.26
body lines kept:   28
has PR links:      true
has author credit: true
entries:           56 (was 55)
next entry intact: 0.3.24 -> 24 lines

Also

The CHANGELOG.md preamble claimed the file is the hand-maintained source and told you to rename ## [Unreleased] at cut time. Both are now false; it describes generation instead, and marks that everything below predates it.

Note on grouping

GitHub's .github/release.yml groups by pull-request labels, not commit-message type. The Conventional Commits work in #985 makes the generated list read better but does not by itself produce grouped sections. Interchange's notes are ungrouped and read fine, so grouping is optional polish — not added here.

Testing

bash -n scripts/release.sh, changelog parser tests (32 pass), tsc --noEmit, bun run lint. The generate-notes call and the parser round-trip were both exercised against the real repo. The full release.sh run is not exercised — it tags and publishes.

@linear-code

linear-code Bot commented Sep 13, 2026

Copy link
Copy Markdown

CL-7888

Cutting a release required hand-writing a CHANGELOG.md section first, and
release.sh hard-failed without one. That is a whole pull request per
release whose only job is narrating the previous ones, written after the
fact by whoever remembers, and free to drift from what actually merged.

release.sh now asks GitHub for the notes covering every pull request since
the previous tag, each credited to its author. The tag does not exist yet
at that point in the run, so target_commitish anchors the range end at the
commit being released — verified against the live API.

The runtime keeps working unchanged. The generated body is written back as
a ## [X.Y.Z] section, so CHANGELOG.md still ships in DOC_FILES and
/changelog still reads it offline. A hand-written section still wins when
a release deserves narration, and --notes still overrides both.

Generated bodies open with their own h2s. parseChangelogText ends a
version section at the next line starting with "## ", so those are demoted
to h3 on write — without that the entry truncates to its header and
/changelog renders an empty release.

CL-7888
@TheGreatAxios
TheGreatAxios force-pushed the cl-7888-generate-release-notes-from-merged-pull-requests-instead-of branch from 38a4d51 to 9702ef0 Compare September 13, 2026 18:02
@TheGreatAxios TheGreatAxios changed the title feat(release): generate release notes from merged pull requests Generate release notes from merged pull requests Sep 13, 2026
Two bugs, both fatal to the generate-notes path.

write_changelog_section ran during preflight, so it dirtied a tracked
file before the release-commit step, which refuses to run on an unclean
tree. Every generated-notes release would have died there. And nothing
staged CHANGELOG.md anyway, so the section would never have reached main.
The write now happens in the release-commit step and is staged with the
version bump, which also puts it in DOC_FILES before the binaries build.

The insertion point matched the first `## [` of any kind, which is the
live `## [Unreleased]` heading — the new release sorted above it and
stranded its contents below every future release. It now targets the
first versioned header, and warns when Unreleased still has content,
since nothing renames it any more.

previous_tag is gone. The script fetches with --no-tags and never
refreshes tags, so deriving the previous tag locally would replay
already-released pull requests from a stale clone. GitHub derives it from
release history instead. That also removes a set -e trap: the pipeline
returned 1 on a first release and only survived because command
substitution under `||` suspends errexit.

CL-7888
@TheGreatAxios

Copy link
Copy Markdown
Collaborator Author

Audit: Approve.

Fit: generated release notes from merged PRs is the right source of truth going forward — it removes hand-written notes drift and matches how GitHub already credits authors. Scope stays tight to the release path.

Fix note: one verified dead branch fixed on this branch (b409727) — in scripts/release.sh write_changelog_section, the first=$(grep -n '^## \\[[0-9]' … | head -1 | cut …) pipeline aborts under set -euo pipefail when zero versioned headers exist, so the if [ -z "$first" ] fallback was unreachable. Appended || true to the pipeline; verified the fallback now appends and the insert-before-first path still works. Same commit also moves the release commit message and release PR title to the chore(release): form so this branch lands consistent with the Conventional Commits rule.

@TheGreatAxios
TheGreatAxios merged commit 5eed102 into main Sep 13, 2026
9 checks passed
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.

1 participant