Main documentation ยท Source commit 73b589630e0330e5410ab3b129ea1fe1bb840373.
Publishing hoimin wheels and build provenance¶
release.yml builds GitHub Releases and can automatically publish their exact
wheels to TestPyPI and then PyPI. Each wheel carries two attestations: its
original build-time SLSA provenance and a publish attestation. Publication uploads
the validated release wheels without rebuilding or changing their contents.
Automatic publication is enabled for this repository through
PYPI_AUTO_PUBLISH=true. Leaving the variable unset or setting it to false
disables automatic dispatch while allowing GitHub Releases to continue.
The previous publish-pypi.yml manual entry point is retired.
Publication order¶
- A PR merged into
mainbuilds, validates and publishes the GitHub Release.release.ymlsigns the existing complete asset inventory and separately signs each of the three wheels with a single-subject SLSA statement. - When the repository variable
PYPI_AUTO_PUBLISHequalstrue, anactions: writejob dispatches a publication-only run ofrelease.ymlonmain. It executes no checked-out code. This explicit dispatch is necessary: PyPI rejects upload tokens frompull_request_targetevents. - The publication run downloads the public release wheels and checksums, verifies metadata/licenses and original build signatures, and converts the original bundles to PEP 740 without changing their signed statement bytes.
- The protected
testpypijob publishes the three wheels with SLSA and publish attestations. A separate read-only job downloads all three wheels and both attestations, verifies signatures and identities, pins the build source and workflow revisions to the tag commit, and compares SHA256 with GitHub bytes. - Only successful TestPyPI verification permits the protected
pypiupload. A final read-only job performs the same checks against production PyPI.
The automatic publisher dispatch is a separate Actions run. Watch both runs;
a successful GitHub Release build alone does not mean PyPI publication passed.
An empty pypi_tag retains the existing manual build-preview behavior and
never publishes to either index. PR previews never dispatch PyPI publication.
Restoring publisher configuration¶
The account owner must register a publisher on both indexes. Existing
publish-pypi.yml registrations do not authorize the new workflow.
| Field | PyPI | TestPyPI |
|---|---|---|
| GitHub owner | HayaoSuzuki |
HayaoSuzuki |
| Repository | hoimin |
hoimin |
| Workflow filename | release.yml |
release.yml |
| Environment | pypi |
testpypi |
Use an ordinary publisher for the existing hoimin project, not a pending
publisher for a new project. No API token or repository secret is required.
Both upload jobs are directly in release.yml. A reusable upload workflow
would change the OIDC job_workflow_ref used to select the publisher and is
therefore deliberately not used.
In GitHub environments,
retain the pypi and testpypi environments and restrict their deployment
branches to main. Required reviewers cause an approval pause; fully automatic
publication requires these environments to allow upload without a reviewer.
Account registration and environment protection are external settings; merging
this change does not install or enable them.
Bootstrap for a newly configured publisher¶
The repository's bootstrap is complete; see the hosted verification record.
The following procedure is for a newly configured or replacement publisher.
Keep PYPI_AUTO_PUBLISH unset until setup is complete. Choose a GitHub Release
whose original build includes the three single-wheel statements. Earlier versions
cannot be repaired by resigning their
wheels in a publication job, and existing PyPI files cannot gain attestations
retroactively. Do not use an already-published legacy version for bootstrap.
First publish the new tag only to TestPyPI:
gh workflow run release.yml --repo HayaoSuzuki/hoimin --ref main \
-f pypi_tag=vX.Y.Z -f pypi_production=false
Review the publication run: all three TestPyPI wheels and both predicates must verify. Then publish the same bytes to production through the same gate:
gh workflow run release.yml --repo HayaoSuzuki/hoimin --ref main \
-f pypi_tag=vX.Y.Z -f pypi_production=true
The already-complete TestPyPI publication is verified again instead of uploaded
again. After both index checks pass, remove the obsolete publish-pypi.yml
publishers and enable automatic publication for future merged releases:
gh variable set PYPI_AUTO_PUBLISH --repo HayaoSuzuki/hoimin --body true
The variable can also be set in repository Actions variables.
Delete it or set it to false to disable future automatic dispatches. This does
not cancel a publication already dispatched. Manual recovery explicitly opts
into a run and remains available while automatic dispatch is disabled.
Failures and recovery¶
Missing or invalid original build evidence, wrong repository/workflow/source, changed bytes, metadata/license mismatches, and malformed index evidence stop publication. An authentication failure on TestPyPI prevents any production upload. A production verification failure marks the publication run failed but cannot undo files already accepted by PyPI. The GitHub Release remains public.
Before each upload, the index is inspected. A version-level HTTP 404 permits a new upload immediately; inspection does not wait for an absent version. After upload, verification retries HTTP 404 responses with exponential backoff (10, 20, 40 seconds, then capped at 60 seconds) for a 600-second propagation budget, with at most 61 attempts. Request and verification time count against that budget; an in-flight request can finish after it expires. The entire verification job, including setup, is limited to 15 minutes. Each retry logs its index, version and remaining budget. Other HTTP errors and invalid bytes, publisher identities or attestations fail immediately. A retry never bypasses the complete inventory, wheel and signature checks.
Inspection propagates other HTTP errors; they are never interpreted as an empty index.
An existing publication is reused only if its exact three files, downloaded
bytes and both cryptographic attestations verify. Partial publication, legacy
publish-only evidence, wrong digests and duplicate predicates fail. The action
keeps skip-existing disabled; it does not silently repair partial uploads.
When no files were accepted, correct the settings and retry the same tag. When all files were accepted, rerunning verifies them and skips only the now-proven complete upload. For partial publication, inspect the files and create a new release version if necessary. Never delete files expecting to reuse filenames. Multiple valid original signatures from a build rerun are all checked and one is selected deterministically. No candidate with the matching wheel subject may fail cryptographic verification.
A workflow/code fix requires a new manual dispatch on updated main because
GitHub Re-run retains the original workflow revision. Manual recovery fetches
original build bundles from GitHub's attestation service, so it does not depend
on a seven-day Actions artifact. It neither rebuilds nor relabels wheels.
Concurrent uploads are serialized per index and version; different release
versions do not replace each other's pending uploads. A concurrent duplicate
may fail after another run publishes; recover by verifying the complete upload.
Verify a downloaded release¶
Use the pinned tooling from this repository with Python 3.14 and GitHub CLI:
uv sync --frozen --no-install-project --only-group provenance
uv run --frozen --no-sync python -m tools.pypi_provenance verify \
--index pypi --tag vX.Y.Z --commit EXPECTED_40_HEX_RELEASE_COMMIT \
--directory github-release-wheels
The directory must contain the three original GitHub wheels. The verifier
retrieves index wheels and evidence through the JSON and Integrity APIs. It
requires HayaoSuzuki/hoimin, build workflow release.yml, the expected source
and build-workflow commit, a GitHub-hosted builder, and one attestation of each
supported predicate. The expected commit comes from the reviewed release tag,
not from an untrusted statement. Use --index testpypi for TestPyPI.
The publish certificate can refer to a later publication-run revision; it is
not treated as the original build source. Provenance does not establish
reproducibility, absence of vulnerabilities, or OS code signing.
Verification scope and references¶
Local tests exercise orchestration and real recorded GitHub signature rejection. They do not establish successful OIDC token exchange or uploads under the new publisher registrations. Hosted bootstrap on 2026-10-11 JST verified v0.3.6 on both indexes, including downloaded bytes, both predicates, original build identity and five rejection cases. Automatic dispatch was enabled afterward. See the hosted verification record and its public evidence.