name: Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Release tag to validate (example: v0.3.1)"
required: false
type: string
publish:
description: "Publish to crates.io and create GitHub release"
required: false
default: false
type: boolean
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Dwarnings
CRATE_NAME: motto
jobs:
verify:
name: Verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: Run quality gates
run: |
cargo check --all-features
cargo fmt --all --check
cargo clippy --all-features -- -D warnings
cargo test --all-features
cargo test --all-features -- --ignored
validate_version:
name: Validate Tag Version
runs-on: ubuntu-latest
needs: verify
outputs:
tag: ${{ steps.resolve.outputs.tag }}
version: ${{ steps.compare.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Resolve tag
id: resolve
run: |
if [ "${{ github.event_name }}" = "push" ]; then
tag="${GITHUB_REF_NAME}"
else
tag="${{ github.event.inputs.tag }}"
fi
if [ -z "${tag}" ]; then
echo "No release tag available. For manual runs, provide the 'tag' input."
exit 1
fi
echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
- name: Compare tag and Cargo.toml version
id: compare
env:
TAG: ${{ steps.resolve.outputs.tag }}
run: |
if [[ ! "${TAG}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
echo "Tag must follow vMAJOR.MINOR.PATCH format. Got: ${TAG}"
exit 1
fi
tag_version="${BASH_REMATCH[1]}"
cargo_version="$(python -c 'import tomllib;print(tomllib.load(open("Cargo.toml","rb"))["package"]["version"])')"
if [ "${tag_version}" != "${cargo_version}" ]; then
echo "Tag version (${tag_version}) does not match Cargo.toml version (${cargo_version})."
exit 1
fi
echo "version=${cargo_version}" >> "${GITHUB_OUTPUT}"
publish:
name: Publish Crate
runs-on: ubuntu-latest
needs: validate_version
if: github.event_name == 'push' || github.event.inputs.publish == 'true'
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Ensure crates.io token exists
run: |
if [ -z "${CARGO_REGISTRY_TOKEN}" ]; then
echo "CARGO_REGISTRY_TOKEN is not set"
exit 1
fi
- name: Check if version already exists
id: crate_check
env:
VERSION: ${{ needs.validate_version.outputs.version }}
run: |
status="$(curl -s -o /tmp/crate.json -w "%{http_code}" "https://crates.io/api/v1/crates/${CRATE_NAME}/${VERSION}")"
if [ "${status}" = "200" ]; then
echo "Version ${VERSION} is already published."
echo "already_published=true" >> "${GITHUB_OUTPUT}"
else
echo "Version ${VERSION} is not published yet."
echo "already_published=false" >> "${GITHUB_OUTPUT}"
fi
- name: Publish to crates.io
if: steps.crate_check.outputs.already_published != 'true'
run: cargo publish --locked
github_release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: [validate_version, publish]
if: github.event_name == 'push' || github.event.inputs.publish == 'true'
steps:
- name: Create release with auto notes
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.validate_version.outputs.tag }}
generate_release_notes: true