name: release # Tag-driven. Cargo.toml carries the version; the tag only points at it, and the # first step refuses to go on if the two disagree — a release tagged v0.3.0 that # publishes 0.2.0 cannot be undone, because PyPI will not let a version be reused # even after it is yanked. on: push: tags: ['v*'] # One job on purpose. Splitting build from publish would mean passing wheels # between jobs through actions/upload-artifact, which is a JavaScript action: the # runner executes it with `node` inside this container, and a Rust image has none. # Everything a release needs fits in one job, so nothing has to be handed over. jobs: publish: runs-on: docker container: image: rust:1.98-bookworm steps: - name: Checkout run: | git init -q . git remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" git fetch -q --depth 1 origin "$GITHUB_SHA" git checkout -q FETCH_HEAD - name: Tag must match Cargo.toml run: | tag="${GITHUB_REF_NAME#v}" crate=$(grep -m1 '^version = ' Cargo.toml | cut -d'"' -f2) echo "tag=$tag crate=$crate" [ "$tag" = "$crate" ] || { echo "::error::tag $tag != Cargo.toml $crate"; exit 1; } - name: Tests run: cargo test - name: Build wheels and sdist run: | apt-get update && apt-get install -y --no-install-recommends python3 python3-venv python3 -m venv /tmp/venv # readme-renderer[md] is what makes `twine check` actually parse the # long description. Without it the render returns None and the # check reports PASSED regardless, even with --strict. /tmp/venv/bin/pip install 'maturin[zig]' twine 'readme-renderer[md]' # The venv has to be first on PATH, not merely called by absolute # path. maturin looks for zig by running `python3 -m ziglang`, and # `maturin[zig]` puts ziglang in the venv only, so a bare python3 # resolves to the system one, which does not have it: "Failed to find # zig", after apt and rustup have already run. export PATH="/tmp/venv/bin:$PATH" python3 -m ziglang version # --zig cross-compiles against an older glibc than this image ships, so # the wheel is not silently pinned to bookworm's. for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do rustup target add "$target" maturin build --release --zig --target "$target" --out dist done maturin sdist --out dist ls -l dist - name: Publish env: # A project-scoped token, not the account-wide one. PyPI's trusted # publishing is OIDC and supports GitHub, GitLab, Google Cloud Build and # ActiveState — not Forgejo — so this forge has to hold a long-lived # credential. Scoping it to this one project is the whole of the # mitigation available: if the forge is compromised, what is lost is the # ability to publish typikon rather than every package the account owns. TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: | # By absolute path: each step gets its own shell, so the PATH the # build step exported for zig's sake does not reach this one. /tmp/venv/bin/twine check --strict dist/* # --skip-existing so a retry after a partial upload finishes the job # instead of dying on the first file. A version cannot be reused, # so a half-published release would otherwise burn it. /tmp/venv/bin/twine upload --non-interactive --skip-existing dist/*