From c92b9a202929acd0d53e94176afbd547a2502d96 Mon Sep 17 00:00:00 2001 From: Michel Wilhelm Date: Fri, 11 Sep 2026 12:57:02 -0300 Subject: [PATCH] docs: record the lockfile step and the approval gate in the release flow Two things the 1.2.0 release hit that neither document mentioned. `make lock` belongs to the version bump. The package is a member of its own workspace, so uv.lock records project.version. Bumping pyproject.toml alone leaves the two disagreeing, and every `uv sync --locked` then fails: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. That includes the release workflow's own sync step, so the release fails before it builds anything. Both files now move together in step 1. The `pypi` environment has a required reviewer. The run parks at `waiting` after the artifacts are built and publishes nothing until someone approves the deployment. The previous wording ("the workflow publishes that version if CI passes and the tag matches") reads as if a green matrix were enough, which invites treating a paused release as a finished one. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 9 ++++++++- CONTRIBUTING.md | 12 +++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f84b575..1c3744a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,12 +154,19 @@ Ruff runs with its default rule set only (`E4`, `E7`, `E9`, `F`) at `line-length Releases are tag-driven and publish to PyPI via **Trusted Publishing** (GitHub OIDC). There are no API tokens. -1. Bump `project.version` in `pyproject.toml` and merge that to `main`. +1. Bump `project.version` in `pyproject.toml`, then run `make lock`. The package is a member of + its own workspace, so `uv.lock` records `project.version`, and bumping without relocking makes + every `uv sync --locked` fail, including the release workflow's own sync step. Merge both files + to `main` together. 2. Run `make ci` locally. 3. Tag with the bare version, **no `v` prefix**: `git tag 1.2.0 && git push origin 1.2.0`. `.github/workflows/production.yml` then: reads `project.version` and **fails the release if the tag does not match it exactly**; reuses `ci.yml` via `workflow_call` as a gate; builds sdist+wheel once; uploads them as an artifact; and publishes *those exact artifacts* from a separate job bound to the `pypi` GitHub environment. +That environment carries a required reviewer, so the run parks at `waiting` once the artifacts are +built and publishes nothing until someone approves the deployment. A green matrix is not a +published release: check the run's own status, not just its checks. + `.github/workflows/publish-to-test-pypi.yml` is the same shape but `workflow_dispatch`-only and bound to a `testpypi` environment. When touching workflows, keep third-party actions **pinned to commit SHAs** with the version in a trailing comment, and keep `permissions:` minimal, with `id-token: write` present only on the publishing job. Dependabot (`.github/dependabot.yml`) currently watches GitHub Actions only; Python dependencies are updated by hand via `make lock`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f5e5bf..39a73b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,6 +81,7 @@ The repository publishes from GitHub Actions using a gated release flow: - production publishing happens only from tags in the format `X.Y.Z` - the production workflow validates that the Git tag matches `project.version` - publishing uses PyPI Trusted Publishing, not long-lived API tokens +- the `pypi` environment requires a manual approval before the publish job runs - TestPyPI publishing is manual via `workflow_dispatch` - TestPyPI can also use Trusted Publishing when configured on TestPyPI @@ -95,6 +96,8 @@ make build Recommended production release flow: ```bash +# after bumping project.version in pyproject.toml +make lock make test make lint make build @@ -105,7 +108,14 @@ git push origin X.Y.Z Replace `X.Y.Z` with the value of `project.version` in `pyproject.toml`. The tag must match it exactly, with no `v` prefix, or the release workflow fails before publishing anything. -After the tag is pushed, the PyPI workflow publishes that version if CI passes and the tag matches the package version. +`make lock` is part of the bump, not an afterthought. The package is a member of its own +workspace, so `uv.lock` records `project.version`, and a lockfile that disagrees with +`pyproject.toml` fails every `uv sync --locked`, starting with the release workflow's own sync +step. + +After the tag is pushed, the PyPI workflow publishes that version if CI passes, the tag matches +the package version, and someone approves the `pypi` environment deployment. Until that approval +the run sits at `waiting`, with every check green and nothing published. If you use GitHub Releases, create the release from the existing version tag instead of using branch pushes as the release trigger.