Engineering

GitHub release notes: generate them, then make them readable

6 min read

GitHub release notes are the description attached to a release on the repository’s Releases page, and GitHub can write a first draft of them for you from the pull requests merged since the previous tag. The draft is a list of pull request titles grouped by label, with a contributors line and a compare link. It is accurate, complete and written for the people who merged the pull requests. Turning it into something a user of the project can read takes a .github/release.yml file and a short edit pass, and both are described below.

Way to produce the notesWhere it runsWhat you get
“Generate release notes” buttonRelease form in the web UIPR titles since the last tag, grouped by release.yml categories
gh release create v1.4.0 --generate-notesGitHub CLI, local or CIThe same draft, created with the release in one command
POST /repos/{owner}/{repo}/releases/generate-notesREST APIThe draft as text, without creating a release, for your own pipeline
release-please, semantic-release, conventional-changelogGitHub Actions or a release jobNotes from commit messages instead of PR titles, plus version bumps and a CHANGELOG.md
Hand-writtenThe release formWhatever you type, with no link back to the pull requests

How do you automatically generate release notes on GitHub?

Open the release form, choose or create the tag, and click “Generate release notes”. GitHub finds the previous release tag, lists every pull request merged into the target branch between the two, and writes one line per pull request with its title, author and number. From the command line, gh release create <tag> --generate-notes does the same thing and publishes the release in one step; --notes-start-tag overrides which earlier tag the range starts from.

The feature works from pull requests, so a repository that merges directly to main with no pull requests gets an empty draft. Squash merges are fine, because the pull request still exists. Merge commits from a long-lived branch into main produce one line for the merging pull request, with none of the pull requests that were merged into that branch earlier. If the range looks wrong, the usual cause is a missing or mis-ordered previous tag; the generator uses the latest release on the target branch, and a tag with no release attached is ignored.

The GitHub Docs page on automatically generated release notes covers the button, and the gh release create manual covers the CLI flags.

What does release.yml change?

.github/release.yml tells the generator which pull requests to leave out and how to group the rest. Without it, every pull request lands in one list, Dependabot included. With it, pull requests are sorted into categories by label, and labelled or bot-authored noise disappears.

# .github/release.yml
changelog:
  exclude:
    labels:
      - skip-changelog
    authors:
      - dependabot
      - renovate
  categories:
    - title: Breaking changes
      labels:
        - breaking-change
    - title: New
      labels:
        - enhancement
    - title: Fixed
      labels:
        - bug
    - title: Other changes
      labels:
        - "*"

Three rules worth knowing. Categories are matched in order, so a pull request with both breaking-change and bug goes under the first heading that matches. The "*" catch-all is required if you want unlabelled pull requests to appear at all; without it they are silently dropped. And the file only affects what the generator writes, so changing it does not touch notes that already exist. Teams that already label pull requests for a changelog check in CI can reuse the same labels here.

Why do generated GitHub release notes read badly?

Because a pull request title is written for the reviewer, before the change is finished, and it describes the code. “Refactor export worker to use streaming” is a good pull request title and a useless release note, because the person reading the release wants to know that exports over 100 MB no longer time out.

The second reason is that every merged pull request gets equal weight. A one-line typo fix sits next to a new authentication flow, in whichever order they merged. The reader has to scan the whole list to find the three lines that affect them.

The third is that the draft has no opening. A list of forty lines with no sentence above it does not tell anyone whether this release is safe to take, needs a migration, or exists only to pin a dependency.

What should the edit pass change?

Keep the generated list and add to it, rather than rewriting from scratch. The list is the audit trail; the pull request numbers are how someone traces a line back to the diff. The edit pass adds the parts the generator cannot know.

  1. Write two or three sentences at the top. What this release is for, whether upgrading needs any action, and what to read first. If there is a breaking change, say so in the first line.
  2. Rewrite the lines people will care about from the reader’s side: the symptom that went away, the thing they can now do, the setting that moved. Leave the pull request number on the end of the line. How to write release notes has the full shape; bug fix release notes covers the fix lines specifically.
  3. Move anything a reader has to do into its own section, with the exact command or setting. A breaking change buried in the “Other changes” list is a support ticket in a week’s time.
  4. Delete nothing from the generated list. Collapse it under a “Full list of changes” heading if it is long, but keep it, because the project’s own contributors use it.

Done this way, GitHub release notes take about ten minutes to edit for a typical release, and the notes serve both audiences: the summary at the top for users, the list below for contributors.

Should the GitHub release be your changelog?

Only if everyone who needs to know about a change already reads the Releases page. For a library consumed by developers through a package manager, that is often true, and the Releases page plus a CHANGELOG.md in the repository is enough. For a product with users who never open GitHub, it is not: the release page is written at the wrong level, lives behind a developer-facing URL, and cannot be subscribed to by someone without a GitHub account.

The pattern that holds up is to treat the GitHub release as the developer-facing record and the product changelog as the reader-facing one, with the release linking to the changelog entry. Git tags, releases and the changelog covers how the three records line up. If the product changelog is generated from the same commits, a changelog generator gives a draft for that side too, and the changelog vs release notes split explains which detail belongs where.

How do you version the release notes themselves?

A published release on GitHub can be edited at any time, and the edit leaves no visible history on the page. For most projects that is fine. For projects where the notes are a record that compliance or customers rely on, two habits help: keep the canonical text in the repository (the CHANGELOG.md, or a docs/releases/ folder) and have the release body point to it, and enable immutable releases on the repository if your plan offers it, which locks the tag and assets once published. The GitHub Docs on managing releases describe drafts, pre-releases and the “latest” label, which is assigned by semantic version order unless you set it by hand.

FAQ

Can GitHub release notes be generated automatically? Yes. The “Generate release notes” button on the release form, the --generate-notes flag on gh release create, and the generate-notes REST endpoint all produce a draft from the pull requests merged since the previous release tag. A .github/release.yml file controls grouping and exclusions.

Why are my generated release notes empty? The generator lists pull requests, so direct commits to the branch do not appear, and the range starts at the latest release on the target branch rather than the latest tag. Check that the previous release exists and that the changes came in through pull requests.

Does release.yml apply to releases that already exist? No. It only shapes notes generated after the file is in place. Existing release bodies stay as they were until someone edits them.

Should I use GitHub release notes or a CHANGELOG.md? Both, if the project has contributors and users. The release page is where developers look and where assets live; the file in the repository survives a move away from GitHub and can be read offline. Generate the release notes, keep the file current, and link one to the other.


The technical claims in this article have not been independently reviewed. If something here is wrong, tell us and we will correct it.

Related on changeloop: Changelog generator, Developer docs

changeloop
The team building a closed-loop changelog. Your users ask, your team ships, the person who asked gets told.