Copperlace Release Process

This runbook describes how to publish a Copperlace release across GitHub Release assets, PyPI, Maven Central, and npm.

For artifact contents and wrapper API details, see Packaging and language wrappers.

Release Prerequisites

Before tagging a release, confirm that:

  • the release commit is on main;

  • the working tree is clean, except for unrelated local files that will not be committed;

  • the package version is final and is not already published to PyPI, Maven Central, or npm;

  • the Package Check workflow passes for the release commit;

  • crates.io ownership is available for copperlace;

  • GitHub release publishing is available for the repository;

  • a PyPI API token is available for manual publication;

  • Maven Central credentials and GPG signing configuration are available for manual publication;

  • the npm package owner has permission to publish copperlace.

The release workflow builds and smoke-tests release artifacts, then attaches them to the GitHub Release. Registry publication to crates.io, PyPI, Maven Central, and npm is manual after the generated packages have been checked.

Prepare the Version

Copperlace uses one lockstep package version for every released surface. Do not publish a release where the Rust, Python, Java, JS/TS, CLI, and GitHub Release artifact versions differ.

The committed package metadata is:

  • rust-core/Cargo.toml

  • python/pyproject.toml

  • java/pom.xml

  • Java module parent versions under java/api/, java/all-platform/, and java/native/

js/pkg/package.json is generated by wasm-pack and must not be committed. The generated version should come from rust-core/Cargo.toml.

Run the version check before tagging. It verifies all committed Copperlace package metadata uses the same version:

make release-check

The release tag must be the package version prefixed with v, such as v0.1.1.

Validate Before Tagging

Run the full local check when the local toolchain is available:

make check

For package-specific validation, build the current-platform packages:

make package

The multi-platform Package Check GitHub Actions workflow is the release gate for first-class platform artifacts. It builds and smoke-tests:

  • Python wheels for Linux x86_64/aarch64, macOS x86_64/aarch64, and Windows x86_64;

  • one Python source distribution;

  • Java API and native JARs for the same first-class native platforms;

  • CLI archives for the same first-class native platforms;

  • the JS/TS WebAssembly package tarball.

Do not push the release tag until the package-check workflow is green on the commit being released.

Tag and Publish Automated Artifacts

Create and push the release tag from the release commit:

VERSION=$(python scripts/check_versions.py)
git tag "v${VERSION}"
git push origin "v${VERSION}"

Pushing a v* tag starts .github/workflows/release.yml. The workflow:

  • verifies the tag matches the Cargo, Python, and Java package version;

  • builds and smoke-tests Python wheels and the source distribution;

  • builds and smoke-tests Java API, all-platform, and platform native JARs;

  • builds and smoke-tests platform CLI archives;

  • builds, packs, and smoke-tests the JS/TS WebAssembly tarball;

  • generates SHA256SUMS;

  • creates or updates the GitHub Release for the tag.

Wait for the full release workflow to finish before publishing registry packages from its artifacts or from the release tag.

Publish Rust to crates.io

Publish the Rust crate from the release tag after the release commit has passed checks and before or after pushing the GitHub release tag. The crate version is immutable once accepted by crates.io.

VERSION=$(python scripts/check_versions.py)
git switch --detach "v${VERSION}"
cargo fmt --check --manifest-path rust-core/Cargo.toml
cargo test --manifest-path rust-core/Cargo.toml
cargo clippy --manifest-path rust-core/Cargo.toml --all-targets -- -D warnings
RUSTDOCFLAGS=-Dwarnings cargo doc --manifest-path rust-core/Cargo.toml --no-deps
cargo package --list --manifest-path rust-core/Cargo.toml
cargo publish --dry-run --manifest-path rust-core/Cargo.toml
cargo publish --manifest-path rust-core/Cargo.toml

cargo package --list includes Cargo-generated files such as .cargo_vcs_info.json and Cargo.toml.orig; those are expected. Do not publish if the dry run warns about missing package metadata or fails verification.

Return to the normal branch after publishing:

git switch main

Publish Python to PyPI

PyPI publication is manual. For Linux x86_64 publication, build the wheel inside a PyPA manylinux container, repair it with auditwheel, smoke-test the repaired wheel, and upload only the repaired manylinux wheel.

From the repository root:

