Recipes

Trusted Publishing release pipeline for openarena: TestPyPI now, PyPI behind a repo flag

Discovered from README cross-links · today

Software & DevOps

OIDC-based GitHub Actions release workflow with build, publish

What they did

The author added `.github/workflows/release.yml` to the openarena repo, publishing the package with PyPI Trusted Publishing (OIDC, no stored secrets). The workflow triggers on `release: published` and `workflow_dispatch`, sets top-level `permissions: contents: read`, uses a `release-<ref>` concurrency group that never cancels, and puts a timeout on every job. Four jobs: (1) build — on manual runs it sets the version to `<pyproject>.dev<run_number>` with `uv version --frozen` (pyproject only, uv.lock untouched); on releases it fails if the tag minus its `v` differs from pyproject. Then `uv build`, `uvx twine check dist/*`, uploads `dist/` as an artifact and outputs `version`. (2) publish-testpypi — manual runs, pre-releases, and every release while PyPI is off; environment `testpypi`, `id-token: write`, `pypa/gh-action-pypi-publish@release/v1` pointed at the TestPyPI legacy URL. (3) publish-pypi — published non-pre-releases only when `vars.PUBLISH_PYPI == 'true'`; environment `pypi`. (4) verify — runs after whichever publish succeeded on a fresh Python 3.12 runner, retrying up to 5 minutes (30 × 10 s, `--no-cache-dir`). From TestPyPI it downloads only the openarena wheel with `--no-deps --only-binary :all:` and installs dependencies from PyPI; from PyPI it runs `pip install openarena==<v>`. It checks `openarena --help` exits 0, `import openarena` works, `importlib.metadata.version("openarena") == <v>`, and runs `openarena init` plus `openarena check` in a new folder, writing version and install command to the step summary. PyPI is deliberately off: the `testpypi` environment's `v*`-only tag rule was removed (confirmed via `gh api`: `deployment_branch_policy: null`) so a manual run from `main` can deploy. Turning PyPI on later needs a PyPI pending publisher (environment `pypi`), a `pypi` environment limited to tags `v*`, then `PUBLISH_PYPI=true`. Also in the PR: README logo and LICENSE links made absolute so they render on PyPI; a new `docs/releasing.md` page (nav after the CLI reference) covering trial builds, releases, pre-releases, and the no-reupload rule, with `docs.yml` now expecting at least 26 pages; and Harbor capped at `>=0.23,<0.25` because Harbor 0.25.0 removed `harbor.agents.model_connection`, which `openarena/harbor.py` imports — `--help` worked but `check` crashed, with 7 suite failures on 0.25 and 521 passing on 0.24. That is why verify now runs `init` and `check`. Local checks: `uv build` + `twine check` passed; ruff clean; pytest 521 passed, 1 skipped; zensical strict build clean at 26 pages; actionlint 1.7.12 with shellcheck 0.11.0 clean on release.yml, docs.yml and tests.yml. Manual-run versioning was simulated with `GITHUB_RUN_NUMBER=7` (0.2.0 → 0.2.0.dev7, PEP 440 devrelease, uv.lock untouched), and a `v0.2.1` tag against pyproject 0.2.0 correctly failed the tag check. The built wheel was installed into a fresh venv outside the repo, where `--help`, import, version, `init` and `check` all behaved.

How it went

The workflow is merged-ready and locally validated end to end: build and twine check pass, ruff and pytest are clean (521 passed, 1 skipped), the docs build strictly at 26 pages, and actionlint/shellcheck report nothing. A simulated manual run produced a correct PEP 440 dev version without touching uv.lock, and a mismatched tag failed as intended. A wheel installed into a fresh venv outside the repo ran `--help`, imported, reported the right version, and passed `init` + `check`. Post-merge steps remain: run the workflow on `main` and confirm build, publish-testpypi and verify are green with publish-pypi skipped, then publish the v0.2.0 draft release to TestPyPI.

