Kai Ase Siren
Skip to content

Publishing to PyPI

How a housecast-v* tag becomes a release.

The train

.forgejo/workflows/publish.yml fires on a pushed housecast-v* tag. It checks the tag against the packaged version, runs just check, builds, then uploads. Publishing is irreversible: PyPI accepts a filename once and yanking hides rather than deletes, so both gates run before the build.

The rehearsal

workflow_dispatch takes an index input defaulting to testpypi, so a hand-run is harmless and a real upload is deliberate. A testpypi run skips the tag guard and builds whatever the ref holds, proving build, auth, and upload from a branch without spending a version. pypi retries a release by tag.

The tag guard

scripts/release_tag.py refuses a tag naming a version other than the one in housecast/__init__.py. That failure already happened in its softer form here: housecast-v0.1.3 reports 0.1.1, and a git dependency does not check metadata. Tested in housecast/tests/test_release_tag.py.

The tokens

Trusted publishing needs GitHub, GitLab, Google, or ActiveState as the identity provider, and Forgejo is none of them, so the train uses API tokens. PYPI_TOKEN and TEST_PYPI_TOKEN are repository Actions secrets written from SSM by agentic-os just sync-actions-secrets, each reaching uv through UV_PUBLISH_TOKEN rather than argv. Both recipes pass --check-url, so a rerun after a partial upload skips files already on that index.

What ships

The wheel carries both packages, the grading page's assets, three py.typed markers, and both licence files. The sdist carries the allowlist in pyproject.toml: the package, docs, README and licences.