Automating changelog generation with release-notes-generator, writing clear and structured release notes, and linking them to GitHub Releases and related issues so every version is easy to audit and trace.

In episode 13 we closed the gate with branch protection. Now imagine: release 1.4.0 just shipped, then a bug appears. The QA team asks "what changed in this version?" If the answer has to be recalled from memory, you're accumulating documentation debt.
This episode covers audit changelog & release notes: how release-notes-generator turns commit history into a structured changelog, how the changelog plugin writes CHANGELOG.md into the repository, and how those release notes automatically appear on GitHub Releases.
@semantic-release/release-notes-generator is the core plugin that reads all commits since the last tag, then groups them:
feat commits.fix commits.perf.docs, chore, and test are usually hidden by default.Its output is release notes text that other plugins then use — as the content of CHANGELOG.md by @semantic-release/changelog, and as the body of the GitHub Release by @semantic-release/github.
Two additional plugins are needed so the changelog is truly written into the repository: @semantic-release/changelog (writes the file) and @semantic-release/git (commits that file). Install both:
bun add -D @semantic-release/changelog @semantic-release/gitThen register both in release.config.cjs, right after release-notes-generator:
module.exports = {
branches: ['main'],
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
['@semantic-release/changelog', {
changelogFile: 'CHANGELOG.md',
}],
'@semantic-release/npm',
['@semantic-release/git', {
assets: ['CHANGELOG.md', 'package.json'],
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
}],
'@semantic-release/github',
],
};The order inside the array determines the execution order in the same phase. The npm plugin updates package.json with the new version during the prepare phase, and the git plugin placed after it commits CHANGELOG.md together with package.json in a single release commit.
Tip
The automatic commit message chore(release): 1.4.0 [skip ci] uses the
skip-ci keyword so the release commit doesn't trigger the workflow, doesn't
trigger a new release, and still passes commitlint rules. This is a safe
and widely used pattern across many teams.
# [1.4.0](https://github.com/devnull/learn-semantic-release/compare/v1.3.1...v1.4.0) (2026-08-03)
### Features
* **api:** add report export endpoint ([#88](https://github.com/devnull/learn-semantic-release/issues/88)) ([a1b2c3d](https://github.com/devnull/learn-semantic-release/commit/a1b2c3d))
* **auth:** support passkey login ([#90](https://github.com/devnull/learn-semantic-release/issues/90)) ([e4f5a6b](https://github.com/devnull/learn-semantic-release/commit/e4f5a6b))
### Bug Fixes
* **export:** fix empty date column in CSV ([#87](https://github.com/devnull/learn-semantic-release/issues/87)) ([c7d8e9f](https://github.com/devnull/learn-semantic-release/commit/c7d8e9f))
* **ui:** fix button overlap on small screens ([#91](https://github.com/devnull/learn-semantic-release/issues/91)) ([b0a1b2c](https://github.com/devnull/learn-semantic-release/commit/b0a1b2c))Notice a few important things in the output above:
compare/v1.3.1...v1.4.0 — an automatic diff link.#88 and the commit hash, both linked to GitHub.Features and Bug Fixes groups are generated purely from the feat and fix commit types.@semantic-release/github takes the generateNotes output and makes it the GitHub Release body. That means the changelog, the GitHub Release, and the v1.4.0 tag are always consistent because they all come from the same source.
Links to issues and PRs appear because release-notes-generator translates the #N pattern in commit subjects into issue references. Example: the subject fix(export): fix empty date column (#87) automatically connects to issue #87 in the release notes. This is what makes releases auditable — QA just clicks the referenced issue without opening git log.
To attach artifacts like binaries or installers to a release, add assets to the github configuration:
['@semantic-release/github', {
assets: [
{ path: 'dist/app-linux-x64.zip', label: 'Linux installer' },
{ path: 'dist/app-macos-arm64.zip', label: 'macOS installer' },
],
}],Changelog quality depends on commit message quality. A few habits that make a changelog speak better:
fix(checkout): fix zero-total price error.#N so links appear automatically.BREAKING CHANGE: footer with a migration explanation so the breaking changes section in the changelog is clear.Tip
Never hand-write CHANGELOG.md once automatic release is active —
manual edits will be overwritten on the next release. Treat the changelog
as a derived product, not a manually managed document. If you want extra
context, write it in the GitHub Release notes instead of editing the file.
Episode 14 recap:
release-notes-generator turns commit history into structured release notes.@semantic-release/changelog writes CHANGELOG.md, @semantic-release/git commits it.@semantic-release/github links release notes to GitHub Releases complete with diffs and issues.Every version now has a complete audit trail. In episode 15 we'll expand this strategy to a Multi-branch Release Strategy — supporting rc and main branches at the same time, understanding the difference between prerelease, release candidate, and stable release, and the SemVer precedence rules for -rc.N. See you there!