relnote docs · GitHub Action · CHANGELOG.md · Next version · Compare

Release notes from squash-merged PR titles

If your team squash-merges pull requests, every PR is already one commit on main named after its title: feat(api): add paging (#41). With Conventional Commits PR titles, that history is enough for grouped release notes. relnote reads it offline and prints a GitHub Release body, with no labels, no API token, and no config file.

1. Squash with the PR title

In the repository's Settings → General → Pull Requests, keep Allow squash merging on and set its default commit message to Pull request title (or Pull request title and commit details). GitHub appends (#NUMBER) to the subject and adds Co-authored-by: trailers for everyone who committed on the branch.

2. Keep PR titles conventional

The squash subject is the PR title, so check the title, not each commit on the branch. One option is amannn/action-semantic-pull-request:

name: PR title
on:
  pull_request_target:
    types: [opened, edited, synchronize, reopened]
permissions:
  pull-requests: read
jobs:
  conventional-title:
    runs-on: ubuntu-latest
    steps:
      - uses: amannn/action-semantic-pull-request@v6
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Add it as a required status check. Contributors can push wip commits freely; only the title has to read fix: …, feat(cli): …, or feat!: ….

3. Generate the notes

A main after four squash merges since v1.2.0:

feat(api)!: paginate list endpoints (#41)      Ada Lovelace (+ Co-authored-by: Grace Hopper)
fix: handle empty config (#42)                 Ada Lovelace
build(deps): bump actions/checkout from 4 to 5 (#43)   dependabot[bot]
docs: fix install command (#44)                Ada Lovelace

Tag and run relnote (the bot PR is dropped by --no-bots, which the Action does by default):

relnote --next-version          # v2.0.0 (feat! is breaking)
git tag v2.0.0
relnote --no-bots
## What's changed

### Breaking

- **api:** paginate list endpoints (#41) (`7f22a1c`) — Ada Lovelace, Grace Hopper

### Fixes

- handle empty config (#42) (`c0ec41c`) — Ada Lovelace

### Other

- **docs:** fix install command (#44) (`4df3794`) — Ada Lovelace

---

Range: `v1.2.0` → `v2.0.0`.

**Full changelog:** https://github.com/you/project/compare/v1.2.0...v2.0.0

_1 commit(s) omitted (bots or secret-looking subjects)._

Output from a test repository built in relnote's CI (tests/test_squash_docs.py); your hashes differ. On GitHub, #41 in a Release body links to the PR automatically.

4. CHANGELOG.md with PR links

GitHub does not autolink #41 inside a Markdown file in the repository. From v0.3.5, --changelog writes each squash reference (#41) as a real link when origin is a github.com remote. The Release body on stdout keeps the short form.

relnote --no-bots --changelog CHANGELOG.md -q
## [v2.0.0] - 2026-10-27

### Breaking

- **api:** paginate list endpoints ([#41](https://github.com/you/project/pull/41)) (`7f22a1c`) — Ada Lovelace, Grace Hopper

### Fixes

- handle empty config ([#42](https://github.com/you/project/pull/42)) (`c0ec41c`) — Ada Lovelace

### Other

- **docs:** fix install command ([#44](https://github.com/you/project/pull/44)) (`4df3794`) — Ada Lovelace

**Full changelog:** https://github.com/you/project/compare/v1.2.0...v2.0.0

Only (#N) in parentheses is linked, which is the form GitHub's squash merge writes. Text in code spans and a bare #8 elsewhere in a subject are left as they are.

5. On every tag push

name: Release
on:
  push:
    tags: ["v*"]
permissions:
  contents: write
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - id: relnote
        uses: loki-inu/relnote@v0.3.7
      - env:
          GH_TOKEN: ${{ github.token }}
          NOTES: ${{ steps.relnote.outputs.notes }}
        run: gh release create "$GITHUB_REF_NAME" --draft --notes "$NOTES"

Pass the notes through env:, not inline ${{ }} in run:; PR titles can contain backticks. More patterns (release button gated on has-changes, monorepo matrix) are in the Action guide.

FAQ

Does relnote call the GitHub API to find the PRs?

No. It reads git log only. The PR number is already in each squash commit subject as (#41), and GitHub turns #41 into a link in a Release body. In CHANGELOG.md, where GitHub does not autolink, relnote v0.3.5+ writes [#41](https://github.com/OWNER/REPO/pull/41) when origin is a github.com remote.

What if some PRs were merged with a merge commit?

Merge commits (Merge pull request #12 from …) are skipped by default; the commits they brought in are still listed. Squash and rebase merges both leave plain commits, so mixed histories work.

Where do co-authors come from?

GitHub adds Co-authored-by: trailers to the squash commit body for everyone who committed on the PR branch. relnote reads those trailers and lists the names after the author.

How do I mark a PR as breaking?

Put ! after the type in the PR title (feat(api)!: …) or add a BREAKING CHANGE: line to the squash commit message. Either one lands under Breaking and makes --next-version bump the major.

How is this different from gh release create --generate-notes?

GitHub's generated notes list PR titles with authors and new contributors, grouped by labels you configure in .github/release.yml. relnote groups by the conventional-commit type in the title, needs no labels or API, and can also write CHANGELOG.md and suggest the next version.