# Release Guide

## 🚀 Automated Release Process

Releases are **fully automated** end-to-end whenever a PR that bumps
`src/local_deep_research/__version__.py` is merged to `main`. No
separate tag push, manual workflow trigger, or release-page click is
required — the workflow detects the new version, runs all gates, cuts
the GitHub release, and (after one approval click) publishes to PyPI
and Docker Hub.

PRs that don't touch `__version__.py` merge normally but skip the
release pipeline (the version-check job sees the tag already exists
and short-circuits everything downstream).

## 📋 How Releases Work

### 1. **Automatic Release Creation**
- **Trigger**: Push to `main` whose `__version__.py` resolves to a tag
  that does not yet exist as a GitHub release. In practice this means
  "merge a PR that bumps `__version__.py`". Tag pushes (`v*.*.*`) and
  manual `workflow_dispatch` runs also trigger the pipeline and bypass
  the version-exists check.
- **Version**: Read from `src/local_deep_research/__version__.py` by
  the `version-check` job; the tag is `v<version>`.
- **Release body**: Composed by `.github/workflows/release.yml` from three sources:
  1. **AI TL;DR** generated by OpenRouter (`vars.AI_MODEL`).
  2. **Hand-written notes** from `docs/release_notes/<version>.md` — produced
     from contributor-supplied `changelog.d/*.md` fragments by
     `pdm run towncrier build` at release prep (see [Release-notes flow](#-release-notes-flow-towncrier-news-fragments) below).
  3. **Auto-generated PR list** from GitHub's generate-notes API,
     label-categorized.
- **No duplicates**: If a release for `v<version>` already exists, the
  `version-check` job sets `should_release=false` and every downstream
  job (security gate, CI gate, build, publish) is skipped.

### 2. **Automatic Publishing** (with approval)
- **GitHub Release** → triggers:
  - **PyPI publishing** (requires `release` environment approval)
  - **Docker publishing** (requires `release` environment approval)

## 👥 Who Can Release

Code owners (defined in `.github/CODEOWNERS`):
- `@LearningCircuit`
- `@hashedviking`
- `@djpetti`

## 📝 Release Workflow

### For Regular Releases:

1. **Render news fragments** (run from repo root):
   ```bash
   pdm run towncrier build --version <X.Y.Z> --yes
   ```
   Towncrier writes `docs/release_notes/<X.Y.Z>.md` (per the
   `{version}`-templated `filename` in `pyproject.toml`'s
   `[tool.towncrier]`) and `git rm`s the consumed `changelog.d/*.md`
   fragments. Preview first with `--draft` (writes nothing). Skip if no
   fragments exist for this release — the workflow tolerates a missing
   per-version file (warns and proceeds with auto-notes only).
2. **Bump version** in `src/local_deep_research/__version__.py`
3. **Commit both** in one go:
   ```bash
   git add src/local_deep_research/__version__.py docs/release_notes/<X.Y.Z>.md
   git commit -m "chore(release): bump to <X.Y.Z> + render news fragments"
   git push
   ```
4. **Open PR** if not on a release branch already, get approval from code owners
5. **Merge to main** → Release automatically created
6. **Approve publishing** in GitHub Actions (PyPI/Docker)

### For Hotfixes:

1. **Create hotfix branch** from main
2. **Make minimal fix**
3. **Bump patch version** (e.g., 0.4.3 → 0.4.4)
4. **Fast-track review** by code owners
5. **Merge to main** → Automatic release

## 🔧 Manual Release Options

### Option A: Manual Trigger
- Go to Actions → "Create Release" → "Run workflow"
- Specify version and prerelease flag

### Option B: Version Tags
- `git tag v0.4.3 && git push origin v0.4.3`
- Automatically creates release

## 🛡️ Branch Protection

- **Main branch** is protected
- **Required reviews** from code owners
- **No direct pushes** - only via approved PRs
- **Status checks** must pass (CI tests)

## 📦 Version Numbering

Follow [Semantic Versioning](https://semver.org/):
- **Major** (X.0.0): Breaking changes
- **Minor** (0.X.0): New features, backward compatible
- **Patch** (0.0.X): Bug fixes, backward compatible

## 🚨 Emergency Procedures

If automation fails:
1. **Manual GitHub release** still triggers PyPI/Docker
2. **Contact code owners** for assistance
3. **Check workflow logs** in GitHub Actions

## 📝 Release-notes flow (towncrier news fragments)

Hand-written release notes are assembled from per-PR fragments using
[towncrier](https://towncrier.readthedocs.io). This replaces the older
shared `docs/release_notes/<version>.md` model, which broke down at
LDR's PR throughput (multiple PRs/day racing for the same file).

### Contributor side

Each PR with user-visible behavior change drops one tiny markdown file:

```
changelog.d/<PR-number>.<category>.md
```

Categories: `breaking`, `security`, `feature`, `bugfix`, `removal`,
`misc` (canonical list lives in `[[tool.towncrier.type]]` entries in
`pyproject.toml`; the pre-commit hook reads it from there). Orphan
fragments (no PR/issue number) use `changelog.d/+<slug>.<category>.md`.
The pre-commit hook (`recommend-release-notes`) nudges contributors who
add ≥20 source lines without a fragment, and validates filenames so a
typo'd category doesn't silently vanish at render time. See
[`changelog.d/README.md`](../changelog.d/README.md) for the full
convention.

### Maintainer side (release prep)

```bash
pdm run towncrier build --version <X.Y.Z> --yes
```

What towncrier does (configured in `pyproject.toml` under
`[tool.towncrier]`):
1. Reads fragments from `changelog.d/`.
2. Writes the rendered output to `docs/release_notes/<X.Y.Z>.md` (per
   the `{version}`-templated `filename`; `single_file = false` makes
   this a per-release file rather than appending to a master CHANGELOG).
3. `git rm`s the consumed fragments (deletion staged for commit).
4. Stages the new release-notes file via `git add`.

The release workflow (`.github/workflows/release.yml`) then `cat`s
`docs/release_notes/<X.Y.Z>.md` into the published release body. If the
file is missing (maintainer forgot to run towncrier), the workflow logs
a warning and proceeds with the AI TL;DR + auto PR list only — no hard
failure, but the hand-written narrative is lost.

### Preview without committing

```bash
pdm run towncrier build --draft --version <X.Y.Z>
```

`--draft` renders to stdout without touching any files or fragments.
Useful while iterating on a fragment locally.

## 📊 Release Checklist

- [ ] News fragments rendered: `pdm run towncrier build --version <X.Y.Z> --yes`
- [ ] Version updated in `__version__.py`
- [ ] Render + bump committed together
- [ ] Changes tested in PR
- [ ] Code owner approval received
- [ ] CI tests passing
- [ ] Merge to main completed
- [ ] Release automatically created
- [ ] PyPI/Docker publishing approved
