How to write release notes and changelogs from GitHub (with a template)
Configure GitHub's generated release notes with release.yml, use Keep a Changelog, compare release-please, Changesets and semantic-release, and copy a template.
The KnowWhyy team · · 8 min read
Release notes tend to go wrong in one of two ways. Either someone pastes git log v1.3.0..v1.4.0 into the release and calls it done, or the release goes out with no notes at all and support learns about the change from a customer. Both happen for the same reason: writing notes at release time, from memory, is a chore nobody owns.
The fix is to collect the raw material as you go, so release notes are an edit instead of an archaeology project. This guide covers GitHub's automatically generated release notes and how to shape them with .github/release.yml and labels, the Keep a Changelog format, the main automation tools, how to write notes people actually read, and a template.
Release notes vs changelog
They overlap, but they're for different moments.
A changelog is a file in the repository, usually CHANGELOG.md. Keep a Changelog defines it as a curated, chronologically ordered list of notable changes for each version. It's the long-term record.
Release notes are what you publish when a specific version ships: a GitHub release, an email, a product update page. They can be more selective and more friendly, and they often lead with the one or two changes people care about.
Most teams can generate both from the same source: well-labeled pull requests.
GitHub's automatically generated release notes
GitHub can draft release notes for you. According to GitHub's docs, generated notes include a list of merged pull requests, a list of contributors and a link to the full changelog between the two tags.
Ask your first question in minutes.
Create a workspace, connect a tool and choose what gets indexed. The free plan includes GitHub, Slack and Notion.
In the web UI: go to Releases, click Draft a new release, choose or create a tag, optionally pick the previous tag, then click Generate release notes above the description field. Review the output before publishing. You can save it as a draft and come back.
From the command line, the GitHub CLI does the same thing:
# Generate notes from merged PRs since the last release
gh release create v1.4.0 --generate-notes
# Add your own summary above the generated list, and keep it as a draft
gh release create v1.4.0 --generate-notes --draft \
--notes "Invoices now support multiple currencies. See the upgrade notes below."
# Compare against a specific earlier tag
gh release create v1.4.0 --generate-notes --notes-start-tag v1.2.0
With --generate-notes, the CLI also generates a title unless you pass one with --title. Notes passed with --notes are added above the generated list, which is exactly where your human-written summary belongs.
Out of the box, the generated list is a flat list of PR titles. That's better than a commit log, but it puts a breaking change next to a typo fix. You fix that with a config file.
Group changes with .github/release.yml
Create .github/release.yml in your repository to sort PRs into categories by label and to exclude noise. The configuration options are:
Key
What it does
changelog.exclude.labels
PRs with these labels never appear
changelog.exclude.authors
PRs from these users or bots never appear
changelog.categories[*].title
Heading for a category (required)
changelog.categories[*].labels
Labels that put a PR in this category; "*" catches everything not matched by an earlier category (required)
changelog.categories[*].exclude.labels
Labels that keep a PR out of this category
changelog.categories[*].exclude.authors
Authors whose PRs stay out of this category
Here's a config that maps labels onto the Keep a Changelog categories, so your GitHub releases and your CHANGELOG.md use the same vocabulary:
The "*" catch-all only collects PRs that didn't match an earlier category, so keep it last. Put breaking changes at the top, where readers look first.
Labels that make the notes sort themselves
Every new GitHub repository starts with default labels, including bug, enhancement and documentation. Add the rest of the set from the config above with gh label create:
gh label create "breaking-change" --color B60205 --description "Requires users to change something"
gh label create "security" --color D93F0B --description "Fixes a vulnerability"
gh label create "change" --color 1D76DB --description "Changes existing behavior"
gh label create "deprecation" --color FBCA04 --description "Will be removed in a future version"
gh label create "ignore-for-release" --color EDEDED --description "Leave out of release notes"
Labels only help if they're applied. Two habits keep them consistent:
Label at review time, not release time. Make "has a release label" part of your review checklist. The reviewer knows what the change does. The person cutting the release three weeks later doesn't.
Write PR titles as release note lines. Generated notes use the PR title. "Add CSV export to invoices" works as a release note. "Fix stuff from review" doesn't. A good pull request description template helps here.
The Keep a Changelog format
If you keep a CHANGELOG.md, Keep a Changelog is a widely used convention. Its guiding principles: changelogs are for humans, every version gets an entry, the latest version comes first, each version shows its release date, and you should say whether you follow Semantic Versioning.
It groups changes under six headings: Added, Changed, Deprecated, Removed, Fixed and Security. It also recommends an Unreleased section at the top, where you add entries as changes merge and then move them under a version number at release time.
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.4.0] - 2026-11-04
### Added
- Multi-currency invoices. Set a currency per customer in Settings.
### Changed
- Invoice PDFs now show tax per line instead of a single total.
### Deprecated
- The `total_tax` field in the invoices API. Use `line_items[].tax` instead.
### Fixed
- Exports no longer time out for accounts with more than 10,000 invoices.
[unreleased]: https://github.com/acme/billing/compare/v1.4.0...HEAD
[1.4.0]: https://github.com/acme/billing/compare/v1.3.0...v1.4.0
Two details from the spec are worth adopting even if you skip the rest. Use ISO dates (2026-11-04), which nobody misreads. And if a release has to be pulled, keep it in the changelog and mark it [YANKED] rather than deleting it. The spec's minimum advice: if you do nothing else, list deprecations, removals and breaking changes.
Tools that automate versions and changelogs
If you want the changelog written and the version bumped for you, three open source tools are common choices. All three were documented and available when we checked in November 2026.
Tool
Input
What it does
Best for
GitHub generated notes
PR labels
Drafts release notes from merged PRs; you click publish
On every CI run on a release branch, picks the next version, generates notes and publishes
Packages where every merge to main should ship
A few specifics worth knowing before you pick:
release-please reads Conventional Commits: fix: means a patch, feat: a minor version, and a ! such as feat!: a major version. Its README recommends squash-merging so each PR becomes one commit. It does not publish to package managers. If a merged commit message was wrong, you can add a BEGIN_COMMIT_OVERRIDE block to the merged PR's description to fix the notes.
Changesets asks contributors to run npx @changesets/cli (or pnpm changeset) and describe their change in a Markdown file. At release time, changeset version consumes those files and updates versions and changelogs. Its docs list Node.js ^22.11 || ^24 || >=26 as the requirement.
semantic-release is fully automated by design and runs in CI after every successful build on a release branch. Its default commit convention is the Angular one.
Without a token input, the action uses the built-in GITHUB_TOKEN. The README warns that releases and PRs created with that token won't trigger other workflows, so if you need CI checks to run on the release PR, pass a personal access token stored as a secret. You may also need to turn on "Allow GitHub Actions to create and approve pull requests" under repository Settings, then Actions, then General.
Commit-driven tools are only as good as your commit messages. If you go this route, writing good commit messages stops being optional.
Writing release notes people want to read
Generated lists are a draft. The difference between notes people skim and notes people read is a few minutes of editing.
Lead with what changed for the reader. One or two sentences at the top: the headline change and who it affects. Put this in --notes above the generated list.
Breaking changes first, with steps. Say what breaks, who's affected and exactly what to do. A code snippet beats a paragraph.
Write in the reader's language. "Exports no longer time out for large accounts" instead of "Refactor export job to stream rows".
Cut internal noise. Refactors, CI changes and dependency bumps with no visible effect go under a collapsed "Other changes" heading, or nowhere.
Link, don't explain everything. Each line links to its PR. Readers who want detail can click.
Credit people. GitHub's generated notes include a list of contributors. Keep that section when you edit.
Before and after, for the same change:
Before: Refactor tax calc into LineItemTax service (#1514)
After: Invoice PDFs now show tax for each line item, not just a total.
If you parse PDFs, the totals table has moved. (#1514)
A release notes template
Use this as the --notes text, or paste it above the generated list in the web UI:
## Highlights
One or two sentences on the most important change and who it's for.
## Breaking changes
- What changed, who is affected, and the exact steps to upgrade. (#PR)
## New
- User-facing feature, in the reader's words. (#PR)
## Improved
- Behavior change or performance improvement people will notice. (#PR)
## Fixed
- The symptom that's gone, not the code that changed. (#PR)
## Deprecated
- What will be removed, when, and what to use instead. (#PR)
## Security
- What was fixed and whether anyone needs to act. (#PR)
**Full changelog:** v1.3.0...v1.4.0
Delete empty sections. Keep a Changelog makes the same point: empty headings add noise, and readers assume missing sections had nothing in them.
For internal tools and services that don't have public releases, the same structure works as a weekly "what shipped" note. The weekly status report template shows how to pull it together from merged PRs and Slack.
Where KnowWhyy fits
Release notes cover what changed. People often also need to know what shipped across several repos last week, or why a change was made. KnowWhyy connects the GitHub repositories, Slack channels and Notion pages you choose. You can ask questions like "what shipped in billing last week?" in the web app and get an answer built from merged PRs, issues and threads, with a link to each source. If the indexed sources don't cover it, it says so. It doesn't write or publish release notes for you. It makes the history behind them easy to ask about. See project status questions, or start free with up to 2 members.
The short version
Generate a draft with Generate release notes or gh release create vX.Y.Z --generate-notes, then add a human-written summary on top.
Add .github/release.yml to group PRs by label, with breaking changes first and a "*" catch-all last.
Label PRs during review and write PR titles that work as release note lines.
Keep a CHANGELOG.md in Keep a Changelog format: an Unreleased section, ISO dates and the six standard headings.
Use release-please, Changesets or semantic-release if you want versions and changelogs automated. All of them depend on disciplined commits or changeset files.