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.

Shared changelog with separate packages

When packages have separate histories in one Markdown file, name the package section explicitly. For example, toml-fmt has ## pyproject-fmt above ### 2.30.4 (date) and #### Fixed. Its Git tag is pyproject-fmt/2.30.4:

uvx --from release-creator release-creator notes \
  --changelog CHANGELOG.md --package pyproject-fmt \
  --tag pyproject-fmt/2.30.4 --repository tox-dev/toml-fmt \
  --previous pyproject-fmt/2.30.3 > release-notes.md

The package selector prevents a matching version in another package’s history from entering the output. It does not change the repository used for GitHub links.

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.