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:
publish-cratepackages and buildsrustmc_coreonce more withcargo package, then uploads that crate to crates.io withcargo 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 rancargo publish -p rustmc_core --lockedby hand from the tagged revision — the job skips the upload and succeeds, so the PyPI upload still follows.publishuploads the verified wheels and source archive to PyPI, so a Python release never goes out without its Rust crate.docsdeploys 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 tov*tags, and add aCARGO_REGISTRY_TOKENenvironment secret holding a crates.io API token scoped topublish-updateforrustmc_coreonly.publish-cratereads nothing else.pypi: restrict deployments tov*tags.publishuses PyPI trusted publishing throughpypa/gh-action-pypi-publish, with job-scopedid-token: writeand no API token. Addpypias 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.