Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
12 changes: 11 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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.

Expand Down