Releasing Vera
August 15, 2026 · View on GitHub
Vera publishes the Python distribution veralang; the installed command and
Python import package remain vera. Releases use GitHub Actions and PyPI
Trusted Publishing. No long-lived PyPI token or repository secret is involved.
The ordinary release signal is a strictly increasing [project].version merged
to main. The workflow builds and tests one wheel and one source archive,
passes those exact files to an approval-protected OIDC job, verifies their
registry hashes, and only then creates the tag and GitHub Release.
One-time maintainer setup
Complete this only after .github/workflows/release.yml exists on main.
Accounts
- Verify the maintainer email address.
- Enable two-factor authentication.
- Store current recovery codes somewhere independent of the password manager session used to perform the release.
GitHub environments
In Settings → Environments, create:
testpypi: allow deployments frommainandrelease/**; it needs no reviewer gate.pypi: allow deployments frommainonly and require maintainer approval. A single-maintainer repository must leave Prevent self-review disabled or the maintainer who triggered the release cannot approve it.
Do not add registry tokens as environment secrets. The environment names are part of the OIDC identities configured below.
Trusted Publishers
TestPyPI and PyPI are separate services and each needs its own publisher. For a project that does not exist yet, use the account-level Publishing page to add a pending GitHub publisher with these exact fields:
| Field | TestPyPI | PyPI |
|---|---|---|
| PyPI project name | veralang | veralang |
| Owner | aallan | aallan |
| Repository | vera | vera |
| Workflow name | release.yml | release.yml |
| Environment | testpypi | pypi |
A pending publisher creates the project on its first successful upload and then becomes a normal publisher. It does not reserve the name. Configure the production pending publisher close to the first production release, after the TestPyPI path has been proved.
Stage the current version on TestPyPI
The manual TestPyPI path exercises the same build, archive inspection, installed wheel smoke test, artifact handoff, attestations, and registry hash verification as production. It never creates a production tag or GitHub Release.
- Open Actions → Release → Run workflow.
- Select
main(or an allowedrelease/**branch). - Choose
testpypiand type the exact current version intoconfirm_version. - Confirm that
publish-testpypiandverify-testpypipass.
For the initial staging run, publish veralang==0.1.4. Verify installation
without asking TestPyPI to supply Vera's third-party dependencies:
python -m venv /tmp/veralang-testpypi
source /tmp/veralang-testpypi/bin/activate
python -m pip download --no-deps \
--index-url https://test.pypi.org/simple/ \
--dest /tmp/veralang-testpypi-dist \
veralang==0.1.4
python -m pip install /tmp/veralang-testpypi-dist/*.whl
vera version
TestPyPI versions are immutable too. A repeated dispatch for the same version fails before upload instead of silently skipping files.
Ordinary production release
The release-prep PR must:
- Increase the version in every location gated by
scripts/check_version_sync.pyand regenerateuv.lock. - Turn the accumulated
[Unreleased]notes into a dated## [X.Y.Z]section with at least one bullet and update the CHANGELOG compare links. - Add the release's one-line HISTORY entry and regenerate site assets.
- Reconcile
KNOWN_ISSUES.md's Bugs table with the tracker, by runningpython scripts/check_doc_counts.py --check-bug-issues. The convention is one row per openbug-labelled issue, and the check needs the GitHub API — it sendsGH_TOKENorGITHUB_TOKENwhen either is set, and is rate limited per IP when neither is, so export one before running it — so it is opt-in rather than part of the pre-commit hook: mid-cycle the two legitimately disagree, since a bug filed against an open PR's branch has an issue before it has a row. At release time they should agree — that is the point at which the file is the published list. - Pass the ordinary protected-branch CI and review process.
After merge, release.yml detects the version increase on main. It then:
- validates the version, CHANGELOG section, and absence of the version/tag;
- builds, inspects, installs, and smoke-tests exactly one universal wheel and one source distribution in a job without OIDC permission;
- stores those archives, release notes, and
SHA256SUMSas one GitHub Actions artifact; - waits for approval on the
pypienvironment; - publishes only the downloaded archives through Trusted Publishing, with digital attestations enabled;
- verifies PyPI exposes exactly those filenames and SHA-256 hashes; and
- creates
vX.Y.Zand the GitHub Release at the merge SHA, attaching the same archives and checksum manifest.
Approve the pypi deployment only after checking that the workflow SHA is the
intended release merge and the displayed version is correct.
Failure and recovery
- Before PyPI accepts files, fix the cause and rerun failed jobs. If the whole
run can no longer be resumed, production-recovery may be dispatched from
mainwith the exact version. It refuses to run if the version, tag, or GitHub Release already exists, or if package-affecting files changed after the version-bump merge. - After PyPI accepts files, do not rerun the whole workflow: the absence guard will reject it. Rerun only the failed verification or GitHub Release jobs so they continue using the retained, already-published artifact.
- If registry verification reports a filename or hash mismatch, stop. Do not create or move a tag while the registry and workflow artifacts disagree.
Immutable-release policy
PyPI does not permit replacing a distribution file for an existing version. Vera applies the same rule to every release surface:
- never move a published version tag;
- never replace release archives;
- never amend an already-released CHANGELOG section;
- yank a bad PyPI release rather than deleting or disguising it; and
- ship every post-release fix as a new patch version.
The pre-PyPI fold-in convention is retired. Once veralang==X.Y.Z exists, that
version describes one source state and one immutable set of archive hashes.