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.