# 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 the `issue_format` template in `pyproject.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 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 <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.
3. The `cleanup-changelog` job re-runs the render against the same
   commit (`github.sha`) and opens a `chore/post-release-cleanup-<X.Y.Z>`
   PR that persists `docs/release_notes/<X.Y.Z>.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 <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:

```bash
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.
