Publish is held. A real
cargo publish/ PyPI publish happens only on the maintainer's explicit approval. CI never publishes — the unified release pipeline builds, attests, and attaches everything to the GitHub Release automatically on a tag push, then waits at thecrates-ioandpypienvironment gates for a required-reviewer approval before either registry push.
ordvec (the Rust crate) and ordvec on PyPI (the PyO3 wheel built from
ordvec-python/) are released by pushing a vMAJOR.MINOR.PATCH tag to a
commit on main. The release workflow handles build, attestation, SLSA
provenance, Release-asset attach, and un-draft automatically; only the two
registry pushes are manual.
The unified release.yml:
- triggers on tag push (
v[0-9]*.[0-9]*.[0-9]*); a strict-SemVer guard step rejects pre-release / leading-zero / non-SemVer tags so they wake the workflow but skip every job below the gate; - runs a
require-ci-greengate confirming the per-commit CI is green onmainfor the tagged SHA —ci.yml,python.yml,fuzz.yml,codeql.yml(a successful run for that exact SHA onmain); - publishes via OIDC trusted publishing (no long-lived crates.io / PyPI tokens in the repo);
- emits GitHub SLSA build provenance (
actions/attest-build-provenance) and a SLSA-generator*.intoto.jsonlattached to the GitHub Release before the gated publishes — a failed attestation fails the release closed, so nothing ships without provenance recorded; - stages the
.crate, wheels, sdist,*.sigstore.jsonbundle, and*.intoto.jsonlprovenance on the GitHub Release while it is still a DRAFT (release-assets-draftis the sole Release-asset writer — no manual attach, which is what v0.2.0's manual step missed); - proves byte-identity in
publish-crateon both sides ofcargo publish:- pre-publish gate — downloads the SLSA-attested
.crateartifact, re-packages with--locked, andsha256-compares before minting the crates.io OIDC token. Defends against toolchain drift / deterministic- packaging regression; if they differ, fails closed before the token is minted (nothing reaches crates.io); - post-publish empirical proof — downloads the just-published
.cratefromcrates.io/api/v1/crates/ordvec/<v>/downloadandsha256-compares to the attested artifact.cargo publishruns its own internal packaging step the pre-publish gate cannot inspect; this is the only check that proves the bytes crates.io actually serves equal the SLSA- attested bytes. A mismatch fails closed, sopublish-github-releasenever un-drafts the Release (the version is then yank-only on crates.io, but the failure is loudly observable);
- pre-publish gate — downloads the SLSA-attested
- un-drafts the GitHub Release ONLY after BOTH
publish-crateANDpublish-pypisucceed (publish-github-releaseis the sole un-draft point). If either publish fails or is skipped, the Release stays DRAFT — no public Release ever exists for a version the registries refused; - pins every third-party action by commit SHA (the one mandated exception
is the SLSA reusable workflow, tag-pinned per SLSA's trust model), sets
persist-credentials: false, and defaults topermissions: contents: read.
The PyPI publish step additionally produces PEP 740 attestations via Trusted Publishing (served from PyPI's Integrity API).
- Required reviewer — each environment (
crates-io,pypi) requires maintainer (Fieldnote-Echo) approval before its publish job runs. - Deployment branches and tags — each environment's "Deployment branches
and tags" policy is set to Selected branches and tags with a single
tag pattern:
v[0-9]*.[0-9]*.[0-9]*(matching the workflow's trigger glob). The release workflow runs onrefs/tags/vX.Y.Z, NOTrefs/heads/main, so a branch-only allowlist (the old setting under the dispatch model) would deadlock the publish — the environment would refuse every tag-triggered run. The "tag must point at a commit onmain" guarantee is preserved byrequire-ci-green, which only passes if a successful push-event CI run exists for the exact SHA onmain— a SHA that exists only via a PR merge to the protected branch. Optionally, a tag ruleset (Settings → Rules → Rulesets → New tag ruleset) can be added to restrict tag creation to refs onmainas defence in depth.
These two settings are the supply-chain backstop the workflow code cannot express on its own (THREAT-SUPPLY-001 in THREAT_MODEL.md).
The crates.io and PyPI Trusted Publisher records must point at this workflow filename. Until either is updated, the corresponding gated publish fails closed at the OIDC exchange (no risk of a bad publish; just a failed run).
- crates.io →
ordvec→ Settings → Trusted Publishing → GitHub publisher:workflow = release.yml,environment = crates-io. - PyPI →
ordvec→ Publishing → GitHub publisher:workflow = release.yml,environment = pypi.
- Immutable releases is enabled, so a published release's
v*tag cannot be force-moved or deleted and its assets cannot be replaced after publication. This closes the GitHub-side mutability surface the registries already close on their end (crates.io is yank-only; PyPI burns a version on delete). mainis a protected branch — pull-request review is required and force-pushes and deletions are blocked, so the branch a release tag points to cannot be rewritten (THREAT-SUPPLY-002).
-
Land everything on
main; confirm the working tree andCargo.lockare in sync (cargo build --locked). -
Bump the version (crate
Cargo.toml, andordvec-pythonif the wheel changed) and updateCHANGELOG.md. Commit onmain. -
Confirm CI is green for current
mainHEAD.require-ci-greenchecksmainHEAD's SHA — which needs a completed, successful (notcancelled, not in-progress) run ofci.yml,python.yml,fuzz.yml, andcodeql.yml.- Do not merge another PR between the release commit and the tag push.
ci.yml/python.ymlusecancel-in-progress, so merging again movesmainHEAD and cancels the previous commit's in-flight CI. The superseded commit is no longer the release target: tag from the new HEAD once its own CI has completed green — never from, or by re-validating, the older commit. - If HEAD's own run shows
cancelled(superseded, but you have since stopped pushing), re-run that HEAD run from the Actions UI and wait for it to finish green before tagging. The SHA you re-run must be the exact SHA you publish; do not hand-clear the gate on any other commit. - Release only from a commit on
mainwith a successful push-to-main run of each gated workflow — in practice the tip the merge produced (a squash commit, a rebased tip, or a merge commit), whatever the merge strategy. An interior commit that exists in history only from a PR branch has no push-to-main run (its CI ran as apull_requeston the branch) and so is not releasable.
- Do not merge another PR between the release commit and the tag push.
-
Get the maintainer's explicit go to publish.
-
Push the version tag from
main(signed):git tag -s vX.Y.Z -m "vX.Y.Z" git push origin vX.Y.Zrelease.ymltriggers automatically. It builds the.crate, wheels, and sdist; attests them (GitHub attestation store +*.sigstore.json); generates the SLSA*.intoto.jsonl; and stages every artifact, the attestation bundle, and the provenance on the GitHub Release — as a DRAFT. It then pauses at the two registry environment gates. -
Approve the two publish environments when they pause in the Actions UI (one for
crates-io, one forpypi). The required-reviewer approval is what authorises the registry push.publish-cratefirst sha256-compares its repackaged.crateto the SLSA-attested artifact — if they diverge (toolchain drift, etc.) the job fails closed BEFORE the OIDC token is minted, so nothing reaches crates.io. Re-run / investigate.- Once both publishes succeed,
publish-github-releaseun-drafts the GitHub Release automatically. If one publish fails, the Release stays DRAFT — re-run the failed job, the un-draft then completes.
-
Verify each published artifact and its provenance:
- crates.io / docs.rs;
- PyPI (
pip download ordvec==X.Y.Zand inspect, plus check the PEP 740 attestation atGET https://pypi.org/integrity/ordvec/X.Y.Z/<file>/provenance); - the GitHub Release page (
.crate, wheels, sdist,*.sigstore.json,*.intoto.jsonlall present); gh attestation verify <file> -R Fieldnote-Echo/ordvecon a downloaded artifact;- for a coordinated release, the Zenodo deposit.
The crate publish, the PyPI wheel, and the paper's Zenodo deposit are coordinated (the paper consumes the bindings for a final cold-repro run). Do not ship one leg in isolation without the maintainer's go.