rm -rf python/build python/dist python/*.egg-info

podman run --rm -v "$PWD:/io" quay.io/pypa/manylinux2014_x86_64 /bin/bash -lc '
  set -eux
  curl https://sh.rustup.rs -sSf | sh -s -- -y
  . "$HOME/.cargo/env"

  cd /io/python
  rm -rf build dist *.egg-info

  /opt/python/cp310-cp310/bin/python -m pip install build auditwheel
  /opt/python/cp310-cp310/bin/python -m build --wheel --outdir dist/raw
  auditwheel repair dist/raw/*.whl --wheel-dir dist
'

python3 -m twine check python/dist/*manylinux*.whl
python3 scripts/smoke_python_wheel.py python/dist/*manylinux*.whl
python3 -m twine upload --verbose python/dist/*manylinux*.whl

Do not upload raw Linux wheels such as copperlace-<version>-py3-none-linux_x86_64.whl; PyPI rejects the linux_x86_64 platform tag. If auditwheel reports too-recent versioned symbols, the wheel was built on a host that is too new. Rebuild inside the manylinux container instead of repairing the host-built wheel.

On SELinux hosts, Podman may need a relabeled volume mount:

podman run --rm -v "$PWD:/io:Z" quay.io/pypa/manylinux2014_x86_64 /bin/bash -lc '...'

Publish Java to Maven Central

Maven Central publication is manual. Use the Java artifacts from the GitHub release workflow, collect the native libraries into java/native-artifacts/, then deploy with the all-platform and Central publishing profiles.

VERSION=$(python scripts/check_versions.py)
RELEASE_RUN_ID=<release-workflow-run-id>
gh run download "${RELEASE_RUN_ID}" --name 'java-jars-linux-x86_64' --dir java-release-artifacts
gh run download "${RELEASE_RUN_ID}" --name 'java-jars-linux-aarch64' --dir java-release-artifacts
gh run download "${RELEASE_RUN_ID}" --name 'java-jars-macos-x86_64' --dir java-release-artifacts
gh run download "${RELEASE_RUN_ID}" --name 'java-jars-macos-aarch64' --dir java-release-artifacts
gh run download "${RELEASE_RUN_ID}" --name 'java-jars-windows-x86_64' --dir java-release-artifacts

python scripts/collect_java_native_artifacts.py \
  --output-dir java/native-artifacts \
  java-release-artifacts

cd java
mvn -q -Pall-platform,central-publish \
  -Dcopperlace.skip.current.native=true \
  -Dgpg.passphrase="${GPG_PASSPHRASE}" \
  deploy

The Maven command expects Central credentials in Maven settings under server id central and local GPG signing material available to maven-gpg-plugin.

Publish JS/TS to npm

After the release workflow succeeds, publish the same generated JS/TS package version to npm.

Build the package locally from the release tag:

git switch --detach "v${VERSION}"
make js-package
cp LICENSE js/pkg/LICENSE
npm pack ./js/pkg --pack-destination /tmp
python scripts/smoke_js_package.py /tmp/copperlace-${VERSION}.tgz

Confirm the local npm client is authenticated as a package owner before publishing. If npm whoami fails, log in and verify again:

npm whoami
npm login
npm whoami

Inspect what npm will publish:

npm publish ./js/pkg --dry-run

Publish from the generated package directory:

npm publish ./js/pkg

npm versions are immutable. If npm publication fails after any other package registry has accepted the release version, fix the npm issue without changing the existing release version unless a registry has permanently rejected that version.

Return to the normal branch after publishing:

git switch main

Post-Release Verification

Verify the GitHub Release contains:

  • one CLI archive for each first-class native platform;

  • all Python wheel files plus the source distribution;

  • Java API, all-platform, and platform native JARs;

  • the JS/TS WebAssembly package tarball;

  • SHA256SUMS.

Install-smoke each published package surface when practical:

python scripts/smoke_python_artifact.py --no-index <downloaded-wheel-or-sdist>
python scripts/smoke_java_artifacts.py --api-jar <api-jar> --native-jar <native-jar>
python scripts/smoke_cli_archive.py <downloaded-cli-archive>
python scripts/smoke_js_package.py <downloaded-js-tarball>

Also confirm the package pages are visible on:

  • PyPI for copperlace;

  • Maven Central for dev.mahe.copperlace;

  • npm for copperlace;

  • the GitHub Release page for the tag.

Failure Handling

If the release workflow fails before publishing to a package registry, fix the problem on main, delete the failed local and remote tag if needed, and tag the fixed commit with the same version.

If PyPI, Maven Central, or npm has accepted the version, treat the version as published. Do not move the tag to different source content. Fix any remaining release issue by rerunning the failed workflow job when safe, uploading missing GitHub Release assets, or preparing a new patch version.

If the GitHub Release upload fails after package registries have published, rerun the release workflow or upload the missing assets from the workflow artifacts. Regenerate SHA256SUMS for the final asset set.

If npm publication fails, first retry with the same generated package from the release tag. Only prepare a new version if npm permanently rejects the original version and the package was not published.