---
name: Publish to crates.io (manual, protected)
on:
workflow_dispatch:
inputs:
dry_run:
description: Do a dry run (no publish)?
required: true
type: boolean
default: true
run_tests:
description: Run cargo test before packaging/publish
required: true
type: boolean
default: true
crate_path:
description: Path to crate ('.' for root, or subcrate path in a workspace)
required: true
type: string
default: .
require_tag:
description: Require a git tag matching version (e.g., vX.Y.Z) and pointing
to HEAD
required: true
type: boolean
default: true
tag_prefix:
description: Tag prefix used for version tags
required: true
type: string
default: v
upload_artifact:
description: Upload built .crate as a workflow artifact
required: true
type: boolean
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
jobs:
publish:
name: Cargo publish (${{ inputs.dry_run && 'dry-run' || 'live' }})
runs-on: ubuntu-latest
environment: crates-io
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 - name: Install Rust (stable)
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo registry + build
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Show toolchain
run: |
rustc -Vv
cargo -Vv
- name: (optional) Run tests
if: ${{ inputs.run_tests == true }}
env:
RUSTFLAGS: -D warnings
run: |
cargo test --locked --workspace --verbose
- name: Read crate name & version
id: meta
run: |
JSON=$(cargo metadata --no-deps --format-version=1 --manifest-path "${{ inputs.crate_path }}/Cargo.toml")
NAME=$(echo "$JSON" | jq -r '.packages[0].name')
VER=$(echo "$JSON" | jq -r '.packages[0].version')
echo "name=$NAME" >> $GITHUB_OUTPUT
echo "version=$VER" >> $GITHUB_OUTPUT
echo "Crate: $NAME@$VER"
- name: Verify tag matches version (optional)
if: ${{ inputs.require_tag == true }}
run: |
set -euo pipefail
TAG="${{ inputs.tag_prefix }}${{ steps.meta.outputs.version }}"
echo "Expecting tag: $TAG"
git fetch --tags --quiet
if ! git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "::error::Required tag '$TAG' does not exist."; exit 1; fi
HEAD=$(git rev-parse HEAD)
TAG_SHA=$(git rev-list -n 1 "$TAG")
if [ "$HEAD" != "$TAG_SHA" ]; then
echo "::error::Tag '$TAG' does not point to HEAD ($HEAD != $TAG_SHA)."; exit 1; fi
echo "Tag check OK."
- name: Ensure version not already published
run: |
set -euo pipefail
NAME='${{ steps.meta.outputs.name }}'
VER='${{ steps.meta.outputs.version }}'
# jq should be present; install if missing
if ! command -v jq >/dev/null; then sudo apt-get update && sudo apt-get install -y jq; fi
EXIST=$(curl -fsSL "https://crates.io/api/v1/crates/$NAME" | jq -r --arg v "$VER" '.versions[].num | select(.==$v)')
if [ -n "$EXIST" ]; then
echo "::error::Version $NAME@$VER already exists on crates.io"; exit 1; fi
echo "Version availability OK."
- name: Verify clean working tree
run: |
git update-index -q --refresh
if ! git diff --quiet -- .; then
echo "::error::Working tree is dirty. Commit or stash changes before publishing."; exit 1; fi
echo "Working tree clean."
- name: Package crate
run: |
cargo package --locked --manifest-path "${{ inputs.crate_path }}/Cargo.toml"
- name: Upload .crate artifact (optional)
if: ${{ inputs.upload_artifact == true }}
uses: actions/upload-artifact@v4
with:
name: ${{ steps.meta.outputs.name }}-${{ steps.meta.outputs.version }}.crate
path: target/package/*.crate
if-no-files-found: error
- name: Dry run publish
if: ${{ inputs.dry_run == true }}
run: |
cargo publish --locked --manifest-path "${{ inputs.crate_path }}/Cargo.toml" --dry-run
- name: Publish to crates.io
if: ${{ inputs.dry_run == false }}
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
cargo publish --locked --manifest-path "${{ inputs.crate_path }}/Cargo.toml"
- name: Guidance
run: |-
echo "If publishing failed due to existing version, bump 'version' in Cargo.toml and re-run."
echo "Ensure you have set the repository secret CARGO_REGISTRY_TOKEN (from crates.io)."
echo "Environment 'crates-io' can be configured to require reviewer approval before this job runs."