Publish is held. A real crates.io / 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 the
crates-ioandpypienvironment gates for a required-reviewer approval before any registry push.
ordvec (the Rust crate), ordvec-manifest (the lockstep manifest verifier
crate), ordvec on PyPI (the PyO3 wheel built from ordvec-python/), and
ordvec-manifest on PyPI (the PyO3 wheel built from
ordvec-manifest-python/) are released by pushing a vMAJOR.MINOR.PATCH tag
to current main HEAD. The release workflow handles build, canonical Python artifact selection,
attestation, SLSA provenance, Release-asset attach, and un-draft automatically;
only the registry environment approvals 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 tag points at currentmainHEAD and that per-commit CI is green onmainfor that SHA —ci.yml,python.yml,fuzz.yml,codeql.yml,actionlint.yml,zizmor.yml(a successful run for that exact SHA onmain); - publishes via OIDC trusted publishing (no long-lived crates.io / PyPI tokens in the repo) for both Rust crates and both Python distributions;
- canonicalizes each Python dist before attestation and release upload: for a new PyPI version it uses the current run's wheels/sdist; if PyPI already owns that immutable version during recovery, it downloads the exact PyPI-served files, verifies their SHA-256 digests from PyPI JSON, and uses those bytes as the GitHub Release assets;
- emits GitHub SLSA build provenance (
actions/attest-build-provenance) and SLSA-generator*.intoto.jsonlassets attached to the GitHub Release before each gated publish — a failed attestation fails the release closed, so nothing ships without provenance recorded. In recovery mode where PyPI files already exist, the initial GitHub/SLSA subjects are deliberately limited to the Rust crate built by the current run; the Python files are verified immutable PyPI bytes from the earlier Trusted Publishing upload, not falsely claimed as rebuilt by the recovery run; - stages the core
.cratefile, canonical wheels, canonical sdist,*.sigstore.jsonbundle, and*.intoto.jsonlprovenance on the GitHub Release while it is still a DRAFT (release-assets-draftowns the core/Python Release uploads, andrelease-manifest-assets-draftlater owns the manifest crate uploads; no manual attach, which is what v0.2.0's manual step missed); - proves byte-identity in
publish-crateandpublish-manifest-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/<crate>/<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
- publishes
ordvec-manifestonly after the lockstepordveccrate has published and passed its crates.io-served byte-identity readback. Cargo cannot package a fresh lockstep manifest version until the matching core crate exists on crates.io, so the workflow builds, attests, generates SLSA provenance for, and stages the manifest.crateon the draft GitHub Release afterpublish-cratesucceeds;publish-manifest-cratethen re-runscargo package -p ordvec-manifest --lockedand byte-compares that output to the attested artifact before minting its own OIDC token; - un-drafts the GitHub Release ONLY after
publish-crate,publish-manifest-crate,publish-pypi, ANDpublish-manifest-pypisucceed (publish-github-releaseis the sole un-draft point). If any 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) on a fresh upload. If the version already exists on PyPI during recovery, the job skips upload and instead verifies that PyPI-served wheel/sdist hashes match the canonical files staged on the GitHub Release.
- 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 a record 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. - crates.io →
ordvec-manifest→ Settings → Trusted Publishing → GitHub publisher:workflow = release.yml,environment = crates-io. If crates.io requires an initial owner bootstrap before a new crate's Trusted Publisher can be configured, do that explicit maintainer-approved bootstrap before tagging. - PyPI →
ordvec→ Publishing → GitHub publisher:workflow = release.yml,environment = pypi, project URLhttps://pypi.org/p/ordvec. - PyPI →
ordvec-manifest→ Publishing → GitHub publisher:workflow = release.yml,environment = pypi, project URLhttps://pypi.org/p/ordvec-manifest.
- 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). -
Review the compatibility impact against
docs/compatibility-policy.md:- classify the release as patch-compatible or minor-breaking;
- identify touched stable Rust, Python, C ABI, Go, Manifest, persisted-format, examples/docs, feature, and MSRV surfaces;
- for patch releases, run a SemVer compatibility check against the latest published crate when practical, or record why an equivalent check is not useful for this release;
- distinguish
ordvecprimitive API/file compatibility from downstream application database behavior.
-
Bump the lockstep version (
Cargo.toml,ordvec-manifest/Cargo.tomlincluding itsordvecdependency,ordvec-python/Cargo.toml,ordvec-python/pyproject.toml,ordvec-python/python/ordvec/__init__.py, andordvec-ffi/Cargo.toml) and updateCHANGELOG.mdwith migration notes for every intentional compatibility break. 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,codeql.yml,actionlint.yml, andzizmor.yml.- The
ci.ymlAVX-512 job is release-blocking and installs Intel SDE. A downloadmirror403/ outage is external infrastructure, but it still means the SHA is not releasable until that same SHA has a successfulci.ymlrun onmain. The setup action restores a SHA-verified archive cache when available; if the cache misses and Intel's download path is unavailable, wait, rerun, or land a reviewed SDE pin/cache update before tagging. - Before the final tag, spot-check
.github/actions/setup-intel-sde/action.ymlagainst Intel's SDE download page: version, Linux archive name, and SHA-256 must match the currently accepted pin. - 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.
- The
-
Run the manual release-settings audit before creating the tag:
bash tests/release_environment_settings.sh
This verifies the GitHub Environments still require the expected reviewer and accept only the stable release tag pattern. Separately verify the registry Trusted Publisher records by hand: crates.io must point both
ordvecandordvec-manifesttorelease.yml/crates-io, and PyPI must point bothordvecandordvec-manifesttorelease.yml/pypi. -
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 core.crate, wheels, and sdist for both Python packages; selects the canonical Python dists (current build for a new PyPI version, verified PyPI bytes for an existing immutable version); attests the files this run can honestly attest (GitHub attestation store +*.sigstore.json); generates SLSA*.intoto.jsonl; and stages the core and Python assets on the GitHub Release — as a DRAFT. Afterpublish-cratesucceeds, it builds, attests, generates SLSA provenance for, and stages the lockstepordvec-manifest.crate, then pauses at the manifest registry environment gate. -
Approve each publish environment pause in the Actions UI. There are four registry publish jobs:
publish-crate,publish-manifest-crate,publish-pypi, andpublish-manifest-pypi. The two crates.io jobs use the samecrates-ioenvironment and may require separate approvals; the two PyPI jobs use thepypienvironment and may also require separate approvals. Required-reviewer approval is what authorises each registry push.publish-crateandpublish-manifest-cratefirst sha256-compare their repackaged.crateto the SLSA-attested artifact — if either diverges (toolchain drift, etc.) the job fails closed BEFORE the OIDC token is minted, so nothing reaches crates.io. Re-run / investigate.- Once all registry publish jobs succeed,
publish-github-releaseun-drafts the GitHub Release automatically. If one gate fails, the Release stays DRAFT — investigate and re-run from a fixed workflow rather than approving another registry into a partial state. publish-pypiandpublish-manifest-pypieither upload their fresh canonical dist or, if PyPI already serves that version, skip upload and verify the existing files. In both modes they compare every PyPI-served wheel/sdist SHA-256 digest against the canonicaldist/files before the GitHub Release can un-draft.
-
Verify each published artifact and its provenance:
- crates.io / docs.rs for
ordvecandordvec-manifest; - PyPI (confirm the post-publish hash-verification log, optionally
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.
- crates.io / docs.rs for
The Rust crate publishes, 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.