Publish is held. A real crates.io / PyPI publish happens only after an eligible release approver explicitly approves the protected deployment. CI never publishes on its own — 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 the required-reviewer approval and 30-minute wait timer 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. The gate polls the latest exact-SHA push run every 30 seconds for at most 30 minutes: missing, queued, and in-progress runs wait; a terminal non-success fails immediately; and a movedmainHEAD fails immediately. Every draft- release and build-artifact job depends directly on this gate, so an unsettled or failed gate creates no release artifacts and no draft GitHub Release; - 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 reviewers and self-review prevention — each environment
(
crates-io,pypi) listsFieldnote-Echoandtoadkickeras required reviewers and has prevent self-review enabled. GitHub still requires one approving reviewer per deployment, but the account that triggered the deployment cannot approve it; a second listed release approver must clear the publish gate. - Wait timer — each environment has a 30-minute wait timer. No registry OIDC credential is minted until both the wait timer and the required-reviewer approval have passed.
- 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 environment 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 and GitHub repository identity. After the GitHub repo transfer, the
registry-side owner must be Project-Navi and the repository must be ordvec.
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:owner = Project-Navi,repository = ordvec,workflow = release.yml,environment = crates-io. - crates.io →
ordvec-manifest→ Settings → Trusted Publishing → GitHub publisher:owner = Project-Navi,repository = ordvec,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,owner = Project-Navi,repository = ordvec,environment = pypi, project URLhttps://pypi.org/p/ordvec. - PyPI →
ordvec-manifest→ Publishing → GitHub publisher:workflow = release.yml,owner = Project-Navi,repository = ordvec,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,ordvec-manifest-python/Cargo.toml,ordvec-manifest-python/pyproject.toml,ordvec-manifest-python/python/ordvec_manifest/__init__.py, andordvec-ffi/Cargo.toml). Also bump every internal path-dependency version:ordvec-manifest→ordvec, both Python binding aliases,ordvec-ffi→ordvec, andbenchmarks/beir-bench→ordvec. UpdateCHANGELOG.mdwith migration notes for every intentional compatibility break. Commit onmain.- Run
python tests/release_publish_invariants.pyafter the bump; it checks lockstep versions, MSRV/docs drift, registry metadata parity, Python classifier/URL parity, docs.rs feature policy, package contents, and release workflow invariants. - Downstream un-patch (one-time, 0.6.0): OrdinalDB's root workspace and
workspace-excluded standalone consumers carry
[patch.crates-io]blocks for pre-releaseordvecandordvec-manifestgit revisions. When 0.6.0 publishes, remove every one of those overrides, regenerate each independent lockfile, and prove that OrdinalDB consumes the published crates.io releases instead of a pre-release git revision.
- Run
-
Confirm CI is green for current
mainHEAD.require-ci-greenchecksmainHEAD's SHA — which needs a completed, successful (notcancelled) latest run ofci.yml,python.yml,fuzz.yml,codeql.yml,actionlint.yml, andzizmor.yml. A just-pushed tag may reach the release workflow before Actions exposes or completes those runs, so the gate waits up to 30 minutes and polls every 30 seconds. It fails immediately on a terminal non-success or ifmainadvances, and fails closed on timeout.- Routine
ci.yml/coverage.ymlruns may warn and skip SDE-dependent steps when Intel's downloadmirror challenges GitHub-hosted runners. That keeps external mirror outages from holdingmainred, but it does not make a release shippable by itself:release.ymlhas a fail-closedrelease-avx512job that installs Intel SDE, runs the AVX-512 CPUID probe, and runs the AVX-512 test lane before assets can be staged. This release proof deliberately avoids writable workflow caches in the tag workflow; if Intel's download path is unavailable, wait, rerun, or land a reviewed SDE pin/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.
- Routine
-
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 reviewers, prevent self-review, apply the 30-minute wait timer, 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 explicit maintainer agreement 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. Each job must also clear the 30-minute wait timer. Because self-review is blocked, the account that triggered the deployment cannot approve it; use the other listed release approver in the Actions UI. 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 Project-Navi/ordvecon a downloaded artifact;- compare the observed release assets against
docs/artifact-platform-matrix.md; - 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.