Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Versioning & Releases

PurRDF ships to three registries — the crates.io crate suite, the PyPI purrdf package, and the npm @blackcatinformatics/purrdf package — from one workspace version, in lockstep. The full process is docs/RELEASE.md.

Semver policy from 1.0.0

From 1.0.0 the suite follows semantic versioning in full:

  • a breaking change bumps the major version. A commit carrying ! or a BREAKING CHANGE: footer is a major-bump trigger, and the changelog marks each such entry BREAKING;
  • a minor bump is additive and API-compatible;
  • a patch bump is bugfix-only.

That is what the version number commits to; it is a policy statement, not a claim of stability beyond what semver means. All three published surfaces share one workspace version and are released together, and a version-coherence check in CI fails the build if the version sources (Cargo.toml, pyproject.toml, package.json, CITATION.cff) disagree.

The one exception is the C ABI. libpurrdf’s purrdf.h carries its own PURRDF_ABI_MAJOR.PURRDF_ABI_MINOR (currently 0.7), bumped on every exported-signature change, pinned by crates/rdf-capi/tests/abi_signatures.rs, and read back at runtime through purrdf_abi_version. It is versioned separately from the workspace and stays 0.x: it is not frozen, and the workspace’s 1.0.0 makes no promise about it.

MSRV policy

The supported minimum Rust is rust-version in the root Cargo.toml — currently 1.96 — on the stable channel, enforced by a dedicated CI MSRV job that sets RUSTUP_TOOLCHAIN explicitly and asserts the compiler it measured really is 1.96. Raising the MSRV is a notable change recorded in the changelog; it rides a minor bump and never ships in a patch release.

The MSRV is a promise to consumers; the development toolchain is a tool choice, and the two are orthogonal. rust-toolchain.toml pins a dated nightly for local work and the CI gates, because nightly clippy and rustdoc carry lints stable lacks — but the source is nightly-free by policy (zero #![feature(...)] attributes, which the MSRV job proves on every change), and the release lanes build every published artifact on stable.

Tag-driven trusted publishing

Releases are tag-driven: rust-v<version> publishes the crate suite to crates.io, py-v<version> publishes to PyPI, and npm-v<version> publishes the wasm package to npm. The lanes share the supply-chain posture of the cargo lane:

  • publication uses Trusted Publishing through GitHub Actions OIDC — no long-lived registry secret;
  • the privileged publish jobs use pinned actions and no dependency cache;
  • every .crate package receives a GitHub build-provenance attestation;
  • the package set receives an SPDX SBOM and SBOM attestation;
  • the release crate set is checked on wasm32-unknown-unknown before publishing;
  • every workspace crate version must match the tag version.

Every version of every crate — all 21 — is published by that lane and nothing else. Each existing crate record is locked on crates.io with Require trusted publishing (trustpub_only), so an API token cannot publish a new version of any of them: crates.io answers with 403 Forbidden: New versions of this crate can only be published using Trusted Publishing. A token has exactly one role left, creating the record of a brand-new crate — the one thing a Trusted Publishing token is refused (Trusted Publishing tokens do not support creating new crates) — and the release process document above is exact about how that bootstrap works: the lane publishes up to the first crate that depends on a new one and stops cleanly, the token creates the new crate’s record, Trusted Publishing is enabled on it, and the same run is resumed.

Four workspace members are deliberately never published to crates.io: purrdf-capi (built via cargo-c, distributed as libpurrdf), purrdf-sparql-conformance (the test harness), purrdf-cli (the purrdf binary), and purrdf-python (the extension crate, which ships to PyPI via maturin instead).

Cutting a release

The coherent flow from main uses the make helpers so the three lanes can never drift:

# 1. Bump all three version sources in lockstep (fails unless they end up equal).
make bump VERSION=0.2.2

# 2. Regenerate the committed C-ABI header from the bumped crate version.
make capi-header

# 3. Regenerate the changelog from the conventional-commit history.
make changelog

# 4. Review, then commit the release bump, generated header, and changelog.
git add -A && git commit -m "chore(release): 0.2.2"

# 5. From an up-to-date main, run every release gate, then push all three tags.
make release-tags VERSION=0.2.2

make release-tags refuses to run unless the working tree is clean, the branch is main and synchronized with origin/main, the version check passes, VERSION matches the tree, the release-notes section exists, and none of the three tags already exists locally or remotely. It then runs the Rust and wasm workspace gate, the generated C-ABI/header check, the native Python binding suite, and the optimized size-gated npm/wasm package tests. Only after every surface passes does it recheck the clean synchronized state and atomically push the rust-v, py-v, and npm-v tags together. No tag is created before the complete cross-surface preflight passes. Each tag triggers its own lane, and the cargo lane additionally publishes a GitHub Release built from the committed CHANGELOG.md.

Citing PurRDF

Releases carry a DOI; if you use PurRDF in research, please cite it — see CITATION.cff in the repository.