A GitHub release and a CHANGELOG.md file solve the same problem from two different places: one lives on GitHub's own Releases page, the other lives in the repository and ships with the code. Steps and facts below are from GitHub's own documentation and the named tools' own docs, read 3 October 2026.
What a GitHub release is
A release bundles one point in a repository's history for people to download and read about. GitHub's documentation lists what it is built from: a tag such as v1.3.2 marking a commit, a title, a description, and binary assets a maintainer attaches by dragging them into the release form. If the description @mentions GitHub users, the published release adds a Contributors section with an avatar list of everyone mentioned.
A release can be saved as a draft before anything is public, or marked a pre-release for a build that is not meant to look finished. Creating one is a page in the repository: open Releases, choose Draft a new release, pick or create the tag, and fill in the title and description before publishing.
The Generate release notes button
Above the description box on that same page sits Generate release notes. Clicking it compiles notes from two sources: the pull requests merged since the previous tag, and the people who contributed them, plus a full changelog link at the bottom. A configuration file in the repository sorts those pull requests into categories by their labels and leaves chosen labels or authors out, so the quality of the notes depends on the labels a team applied before release day, not on anything written at release time.
Configuring categories with .github/release.yml
A file named release.yml (or release.yaml) in the .github directory controls what Generate release notes produces. Everything sits under one top level key, changelog, with two children: exclude, which keeps pull requests out entirely by labels or by authors, and categories, an ordered list of category objects. Each category needs a title and a labels list, and the wildcard label "*" catches the pull requests that matched no category above it. A category can also carry its own exclude.labels or exclude.authors to keep specific pull requests out of that one category without excluding them from the release notes altogether.
GitHub's own documentation page gives this complete example:
# .github/release.yml
changelog:
exclude:
labels:
- ignore-for-release
authors:
- octocat
categories:
- title: Breaking Changes ðŸ›
labels:
- Semver-Major
- breaking-change
- title: Exciting New Features 🎉
labels:
- Semver-Minor
- enhancement
- title: Other Changes
labels:
- "*"
Three categories, in the order they should read: breaking changes first, then new features, then a catch-all for whatever carries neither label. The web button and the API below both read this file, and the API also accepts a path to a configuration file kept somewhere else.
The REST endpoint that generates notes
The same feature is an API call: POST /repos/{owner}/{repo}/releases/generate-notes. It takes a required tag_name, and optional target_commitish, previous_tag_name, and configuration_file_path if the release config file lives somewhere other than .github/release.yml. The response is a name and a body, the generated title and the generated Markdown, returned without creating or saving anything.
curl -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer YOUR_TOKEN" \
https://api.github.com/repos/OWNER/REPO/releases/generate-notes \
-d '{
"tag_name": "v1.0.0",
"target_commitish": "main",
"previous_tag_name": "v0.9.2"
}'
GitHub's own command line tool wraps the same API in one flag. gh release create v1.2.3 --generate-notes creates the release and fills its title and body from the API in one step; --notes-start-tag sets the earlier tag to start from when the default is wrong.
Generating release notes from an Actions workflow
GitHub's documentation says the CLI is preinstalled on every runner GitHub hosts, and that each step using it must set GH_TOKEN to a token. With that, the same flag works inside a workflow that fires on a tag push:
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: gh release create ${{ github.ref_name }} --generate-notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Pushing a tag matching v* is the only trigger this needs. The same .github/release.yml categories apply here, because the workflow is calling the same API the web button and the CLI call.
A third party action does the same job with no shell step: softprops/action-gh-release takes a generate_release_notes input, off by default, that creates the release with a name and body built the same way; anything put in that step's own body input is placed ahead of the generated text rather than replaced by it.
CHANGELOG.md beside the releases, grouped the Keep a Changelog way
A GitHub release lives on GitHub. A CHANGELOG.md file lives in the repository, reads in a pull request diff, and survives a move off GitHub entirely. Keep a Changelog sets out one format for keeping it by hand: newest version first, a date on every version in YYYY-MM-DD order, and entries grouped under six fixed headings, Added, Changed, Deprecated, Removed, Fixed and Security, with an Unreleased section at the top holding whatever has landed since the last tagged version. The changelog generator fills those headings as you type and renders the Markdown, and the release notes template is the version written for customers rather than developers. What release notes are, and who reads them, covers the writing itself.
Of 327 public changelogs measured on 3 October 2026, the typical team keeping one posts four entries a year at a median of 84 words each, and 61% had gone 90 days without a new one. The full measurement suggests the hard part is not the format but touching the file on release day.
Generating release notes from commit messages
GitHub's own generator works from merged pull requests and their labels. A separate family of tools works from commit messages, provided the commits follow a convention. Conventional Commits is the specification most of them target: a commit message starts with a type, feat: for a new capability or fix: for a bug fix, optionally followed by a scope in parentheses, then a short description; a BREAKING CHANGE: footer, or an exclamation mark right after the type, marks a change that is not backward compatible. The specification ties those three types directly to semantic versioning: a fix is a patch release, a feat is a minor release, a breaking change is a major release.
Three named tools read commits in that shape and turn them into a changelog or a release:
- conventional-changelog is the command line tool that walks git history and writes a CHANGELOG.md from commits that follow the convention, with no publishing step of its own.
- semantic-release runs in CI on every push to a release branch, decides the next version number from the commit types since the last release, writes the release notes, tags the commit, and publishes, with no manual version number at any point.
- release-please, built by Google, works the other way: it opens and keeps updating a pull request that holds the next version's CHANGELOG.md entry and version bump, so a maintainer reviews the generated text before merging it, at which point it tags the commit and creates the GitHub release itself.
All three need commit messages that already follow the convention; none of them can read a history of ordinary, free text commit messages and recover categories that were never there.
VoteFirst beside a GitHub release
A GitHub release tells everyone watching the repository. VoteFirst's changelog is a different list for a different audience: it is built from the requests a team moved to its board's Completed column, not from commits, pull requests, tags or releases, because VoteFirst has no GitHub integration of any kind. Publishing a release emails every signed in voter whose own vote is on a request in it, naming the headings they voted for; a voter who posted or voted anonymously left no address and gets nothing.
Lite is $9 a month for one project, Pro is $39 for unlimited projects, a custom domain, the API and a team. The changelog and its widget work on both plans.
14 days, a card at signup, then $9 or $39 a month.