Guide

GitHub changelog generators, compared by what they actually read

Eight ways to get a changelog out of a repo, in one table, sorted by the thing that decides everything else: does it read your commits, your pull requests, or files somebody wrote by hand. Where I ran a tool, I say so. Where I didn’t, it’s from the tool’s own docs.

Definition

Generating a changelog from commits

A changelog generator is a tool that reads what your repo already records and writes the list of what changed in each release, so nobody has to keep CHANGELOG.md by hand. Most of them read commit messages, which only works if every commit says what kind of change it is. That’s the job of conventional commits: feat: lands under Features, fix: under Bug Fixes, and the rest is hidden or grouped. A second group reads merged pull requests and their labels through the GitHub API instead, so messy commits don’t matter but labels do. Changesets reads neither and has people write a short file per change. The other split is scope. Some tools only write the changelog. Others bump the version, tag it and publish, and the changelog comes along for the ride. Decide which of those you want before you pick one.

At a glance

The comparison table

ToolReadsWrites CHANGELOG.md?Cuts releases?Needs conventional commits?
git-cliffCommit messagesYes, with -o or --prependNo. It can print the next version, nothing moreBy default. Regex parsers otherwise
release-pleaseCommit messagesYes, in a release PRYes, when you merge the release PRYes
semantic-releaseCommit messagesNot by default. Notes go on the GitHub releaseYes, on every push that warrants oneYes (Angular preset by default)
ChangesetsChangeset files people writeYesBumps versions and publishesNo, it ignores commits
standard-version (deprecated)Commit messagesYesLocally: bump, commit, tag. No pushYes
GitHub generated release notesMerged pull requests and their labelsNo, it fills in the release descriptionNo, you create the releaseNo
github-changelog-generatorTags, issues and merged PRs from the GitHub APIYesNoNo, it sorts by labels
auto-changelogGit tags and commit historyYes, CHANGELOG.md by defaultNoNo

Input: commit messages

Tools that read commit messages

git-cliff only writes the changelog. It parses conventional commits by default, takes regex parsers if your history is something else, and renders through a template in cliff.toml. --bumped-version prints the next version, but it never tags or publishes. I ran it on a few repos, and the git-cliff guide has the real output.

release-please keeps a release pull request open with the next version and the changelog entry in it. Merge the PR and it tags and creates the GitHub release. Features, Bug Fixes, Performance Improvements and Reverts show by default. docs, chore, refactor and the rest are parsed and then hidden, and if that’s all you have, it opens no release PR at all. The release-please guide covers the setup.

semantic-release releases on every push that warrants one, no PR in between. Its changelog is @semantic-release/release-notes-generator, one of four plugins in the default list along with the commit analyzer, npm and GitHub plugins. The notes go into the body of the GitHub release. None of the four writes a CHANGELOG.md. For that you add @semantic-release/changelog, which npm describes as a plugin “to create or update a changelog file.”

standard-version did all of this locally: bump the version files, write the changelog, commit, tag. It never pushed or published. Its README now opens by saying it’s deprecated and recommends release-please, or the commit-and-tag-version fork if you can’t use GitHub Actions.

Input: pull requests and issues

Tools that read pull requests

GitHub’s automatically generated release notes are built in. When you draft a release there’s a Generate release notes button, and GitHub’s docs say the result lists the merged pull requests, the contributors, and a link to the full changelog. From the CLI it’s gh release create v1.2.3 --generate-notes. It writes the release description, not a file in your repo. To group PRs, add a .github/release.yml. This is trimmed from GitHub’s own example:

# .github/release.yml

changelog:
  exclude:
    labels:
      - ignore-for-release
  categories:
    - title: Breaking Changes 🛠
      labels:
        - Semver-Major
        - breaking-change
    - title: Other Changes
      labels:
        - "*"

Categories match on PR labels, and "*" catches whatever didn’t match an earlier category. So the quality of the notes is the quality of your PR titles and labels. Commit messages don’t enter into it.

