# 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/.md` automatically at release time, surfaces that file in the published GitHub release body, and opens a follow-up PR (`chore/post-release-cleanup-`) 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/.md` — that broke down at LDR's PR throughput (12+ PRs/day, 25–50 PRs per release). ## Naming ``` changelog.d/..md changelog.d/+..md # orphan (no PR/issue number) ``` - `` — the PR or issue number (e.g., `3768`). The rendered changelog links to it via the `issue_format` template in `pyproject.toml`. - `+` — for fragments where no PR/issue number applies. The `+` prefix tells towncrier this is an orphan fragment. - `` — one of the `[[tool.towncrier.type]]` directories declared in `pyproject.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: ```markdown # changelog.d/3768.feature.md Release notes are now prepended to the GitHub release body, with an AI-generated TL;DR at the top. ``` ```markdown # 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: 1. Merge the version-bump PR (or push a `v*.*.*` tag). 2. The `create-release` job sparse-checks-out `changelog.d/`, runs `towncrier build --yes --version ` in a throwaway runner workspace, and uses the rendered file as input to the AI summary plus the published GitHub release body. 3. The `cleanup-changelog` job re-runs the render against the same commit (`github.sha`) and opens a `chore/post-release-cleanup-` PR that persists `docs/release_notes/.md` and 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): ```bash pdm run towncrier build --draft --version ``` 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: ```bash pdm run towncrier build --keep --version ``` `git restore` afterwards undoes both the file write and any deletions. The release workflow reads `docs/release_notes/.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.