name: Release Automation Template
on:
workflow_dispatch: inputs:
version:
description: 'Version to release (leave empty to use Cargo.toml version)'
required: false
type: string
skip_tests:
description: 'Skip test execution (emergency releases only)'
required: false
type: boolean
default: false
env:
CARGO_TERM_COLOR: always
permissions:
contents: write packages: read pull-requests: write
jobs:
check-tag:
name: Check Release Prerequisites
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version-check.outputs.version }}
tag-exists: ${{ steps.version-check.outputs.tag-exists }}
is-major-branch: ${{ steps.version-check.outputs.is-major-branch }}
should-release: ${{ steps.version-check.outputs.should-release }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check version and release conditions
id: version-check
run: |
# Get version from input or Cargo.toml
if [[ -n "${{ github.event.inputs.version }}" ]]; then
VERSION="${{ github.event.inputs.version }}"
echo "Using input version: $VERSION"
else
VERSION=$(grep "^version" Cargo.toml | sed 's/version = "\(.*\)"/\1/')
echo "Using Cargo.toml version: $VERSION"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Current version: $VERSION"
# Check if we're on a major version branch
BRANCH_NAME="${{ github.ref_name }}"
if [[ $BRANCH_NAME =~ ^v[0-9]+$ ]]; then
echo "is-major-branch=true" >> $GITHUB_OUTPUT
echo "On major version branch: $BRANCH_NAME"
else
echo "is-major-branch=false" >> $GITHUB_OUTPUT
echo "On main/master branch: $BRANCH_NAME"
fi
# Check if tag already exists
if git tag | grep -q "^v$VERSION$"; then
echo "tag-exists=true" >> $GITHUB_OUTPUT
echo "should-release=false" >> $GITHUB_OUTPUT
echo "โ ๏ธ Tag v$VERSION already exists - skipping release"
else
echo "tag-exists=false" >> $GITHUB_OUTPUT
echo "should-release=true" >> $GITHUB_OUTPUT
echo "โ
Tag v$VERSION does not exist - proceeding with release"
fi
test:
name: Test Before Release
runs-on: ubuntu-latest
needs: check-tag
if: needs.check-tag.outputs.should-release == 'true' && github.event.inputs.skip_tests != 'true'
strategy:
matrix:
rust: [stable]
include:
- rust: stable
coverage: true
steps:
- uses: actions/checkout@v4
- name: Install Rust ${{ matrix.rust }}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-release-${{ matrix.rust }}-${{ hashFiles('**/Cargo.lock') }}
- name: Run comprehensive test suite
run: |
echo "๐งช Running comprehensive test suite..."
cargo test --verbose --all-features
cargo test --doc --all-features
cargo test --no-default-features
- name: Build release
run: cargo build --release --all-features
- name: Test package publishing
run: cargo publish --dry-run --all-features
security-audit:
name: Security Audit
runs-on: ubuntu-latest
needs: check-tag
if: needs.check-tag.outputs.should-release == 'true'
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install cargo-audit
run: cargo install cargo-audit --locked
- name: Run security audit
run: cargo audit
create-tag:
name: Create Release Tag
runs-on: ubuntu-latest
needs: [check-tag, test, security-audit]
if: |
always() &&
needs.check-tag.outputs.should-release == 'true' &&
(needs.test.result == 'success' || github.event.inputs.skip_tests == 'true') &&
needs.security-audit.result == 'success'
outputs:
tag: ${{ steps.create-tag.outputs.tag }}
changelog: ${{ steps.generate-changelog.outputs.changelog }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Generate changelog
id: generate-changelog
run: |
VERSION="${{ needs.check-tag.outputs.version }}"
# Get the latest tag to generate changelog from
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [[ -n "$LATEST_TAG" ]]; then
echo "Generating changelog from $LATEST_TAG to HEAD"
CHANGELOG=$(git log --pretty=format:"- %s" "$LATEST_TAG"..HEAD | head -20)
else
echo "No previous tags found, generating changelog from all commits"
CHANGELOG=$(git log --pretty=format:"- %s" HEAD | head -20)
fi
# Create a safe changelog for GitHub
{
echo "changelog<<EOF"
echo "$CHANGELOG"
echo "EOF"
} >> $GITHUB_OUTPUT
- name: Create annotated tag
id: create-tag
run: |
VERSION="${{ needs.check-tag.outputs.version }}"
TAG="v$VERSION"
# Create annotated tag with release information
TAG_MESSAGE="Release $TAG
Version: $VERSION
Branch: ${{ github.ref_name }}
Commit: ${{ github.sha }}
Automated release created by GitHub Actions"
git tag -a "$TAG" -m "$TAG_MESSAGE"
git push origin "$TAG"
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "โ
Created and pushed tag: $TAG"
publish-crate:
name: Publish to crates.io
runs-on: ubuntu-latest
needs: [check-tag, create-tag]
if: success()
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.create-tag.outputs.tag }}
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-publish-${{ hashFiles('**/Cargo.lock') }}
- name: Final validation before publish
run: |
echo "๐ Final validation before publishing..."
cargo build --release --all-features
cargo test --all-features
cargo publish --dry-run --all-features
- name: Check if version already exists on crates.io
id: version-check
run: |
set -euo pipefail
VERSION="${{ needs.check-tag.outputs.version }}"
NAME="${{ github.event.repository.name }}"
if cargo search "^${NAME}$" --limit 1 | grep -q "${NAME} = \"${VERSION}\""; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Version $VERSION already exists on crates.io"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "Version $VERSION not found on crates.io"
fi
- name: Publish to crates.io
if: steps.version-check.outputs.exists == 'false'
run: |
echo "๐ฆ Publishing to crates.io..."
cargo publish --all-features
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Skip publish (version exists)
if: steps.version-check.outputs.exists == 'true'
run: "echo \"Skipping publish: version ${{ needs.check-tag.outputs.version }} already exists on crates.io\""
create-github-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: [check-tag, create-tag, publish-crate]
if: success()
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.create-tag.outputs.tag }}
- name: Extract version info
id: version-info
run: |
VERSION="${{ needs.check-tag.outputs.version }}"
TAG="${{ needs.create-tag.outputs.tag }}"
# Determine version type
IFS='.' read -r major minor patch <<< "$VERSION"
if [[ $patch == "0" && $minor == "0" ]]; then
VERSION_TYPE="Major Release ๐ฅ"
EMOJI="๐ฅ"
elif [[ $patch == "0" ]]; then
VERSION_TYPE="Minor Release โจ"
EMOJI="โจ"
else
VERSION_TYPE="Patch Release ๐"
EMOJI="๐"
fi
echo "version-type=$VERSION_TYPE" >> $GITHUB_OUTPUT
echo "emoji=$EMOJI" >> $GITHUB_OUTPUT
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ needs.create-tag.outputs.tag }}
release_name: "${{ steps.version-info.outputs.emoji }} Release ${{ needs.create-tag.outputs.tag }}"
body: |
# ${{ steps.version-info.outputs.version-type }}
**Version:** ${{ needs.check-tag.outputs.version }}
**Branch:** ${{ github.ref_name }}
**Published to:** [crates.io](https://crates.io/crates/${{ github.event.repository.name }}) (if not already existing)
## What's Changed
${{ needs.create-tag.outputs.changelog }}
## Installation
```toml
[dependencies]
${{ github.event.repository.name }} = "${{ needs.check-tag.outputs.version }}"
```
## Documentation
- [docs.rs](https://docs.rs/${{ github.event.repository.name }}/${{ needs.check-tag.outputs.version }})
- [Repository](https://github.com/${{ github.repository }})
---
*This release was automatically created by GitHub Actions*
draft: false
prerelease: false
post-release-notification:
name: Post-Release Notifications
runs-on: ubuntu-latest
needs: [check-tag, create-tag, publish-crate, create-github-release]
if: always()
steps:
- name: Notify release status
run: |
VERSION="${{ needs.check-tag.outputs.version }}"
TAG="${{ needs.create-tag.outputs.tag }}"
if [[ "${{ needs.create-github-release.result }}" == "success" ]]; then
echo "๐ Release $TAG completed successfully!"
if [[ "${{ needs.publish-crate.result }}" == "success" ]]; then
echo "โ
Published to crates.io"
else
echo "โน๏ธ Skipped publishing (version already existed on crates.io)"
fi
echo "โ
GitHub release created"
echo "๐ https://crates.io/crates/${{ github.event.repository.name }}"
echo "๐ https://github.com/${{ github.repository }}/releases/tag/$TAG"
else
echo "โ Release $TAG failed!"
echo "Crate publish: ${{ needs.publish-crate.result }}"
echo "GitHub release: ${{ needs.create-github-release.result }}"
fi