diff --git a/README.md b/README.md index 434098f..b3bc115 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,46 @@ stays sufficient for the gate sequence): a pinned pre-commit hook locally and a pinned `secret-scan` job in CI, kept in lockstep by the `repo-audit` `secret-scanning` check. The allowlist lives in `.gitleaks.toml`. +## Quality gates & CI/CD + +This repo is a working reference for a strict Python quality bar and CI/CD; +everything below runs identically locally and in CI. The full detail is in +[docs/dev-tooling.md](docs/dev-tooling.md). + +**Quality gates** — one `check-ci` sequence, run the same way locally and in CI: + +| Gate | Tool | Proves | +| --- | --- | --- | +| format | `ruff format` | consistent formatting | +| typecheck | `pyright` (strict) | types across `src`/`tests`/`tools` | +| lint | `ruff` | lint rules | +| markdownlint | `pymarkdown` | Markdown hygiene | +| codespell | `codespell` | spelling | +| import-linter | `import-linter` | import-direction boundaries | +| dependency-hygiene | `deptry` | no unused/missing/misplaced deps | +| pip-audit | `pip-audit` | no known vulnerabilities in the locked deps | +| repo-audit | `tools/repo_audit.py` | repo identity, and that the gates' own config cannot be weakened | +| build | `uv build` + wheel smoke | the built wheel installs and both entry points run | +| test | `pytest` (+ Hypothesis) | behaviour, including property-based tests | +| coverage | `coverage.py` (branch) | coverage stays at or above the honest floor | + +Three more gates run outside `check-ci`: **gitleaks** (secret scanning), +**SonarCloud** (pull-request analysis), and **CodeQL** (code quality). + +**CI/CD**: + +- **CI** runs the same `check-ci` on every push and pull request. +- **Continuous release on merge**: every qualifying merge to `main` advances the + version (semantic-release via the Oak Semantic Release Bot) and publishes a + GitHub Release with the wheel + sdist. See [Releases](#releases). +- **Supply-chain pinning**: every Actions `uses:` is pinned to a commit SHA, with + Dependabot keeping the pins and the locked deps current. +- **Branch + tag rulesets**: PR required, required status checks, no force-push, + and `v*` tags protected. +- **Self-guarding**: `repo_audit.py` audits the gates' own configuration (the + coverage floor + branch mode, the supply-chain pins, the release workflow's + shape), so the gates cannot be quietly weakened. + ## Agentic Engineering This repo is optimised for agentic engineering. Its Practice surfaces, @@ -194,7 +234,9 @@ The bump level is computed by Commitizen with this repo's policy: stand down, and the `prevent-accidental-major` commit-msg hook rejects `type!:` / `BREAKING CHANGE` in commits so one cannot land by accident. On the rare occasion a major is warranted, a human engineer cuts it strategically, outside this repo's -automation. Releases publish to GitHub Releases only (not PyPI). +automation. Releases publish to GitHub Releases only, not PyPI — if a project +based on this template needs PyPI, see +[docs/publishing-to-pypi.md](docs/publishing-to-pypi.md). The workflow needs the `RELEASE_APP_CLIENT_ID` / `RELEASE_APP_PRIVATE_KEY` secrets and the bot added as a ruleset bypass actor — see diff --git a/docs/publishing-to-pypi.md b/docs/publishing-to-pypi.md new file mode 100644 index 0000000..50f865c --- /dev/null +++ b/docs/publishing-to-pypi.md @@ -0,0 +1,97 @@ +# Publishing to PyPI + +This template publishes to **GitHub Releases only** — it deliberately does not +publish to PyPI. If a project based on this template needs to publish to PyPI, +this guide shows how, using the current recommended approach. + +You do not need to change how the package is built: the repo already produces a +wheel + sdist via `uv build` (Hatchling backend), and the release workflow +attaches them to every GitHub Release. Publishing to PyPI is an *additional* +workflow you opt into. + +## Recommended: Trusted Publishing (OIDC, no tokens) + +[PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) lets a +GitHub Actions workflow publish using short-lived OpenID Connect credentials, so +there is no long-lived API token to leak or rotate. This is the recommended +approach. + +1. **Register the publisher on PyPI.** On your PyPI project page go to + *Publishing* and add a GitHub Actions trusted publisher: your org/repo, the + workflow filename (e.g. `publish-pypi.yml`), and an optional environment name. + For a brand-new project, use a + [pending publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/) + so the first publish also creates the project. +2. **Add the publish workflow** below. It runs when a GitHub Release is published + (which this template does on every release), builds, and uploads via + [`pypa/gh-action-pypi-publish`](https://github.com/pypa/gh-action-pypi-publish). + +```yaml +name: Publish to PyPI + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + environment: pypi # optional: gate the publish behind a protected environment + permissions: + id-token: write # OIDC token for Trusted Publishing — no password needed + steps: + - uses: actions/checkout@ # v4 + - name: Install uv + uses: astral-sh/setup-uv@ # v6 + - name: Build the distribution + run: uv build + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@ # release/v1 +``` + +Because this template already creates a GitHub Release on every qualifying merge, +the `release: published` trigger fires automatically — so a PyPI publish happens +per release once the publisher is configured. To publish only some releases, +trigger on tags matching a pattern instead, or add a manual approval via the +`environment:`. + +Keep every `uses:` pinned to a commit SHA (this repo's `audit_supply_chain` +enforces SHA pins, and Dependabot keeps them current). + +## Test against TestPyPI first + +Validate the flow against [TestPyPI](https://test.pypi.org/) before the real +index: register a trusted publisher there too, and point the publish step at it: + +```yaml + - name: Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@ # release/v1 + with: + repository-url: https://test.pypi.org/legacy/ +``` + +## Alternative: API token + +If you cannot use Trusted Publishing, create a +[PyPI API token](https://pypi.org/help/#apitoken), store it as a repository +secret (e.g. `PYPI_API_TOKEN`), and pass it to the publish step: + +```yaml + - uses: pypa/gh-action-pypi-publish@ # release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} +``` + +Prefer Trusted Publishing where you can — an API token is a long-lived secret +that must be guarded and rotated. + +## Official documentation + +- [Publishing package distribution releases using GitHub Actions](https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/) + — the canonical Python Packaging guide +- [PyPI Trusted Publishers](https://docs.pypi.org/trusted-publishers/) +- [`pypa/gh-action-pypi-publish`](https://github.com/pypa/gh-action-pypi-publish) +- [uv — building and publishing a package](https://docs.astral.sh/uv/guides/package/)