Files
wehub-resource-sync 7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

291 lines
13 KiB
Markdown

# 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 narrative** generated by OpenRouter (`vars.AI_MODEL`, default
`moonshotai/kimi-k2-thinking`). The model receives the rendered
hand-written notes, the auto-generated PR list, every PR's title +
body (batched via one GraphQL call), and the diff between the
previous release tag and this one (filtered to drop lockfiles,
generated docs, SBOM, static assets, and binary patches; capped at
700k chars).
2. **Hand-written notes** from `docs/release_notes/<version>.md`
rendered from contributor-supplied `changelog.d/*.md` fragments by
the workflow itself at release time (see
[Release-notes flow](#-release-notes-flow-towncrier-news-fragments)
below). No manual `pdm run towncrier build` is required before
merging the bump.
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. **Approval and Publishing**
The release pipeline uses the `release` GitHub environment to gate the
publish steps. `DOCKER_USERNAME` / `DOCKER_PASSWORD` are scoped to that
environment, so any job that pushes to Docker Hub must declare
`environment: release` and therefore goes through the approval gate.
When you merge to `main` (or push a tag), the pipeline runs in this
order:
1. Security gates + CI gates run automatically.
2. `build` job runs (version pin, SBOM, Sigstore bundles), then
`provenance` job generates SLSA provenance for those artifacts.
3. **One `release` env approval prompt** in `release.yml`. Approving
unlocks all release-env jobs in the same run, which then execute
sequentially:
1. `prerelease-docker` — canonical multi-arch Docker build, cosign
sign, SBOM/SLSA attestations, push as `prerelease-v<ver>-<sha>`
and re-point the floating `:prerelease` tag.
2. `publish-docker` — retags the prerelease manifest as `:1.6.9`,
`:1.6`, `:latest` (no rebuild, digest-preserving), then re-verifies
digest + cosign + Trivy on the promoted tag.
3. `trigger-pypi` — dispatches `publish.yml` via `repository_dispatch`
(PyPI Trusted Publishing requires the publish step to run in a
top-level workflow, so this can't be a reusable workflow_call).
4. `monitor-pypi` — polls `publish.yml` for completion. The inner
polling loop times out at 40 minutes (after which the job fails);
the surrounding GH Actions `timeout-minutes` is 90 to leave a
safety margin around the poll budget.
5. `create-release` — publishes the GitHub Release with
SBOM/sig/provenance assets. Runs **last**, gated on all of the
above succeeding, so the public Release never points at missing
Docker tags or a missing PyPI version.
If any of `prerelease-docker`, `publish-docker`, or `monitor-pypi` fails,
`create-release` is skipped and no public GitHub Release is created. The
`cleanup-on-rejection` job then handles failure-mode cleanup:
- If `publish-docker` failed mid-retag (e.g., `:1.6.9` landed but
`:latest` failed), it rolls back any landed release tags BEFORE
deleting prerelease tags and cosign artifacts (deleting cosign
artifacts while release tags share the manifest digest would invalidate
release-tag signatures).
- If `publish-docker` succeeded but a later step (PyPI or
create-release) failed, `cleanup-on-rejection` does NOT fire — Docker
release tags exist and their cosign artifacts must stay. See
"Recovery from PyPI failure" below.
### Recovery from PyPI failure (atomicity hole)
The one orphan state the pipeline cannot fully clean up: `publish-docker`
succeeded, PyPI failed. At this point Docker `:1.6.9` / `:1.6` /
`:latest` exist and are signed; PyPI has nothing; no GitHub Release.
`monitor-pypi` opens a tracking issue labeled `ci-cd`. To recover:
1. Inspect the `publish.yml` workflow run, fix the underlying cause.
2. Manually re-dispatch PyPI publish. `client_payload[sha]` is REQUIRED
(publish.yml fails closed without it) and must be the full 40-hex
release commit — the commit the failed `release.yml` run built, NOT
current main HEAD (main may have moved during the approval wait).
Get it with `gh run view <release-run-id> --json headSha`:
```bash
gh api repos/LearningCircuit/local-deep-research/dispatches \
-f event_type=publish-pypi \
-F 'client_payload[tag]=v<X.Y.Z>' \
-F 'client_payload[sha]=<40-hex release commit>'
```
3. Once PyPI publishes successfully, manually create the GitHub Release
from the existing tag (the SBOM/sig/provenance artifacts are still
uploaded as workflow artifacts on the failed `release.yml` run; you
can download them and attach manually, or re-run `create-release`
manually if the run is still re-runnable in the Actions UI).
> Earlier iterations of this refactor described a single approval gate
> with a pre-approval testing window. That design required
> `DOCKER_USERNAME` / `DOCKER_PASSWORD` to be repo-level secrets so the
> canonical build could run without env approval. They are env-scoped to
> `release` instead, so the gate sits in front of the build. The
> atomicity refactor preserves this single-approval model — one click
> unlocks the whole chain, and create-release runs last so the
> "published Release with broken artifacts" failure mode is closed.
## 👥 Who Can Release
Code owners (defined in `.github/CODEOWNERS`):
- `@LearningCircuit`
- `@hashedviking`
- `@djpetti`
## 📝 Release Workflow
### For Regular Releases:
1. **Bump version** in `src/local_deep_research/__version__.py` (or
merge the auto-bump PR opened by `.github/workflows/version_check.yml`).
2. **Merge to main** → Release automatically created. The release
workflow renders fragments from `changelog.d/*.md` into
`docs/release_notes/<X.Y.Z>.md`, composes the body (AI narrative +
rendered changelog + auto PR list), and publishes the GitHub release.
3. **Approve publishing** in GitHub Actions (PyPI/Docker).
4. **Merge the cleanup PR** opened automatically by the
`cleanup-changelog` job (titled
`chore: clear changelog fragments for <X.Y.Z>`). It persists
`docs/release_notes/<X.Y.Z>.md` and removes the consumed fragments
from `changelog.d/`. Squash-merge — the diff has no review value
beyond a sanity check that the rendered notes look right.
To preview the rendered notes locally before merging the bump:
```bash
pdm run towncrier build --draft --version <X.Y.Z>
```
(`--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 plus AI summary only).
### 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"
- No inputs are required: the workflow reads the version from
`src/local_deep_research/__version__.py` at HEAD. To release an
older or different version, use Option B (push a version tag).
### Option B: Version Tags
- `git tag v0.4.3 && git push origin v0.4.3`
- Automatically creates release; the workflow uses the tag's commit
SHA (not `main` HEAD), so this is the correct path for backporting.
## 🛡️ 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, do NOT create a GitHub release through the UI as
the first recovery step — under the atomicity refactor, a manually
created GitHub release does NOT trigger `publish.yml` (it listens only
on `repository_dispatch`) and does NOT trigger `docker-publish.yml`
(workflow_call only). The downstream `release:` listeners that DO fire
(`backwards-compatibility.yml`, `sbom.yml`) are observability-only.
Recovery, in order of preference:
1. **Check workflow logs** in GitHub Actions to identify which job
failed, and use the targeted recovery for that failure mode:
- PyPI failure with Docker already promoted: see
[Recovery from PyPI failure](#recovery-from-pypi-failure-atomicity-hole) above.
- Any other failure: re-run the failed job via the Actions UI if
it's still re-runnable (typically within 30 days).
2. **Re-trigger the full pipeline** via `workflow_dispatch` if
re-running individual jobs isn't possible. Safe for digest-keyed
cosign verification — old digests remain valid because their cosign
artifacts persist; the new run produces a new digest with its own
signatures.
3. **Contact code owners** if recovery requires manual Docker Hub or
PyPI intervention.
## 📝 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
The release workflow handles the render. There is nothing to run
manually before merging the version bump.
`.github/workflows/release.yml` (`create-release` job):
1. Sparse-checks-out `changelog.d/` + `pyproject.toml`, installs
`towncrier~=24.8`.
2. Runs `towncrier build --yes --version <X.Y.Z>` against the runner's
throwaway workspace. Towncrier 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) and removes the
consumed fragments locally.
3. Reads the rendered file as input to the AI summary and as the
"hand-written notes" section of the published GitHub release body.
Persistence to `main` happens in the `cleanup-changelog` job, which
re-runs the same render against the release commit (`github.sha`) and
opens a `chore/post-release-cleanup-<X.Y.Z>` PR with the deletions and
the rendered file. Squash-merge it.
If `changelog.d/` is empty (maintenance release with no fragments), the
render step is skipped cleanly and the release proceeds with the AI
narrative + auto PR list only — no hard failure.
### 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
- [ ] Version updated in `__version__.py` (or auto-bump PR merged)
- [ ] Code owner approval received
- [ ] CI tests passing
- [ ] Merge to main completed
- [ ] Release automatically created
- [ ] PyPI/Docker publishing approved
- [ ] `chore: clear changelog fragments for <X.Y.Z>` PR merged