Skip to content

Releasing rustmc

The checklist for cutting a release: which versions must agree, what to verify before tagging, and how the two packages are published. It is for maintainers only.

rustmc_core on crates.io and rustmc on PyPI share one version. The internal python_bindings crate is also named rustmc, but it has publish = false: that name on crates.io belongs to an unrelated project.

Before publishing, synchronize the package versions in rust_core/Cargo.toml, python_bindings/Cargo.toml, and pyproject.toml; the binding's rustmc_core path and version dependency; and both workspace package entries in Cargo.lock. Update the changelog and crate README dependency example. Check the release tag:

python3 scripts/verify_version.py v0.13.0
cargo metadata --locked --offline --no-deps --format-version 1
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --release
cargo package -p rustmc_core --locked

The version verifier also runs without a tag on every CI change. It supports Python 3.9+ without tomllib or third-party packages. Its narrow parser reads the repository's literal version fields; Cargo checks full manifest/lock validity. Explicit malformed tags and mismatched dependency/lock versions fail validation.

Both registries publish from CI when the matching vX.Y.Z tag is pushed; neither is published by hand. The tag triggers .github/workflows/ci.yml, whose release path requires Rust tests/package verification, version checks (including the tag), and source/wheel installs across CPython 3.9–3.14. Each release wheel is then built and installed on a matching native platform; a clean temporary environment runs the API smoke check and full Python tests against that exact wheel before upload. The source archive is likewise installed and tested before upload. Pull requests run these same native artifact checks before merge. Publishing remains restricted to matching release tags.

Once every check passes, three jobs run, in this order, each only if the one before it succeeded:

  1. publish-crate packages and builds rustmc_core once more with cargo package, then uploads that crate to crates.io with cargo publish --no-verify, so dependency build scripts never run with the registry token in their environment. If that version is already on crates.io — because a maintainer ran cargo publish -p rustmc_core --locked by hand from the tagged revision — the job skips the upload and succeeds, so the PyPI upload still follows.
  2. publish uploads the verified wheels and source archive to PyPI, so a Python release never goes out without its Rust crate.
  3. docs deploys the documentation site from the tag (.github/workflows/docs.yml), so the site describes the version that was just released.

Registry releases are immutable, so inspect package contents and test the final revision before tagging. If a later job fails after publish-crate succeeded, fix the cause and re-run only that job; re-running publish-crate fails because the version already exists on crates.io. publish skips files PyPI already holds, so it can be re-run after a partial upload. See Cargo's publishing guide.

Release builds use CPython 3.11 and retain the cp39-abi3 compatibility tag. Native Linux ARM and Intel/ARM macOS jobs use standard GitHub-hosted runner labels; see GitHub's runner reference. The earlier Linux matrix separately verifies the advertised Python compatibility floor. Test commands can also be run locally after building exactly one artifact:

python scripts/test_release_artifact.py wheel --dist-dir dist
python scripts/test_release_artifact.py sdist --dist-dir dist

One-time repository setup

The workflow names two GitHub environments. A maintainer creates them under Settings → Environments before the first tagged release, since a job that names a missing environment creates it without protection:

  • crates-io: add required reviewers, restrict deployments to v* tags, and add a CARGO_REGISTRY_TOKEN environment secret holding a crates.io API token scoped to publish-update for rustmc_core only. publish-crate reads nothing else.
  • pypi: restrict deployments to v* tags. publish uses PyPI trusted publishing through pypa/gh-action-pypi-publish, with job-scoped id-token: write and no API token. Add pypi as the environment in the project's trusted-publisher entry on PyPI so uploads are accepted only from this job; see PyPI's trusted publishing guide.

The required reviewers on crates-io are the release's single approval: publish and docs run only after publish-crate, so pushing a tag publishes nothing, the site included, until a reviewer approves. Do not also add required reviewers to pypi. A second approval that is declined or times out after the crate uploaded leaves the version on crates.io only, which is the split this workflow exists to prevent. The third-party actions on the release path (PyO3/maturin-action, pypa/gh-action-pypi-publish, and dtolnay/rust-toolchain in the source-archive and crate jobs) are pinned to full commit SHAs; update a pin by resolving the new tag to its commit, not by editing the version comment alone.

Pushes to main and pull requests build the documentation site without deploying it. To redeploy the site, run the Docs workflow by hand and choose the release tag; for any ref that is not a v* tag the deploy job is skipped, so the run builds the site and finishes without deploying it.

A green build is not itself evidence that publishing succeeded: verify both registry versions and install the published distribution before reporting the release complete.