ADR-0002: Release & distribution — automatic CalVer, GitHub Pages, PyPI trusted publishing¶
- Status: Accepted
- Date: 2026-09-27
- Deciders: pc-1, user
- Related: ADR-0001 §1 (name/distribution), §8 (fixtures)
Context¶
ADR-0001 fixed the name parsecraft and deferred public release gates. The
project needs an automated, low-touch path from "push to main" to a distributable
artifact, a published docs site, and a PyPI release — without hand-bumped
versions or stored publishing secrets.
Decisions¶
1. Repository is public; docs publish to GitHub Pages¶
github.com/jr2804/parsecraft is public. GitHub Pages is enabled with
build_type: workflow → https://jr2804.github.io/parsecraft/. GitHub Pages is
not available for private repositories on the free plan (verified: HTTP 422 from
the Pages API), so public visibility is a hard prerequisite, not a preference.
Zensical's site_url is the project-page URL (https://jr2804.github.io/parsecraft/),
distinct from repo_url.
2. Automatic CalVer on every push to main¶
- Tag format:
YYYY.M.N— novprefix, month is not zero-padded (matches PyPI's normalized form),Nincrements within the month. - A tag is created only for a commit that has none; a re-run of an already-tagged
commit reuses the tag (
needs_tag=false) so it is idempotent. - Git tag text matches the wheel version exactly — no PEP 440 padding skew.
uv-dynamic-versioning(dunamai) derives the package version from the tag;fallback-version = "0.0.0". dunamai's default tag pattern expects avprefix, so[tool.uv-dynamic-versioning] patternis set explicitly to the CalVer form^(?P<base>\d+\.\d+\.\d+)$. Without it, bare tags are ignored and the build falls back to a local version (0.0.0.postN.dev0+<hash>) that PyPI rejects (HTTP 400).
Every push to
Amended 2026-09-28: every push to main produces a release, including docs-only pushes.main is validated by CI, but a release is
a deliberate milestone. A push whose head commit message contains
[skip release] is never tagged, published, or deployed; omit the marker to cut
a release. Development pushes carry the marker; milestone pushes omit it.
(Originally every push released — abandoned after 25 releases accumulated during
an unfinished development phase.)
3. PyPI publishing uses trusted publishing (OIDC)¶
No long-lived PyPI token is stored. The release workflow requests id-token: write
in a pypi environment; pypa/gh-action-pypi-publish exchanges it for a
short-lived credential. Requires a one-time pending-publisher registration on
PyPI: owner jr2804, repository parsecraft, workflow release.yml,
environment pypi.
skip-existing: true keeps re-runs safe when a version already exists.
4. One workflow owns the whole release¶
.github/workflows/release.yml performs, in order: compute CalVer → tag → build →
GitHub release → PyPI publish → GitHub Pages deploy. The separate docs.yml was
removed to avoid two competing Pages deployments. ci.yml remains the
quality/test gate on pull requests.
Consequences¶
- First publish reserves the PyPI name (ADR-0001 §1 consequence).
- Pages availability is coupled to repository visibility; a future move back to private would break the docs deploy.
- CalVer tags are the sole source of the package version — no version file to bump.