name: release
on:
workflow_dispatch:
inputs:
version:
description: "Version WITHOUT a leading v, e.g. 3.3.3 or 3.3.3-rc1 (must match Cargo.toml)"
required: true
sha:
description: "Full commit SHA to release (must be on main)"
required: true
concurrency:
group: release
cancel-in-progress: false
jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Validate version input format
env:
VERSION: ${{ inputs.version }}
run: |
# Accept stable versions and versions with a prerelease suffix.
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
echo "::error::version must be semver like 3.3.3 or 3.3.3-rc1 (no leading 'v')."
exit 1
fi
- name: Restrict to main branch
if: github.ref != 'refs/heads/main'
run: |
echo "::error::This workflow must be run from main (ref was '${{ github.ref }}')."
exit 1
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with:
fetch-depth: 0
ref: ${{ inputs.sha }}
- name: Validate commit is on main
env:
RELEASE_SHA: ${{ inputs.sha }}
run: |
# An ancestor check prevents releasing an unrelated commit.
if ! git merge-base --is-ancestor "$RELEASE_SHA" origin/main; then
echo "::error::Input SHA '$RELEASE_SHA' is not an ancestor of main."
exit 1
fi
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659
- name: Validate version matches Cargo.toml
env:
VERSION: ${{ inputs.version }}
run: |
# Read the package version from Cargo's normalized metadata.
manifest_version=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[0].version')
if [ "$manifest_version" != "$VERSION" ]; then
echo "::error::Input version '$VERSION' does not match Cargo.toml version '$manifest_version'."
exit 1
fi
- name: Validate tag does not already exist
env:
VERSION: ${{ inputs.version }}
run: |
# Query the remote tag directly without fetching every tag.
if git ls-remote --tags origin "refs/tags/v$VERSION" | grep -q .; then
echo "::error::Tag v$VERSION already exists."
exit 1
fi
- name: Validate version not already published
env:
VERSION: ${{ inputs.version }}
run: |
# A successful API response means this exact version is published.
if curl -sf "https://crates.io/api/v1/crates/cargo-readme/$VERSION" > /dev/null; then
echo "::error::cargo-readme $VERSION is already published on crates.io."
exit 1
fi
- name: Run CI checks
run: make ci
publish:
needs: verify
runs-on: ubuntu-latest
timeout-minutes: 30
environment: release
permissions:
contents: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with:
fetch-depth: 0
ref: ${{ inputs.sha }}
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659
- name: Tag release
env:
VERSION: ${{ inputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "v$VERSION" -m "v$VERSION"
git push origin "v$VERSION"
- name: Create GitHub release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
prerelease_flag=""
# Treat any version with a suffix as a prerelease.
case "$VERSION" in
*-*) prerelease_flag="--prerelease" ;;
esac
# Pass the optional prerelease flag only when it is needed.
gh release create "v$VERSION" \
--title "v$VERSION" \
$prerelease_flag \
--generate-notes
- name: Authenticate with crates.io
id: auth
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18
- name: Publish to crates.io
run: cargo publish --locked
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}