Worth knowing

Nothing has actually been published yet — the post describes the PR and local checks, with the first real TestPyPI run listed as an after-merge step. PyPI publishing is intentionally disabled until a pending publisher, a tag-restricted `pypi` environment, and `PUBLISH_PYPI=true` are set up. The Harbor pin is a stopgap: 0.25 removed `harbor.agents.model_connection`, so porting to `harbor.agents.agent_auth_resolution` is a separate follow-up, and uv.lock still holds 0.23.0 while only the specifier changed. The `testpypi` environment no longer restricts deployments to `v*` tags, so manual runs from `main` can publish there. No version can be uploaded twice, so re-running a release needs a version bump.

Wiring

Order: build → publish-testpypi or publish-pypi → verify. The build job runs `uv version --frozen` (manual runs only), `uv build`, `uvx twine check dist/*`, uploads `dist/` via actions/upload-artifact and exposes a `version` output consumed by the publish and verify jobs. Publish jobs use `pypa/gh-action-pypi-publish@release/v1` with `id-token: write` and a GitHub environment (`testpypi` or `pypi`) that holds the Trusted Publishing binding; TestPyPI passes `repository-url: https://test.pypi.org/legacy/`. PyPI is gated by `if: vars.PUBLISH_PYPI == 'true'` plus a non-prerelease check. Verify runs on a fresh runner with Python 3.12, retries the install loop 30 times at 10 s with `--no-cache-dir`, and branches on which publish job ran to pick the index. Config touched: `.github/workflows/release.yml` (new), `docs.yml` (page-count expectation raised to 26), `pyproject.toml` (Harbor specifier `>=0.23,<0.25`), `docs/releasing.md` (new), README links. Repo settings: `testpypi` environment with the `v*` tag rule removed, `PUBLISH_PYPI` repository variable, and later a `pypi` environment restricted to `v*` tags plus a PyPI pending publisher.

Try it yourself

Add a GitHub Actions release workflow to this repo that publishes the package to TestPyPI with PyPI Trusted Publishing (OIDC, no secrets), and keeps PyPI behind a `PUBLISH_PYPI` repository variable. Requirements: trigger on `release: published` and `workflow_dispatch`; top-level `permissions: contents: read`; a `release-<ref>` concurrency group that never cancels; a timeout on every job. Jobs: (1) build — on manual runs set the version to `<pyproject>.dev<run_number>` with `uv version --frozen` (pyproject only, do not re-resolve uv.lock); on releases fail if the tag minus its `v` differs from pyproject; then `uv build`, `uvx twine check dist/*`, upload `dist/` as an artifact, output `version`. (2) publish-testpypi — manual runs, pre-releases, and every release while PyPI is off; environment `testpypi`, `id-token: write`, `pypa/gh-action-pypi-publish@release/v1` with `repository-url: https://test.pypi.org/legacy/`. (3) publish-pypi — published non-pre-releases only when `vars.PUBLISH_PYPI == 'true'`; environment `pypi`, `id-token: write`, same action. (4) verify — after whichever publish succeeded, on a fresh Python 3.12 runner, retry up to 5 minutes (30 × 10 s) with `--no-cache-dir`; from TestPyPI download only the wheel with `pip download --no-deps --only-binary :all: -i https://test.pypi.org/simple/` and install dependencies from PyPI, from PyPI run `pip install <pkg>==<v>`; assert `--help` exits 0, `import <pkg>` works, `importlib.metadata.version` matches, and run `init` + `check` in a new folder; write version and install command to the step summary. Also make README image and LICENSE links absolute so they render on PyPI, add a Releasing docs page covering trial builds, releases, pre-releases and the no-reupload rule, and pin any dependency whose new minor version breaks imports. Validate locally with `uv build`, `twine check`, ruff, pytest, a strict docs build, and actionlint with shellcheck.

Source

Tools in this stack