Release-notes news fragments
Each PR with user-visible behavior change drops one tiny markdown file in
this directory. The release workflow renders the fragments into
docs/release_notes/<X.Y.Z>.md automatically at release time, surfaces
that file in the published GitHub release body, and opens a follow-up PR
(chore/post-release-cleanup-<X.Y.Z>) that persists the rendered file
and deletes the consumed fragments. Maintainers do not need to run
towncrier by hand.
This per-PR fragment model replaces the old approach where every
contributor edited a shared docs/release_notes/<version>.md — that
broke down at LDR's PR throughput (12+ PRs/day, 25–50 PRs per release).
Naming
changelog.d/<id>.<category>.md
changelog.d/+<slug>.<category>.md # orphan (no PR/issue number)
<id>— the PR or issue number (e.g.,3768). The rendered changelog links to it via theissue_formattemplate inpyproject.toml.+<slug>— for fragments where no PR/issue number applies. The+prefix tells towncrier this is an orphan fragment.<category>— one of the[[tool.towncrier.type]]directories declared inpyproject.toml(currently:breaking,security,feature,bugfix,removal,misc).
What goes in the file
A short user-facing description of the change. One sentence is usually enough; longer prose is fine for breaking changes that need a "what to do" line. Markdown is supported.
Examples:
# changelog.d/3768.feature.md
Release notes are now prepended to the GitHub release body, with an
AI-generated TL;DR at the top.
# changelog.d/3670.breaking.md
**`llm.model` no longer auto-fills `gemma3:12b`.** Set the model
explicitly in Settings → LLM, or research will fail loudly with a
clear `ValueError`.
What does NOT go here
- Dependency bumps (the auto-generated PR list catches these).
- CI/workflow tweaks invisible to users.
- Internal refactors with no behavior change.
- Doc-only changes unless the new doc is itself the user-facing point.
If in doubt, ask: would a user want to read about this on the GitHub release page? If no, skip the fragment.
Categories
| directory | rendered as | use for |
|---|---|---|
breaking |
💥 Breaking Changes | API/CLI/config that is no longer compatible with prior releases |
security |
🔒 Security | CVE fixes, hardening users should know about |
feature |
✨ New Features | New user-facing capability |
bugfix |
🐛 Bug Fixes | User-visible bug fix |
removal |
🗑️ Removed | Removed setting, endpoint, or feature |
misc |
📝 Other Changes | Anything else worth highlighting |
Release flow (for maintainers)
The release workflow does the towncrier render itself — there's nothing to run before merging the version bump. The flow is:
- Merge the version-bump PR (or push a
v*.*.*tag). - The
create-releasejob sparse-checks-outchangelog.d/, runstowncrier build --yes --version <X.Y.Z>in a throwaway runner workspace, and uses the rendered file as input to the AI summary plus the published GitHub release body. - The
cleanup-changelogjob re-runs the render against the same commit (github.sha) and opens achore/post-release-cleanup-<X.Y.Z>PR that persistsdocs/release_notes/<X.Y.Z>.mdand deletes the consumed fragments. Squash-merge it.
Towncrier is configured under [tool.towncrier] in pyproject.toml:
single_file = false plus filename = "docs/release_notes/{version}.md"
makes each release land in its own per-version file.
Previewing locally
To see what the next release's notes will look like without touching anything (no fragment deletion, no file write):
pdm run towncrier build --draft --version <X.Y.Z>
If you do want a local dry-run that actually writes the file (e.g. to
sanity-check rendering of a tricky fragment), pass --keep so the
fragments are not deleted:
pdm run towncrier build --keep --version <X.Y.Z>
git restore afterwards undoes both the file write and any deletions.
The release workflow reads docs/release_notes/<version>.md for the
human-narrative input to the published release body.
Note for contributors: gitignore + dotted root directories
This directory's name (changelog.d/) ends in .d, which interacts
with the project's root-level .gitignore rule /*.* (deny anything
at the root containing a dot). Without an explicit re-allow, the
directory itself would be excluded — and once a parent directory is
excluded, gitignore cannot re-include child files via later negations
(see https://git-scm.com/docs/gitignore).
.gitignore already re-allows changelog.d/ explicitly. If you ever
add a new dotted-name root directory (e.g. i.18n/, docs.old/),
add a matching !yourdir/ line near the existing !changelog.d/
entry, otherwise contributors will hit a confusing "git add silently
does nothing" experience.