github-changelog-generator is the Ruby gem that owns the literal name. Its README says it builds the changelog from tags, issues and merged pull requests on GitHub, split into sections by label: issues labelled bug become fixed bugs, enhancement becomes implemented enhancements. You install it with gem install github_changelog_generator or run the Docker image, and you’ll want a token in CHANGELOG_GITHUB_TOKEN, because the README warns that unauthenticated API requests run out fast. Defaults can live in a .github_changelog_generator file in the repo root.

auto-changelog is the npm one, and its README describes it as a tool “for generating a changelog from git tags and commit history.” It sits between the two groups. It runs git lograther than calling the API, needs semver tags, and lists merged pull requests and closed issues too, which works best when PRs keep the platform’s standard merge commit message. Check one default before you run it. --commit-limit defaults to 3, so each release shows at most three commits unless you pass --commit-limit false. The README’s suggested setup puts auto-changelog -p && git add CHANGELOG.md in your version script, so npm version updates the changelog inside the version commit.

Input: files you write

The one that reads neither

Changesets doesn’t read commit messages at all. Each change comes with a small markdown file in .changeset/ saying which package it touches, whether it’s a patch, minor or major, and a one-line summary. changeset version consumes those files, bumps the versions and writes CHANGELOG.md. changeset publish publishes. It’s the only tool here where the changelog line is written on purpose, for a reader, rather than salvaged from a commit. That’s the case for it in a monorepo of npm packages. The cost is one more file per PR, and someone has to remember.

What I ran

Same repo, different changelogs

release-please and git-cliff both went over the same small public repo. It has four commits: an initial commit, a feat:, a fix: made on a branch, and the merge commit GitHub wrote when that branch’s PR was merged. GitHub puts the PR title into the merge commit’s body, and the PR title was the fix.

  • release-please listed the fix twice. It reads every conventional line in a commit body as another commit, so it counted the branch commit and the merge commit.
  • git-cliff’s default config listed it once. The merge commit’s subject isn’t conventional, so it was skipped, along with the initial commit.
  • git-cliff’s built-in github template listed it twice again. That template turns conventional parsing off and pulls PR data from GitHub, so the merge commit comes back in.

None of these is a bug. They’re different answers to “what is one change.” Squash merging makes all three agree, because then one PR is one commit.

Troubleshooting

Where each one trips people up

  • release-please opens nothing. The log says No user facing commits found since ... - skipping. Everything since the last release was a hidden type. Ship a fix or feat and it comes back.
  • semantic-release ignores a breaking change. With no config it uses the Angular preset, which doesn’t understand !. A feat!: commit produces no release at all. Setting preset: "conventionalcommits" on the analyzer and the notes generator fixed it.
  • git-cliff writes an empty changelog. On a repo with messages like “Add frog search” the default config skipped all five commits, printed 5 commit(s) were skipped due to parse error(s), and exited 0. Either write conventional commits or give it your own parsers.
  • github-changelog-generator stops partway. Its README quotes the message, API rate limit exceeded for github_username., and the fix is the token above.

Choosing

Which one to use

  • You want a person to press merge on each release: release-please.
  • You want releases to just happen on push: semantic-release.
  • A monorepo of npm packages, and contributors should describe their own changes: Changesets.
  • You only want the file, with full control over how it looks: git-cliff.
  • Your commits are a mess but your PR titles are good: GitHub’s generated notes.
  • Still on standard-version: move to release-please.

Whichever you pick, the output is only as good as its input. The changelog format guide covers what a readable changelog looks like once it’s written.

Honesty

What this doesn’t fix

Every tool on this page writes a changelog for people who already use the thing. They find it because they went looking, in the repo or on the releases page. Nobody else sees it. A good changelog is still a list of changes, and a list of changes is not how anyone new hears that you shipped something.

That’s the part I built Merge & Tell for. It reads each merged pull request and drafts an announcement for each social network, and I read every one before it posts. There’s more on the changelog tool page, and the updates page is it running on this product.

The changelog is done. Nobody has heard about it yet.

You were going to merge the PR anyway.

GitHub changelog generators compared: 8 tools, one table