Render GitHub Release notes¶
notes turns one reviewed changelog section into a GitHub Release body. Run it from the tagged checkout so the
description matches the source that was published. It reads the changelog and prints Markdown to standard output; it
does not edit the changelog or create the GitHub Release.
Every release-creator command explains its choices on standard error, one line per decision, while standard output and
the files it writes carry only the data. For the example below, notes logs Release notes for 1.2.3: 1 entry in 1
category from docs/changelog.md, and > release-notes.md still captures the body alone.
A complete Markdown example¶
Suppose docs/changelog.md contains these two releases:
# Changelog
<a name="release-1-2-3"></a>
## 1.2.3 (2026-10-18)
<a name="release-1-2-3-fixed"></a>
### Fixed
- Avoid stale cache reads ([#42](https://github.com/owner/project/pull/42)) by [@alice](https://github.com/alice).
<a name="release-1-2-2"></a>
## 1.2.2 (2026-09-20)
### Fixed
- Reject invalid cache keys by [@alice](https://github.com/alice).
Render only 1.2.3:
uvx --from release-creator release-creator notes \
--changelog docs/changelog.md \
--tag 1.2.3 \
--repository owner/project \
--previous 1.2.2 \
--artifact-url https://pypi.org/project/project/1.2.3/ \
> release-notes.md
--tag selects one section. --repository builds GitHub links. --previous selects the start of the comparison;
the tags command can find it. --artifact-url adds the published package link.
release-notes.md contains the complete GitHub description:
<a name="release-1-2-3-fixed"></a>
### Bug fixes
- Avoid stale cache reads ([#42](https://github.com/owner/project/pull/42)) by @alice.
[🐍 PyPI release](https://pypi.org/project/project/1.2.3/)
[📜 Full Changelog](https://github.com/owner/project/compare/1.2.2...1.2.3)
Review that file before passing it to gh release create --notes-file release-notes.md. The by credit stays in
the changelog; the GitHub body uses an @alice mention.
Rules and failure cases¶
The tagged changelog decides which changes appear, their order, their categories, and their authors. notes does not
infer changes or people from commits, and it does not check credits or pull request links: the fragments linker
writes them, and validate-news rejects fragments that carry their own.
notes uses the first section for --tag. It renames Fixed to Bug fixes and Added to Features
while keeping each release bullet on one line; an unknown category keeps its own name as the heading. Markdown text
outside the categories passes through as written, and notes keeps empty Markdown headings, so leave them out of the
tagged changelog; the supplied Towncrier template does this. The reStructuredText reader omits empty categories and
renders blocks outside a category or release item as plain paragraphs. If the source section already has a Full
Changelog line, notes replaces it with one link built from --previous and --tag. The source’s category
anchors remain stable. See the format reference for every category name.
Omit --artifact-url if there is no external artifact. Use --artifact-label to name another ecosystem’s package
link. A project that publishes several packages repeats --artifact-url, each followed by its --artifact-label;
the links share one line, separated by commas. Each link starts with an emoji picked from its host: 🐍 for PyPI, 🧩 for
the JetBrains Marketplace, and 📦 for anything else. If --previous is absent, the final link points to the commits
at this tag instead of a comparison.
New contributors¶
During release preparation, run contributors after towncrier build and before committing the changelog:
uvx --from release-creator release-creator contributors \
--changelog docs/changelog.md --version 1.2.3 --repository owner/project
The command reads the pull requests linked in the 1.2.3 section and asks GitHub for each one’s opener and merge
time. A pull request opener is new when no merged pull request of theirs in the repository predates this one; when the
release holds several of their pull requests, only the earliest counts. Co-authors never count, and bots and AI
assistant accounts (claude, codex, CommandCodeBot, Copilot) are skipped. The command needs gh with
read access to the repository and edits only Towncrier changelogs; Knope projects do not use it.
For a shared Markdown changelog, pass the package name, as in --package pyproject-fmt. The command then edits the
### 1.2.3 section under ## pyproject-fmt, found as notes --package finds it, and the subsection gets a
#### New Contributors heading and a release-pyproject-fmt-1-2-3-new-contributors anchor, matching that package’s
categories.
When someone is new, the command appends a subsection at the end of the version section, with the same anchor and heading markup as the categories. In Markdown:
<a name="release-1-2-3-new-contributors"></a>
### New Contributors
- [@alice](https://github.com/alice) made their first contribution in [#42](https://github.com/owner/project/pull/42)
In reStructuredText, the heading uses the categories’ underline character:
.. _release-1-2-3-new-contributors:
New Contributors
----------------
- :user:`alice` made their first contribution in :pr:`42`
Entries follow merge order. With no new contributor, the changelog stays unchanged. notes renders the subsection
last in the release body, before the artifact and Full Changelog links. Pull requests that are not merged are skipped.
Run the command once per release: a second run appends another subsection.
The command logs each decision on standard error and leaves standard output empty. For the example above it logs New
Contributors: @alice (#42), or a reason such as New Contributors: none for 1.2.3 (no first-time author among its
pull requests).
reStructuredText changelogs¶
Use a .rst changelog path to select the reStructuredText reader:
uvx --from release-creator release-creator notes \
--changelog docs/changelog.rst --tag 1.2.3 \
--repository owner/project > release-notes.md
:issue: links target issues; :pr: and :pull: target pull requests. :user: becomes an @handle
mention.
Nested lists stay nested. .. code-block:: directives become fenced Markdown blocks inside their release items.
Top-level .. warning:: directives become separate warning blocks after the preceding item.