name: Release
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., v1.0.0)'
required: true
type: string
env:
CARGO_TERM_COLOR: always
permissions:
contents: write
packages: write
jobs:
validate-tag:
name: Validate Tag
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
is_prerelease: ${{ steps.version.outputs.is_prerelease }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract version from tag or input
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ inputs.version }}"
else
VERSION=${GITHUB_REF#refs/tags/}
fi
# Remove 'v' prefix if present
VERSION=${VERSION#v}
# Validate version format
if [[ ! $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then
echo "❌ Invalid version format: $VERSION"
exit 1
fi
# Check if it's a prerelease
if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
IS_PRERELEASE=false
else
IS_PRERELEASE=true
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT
echo "✅ Version: $VERSION (prerelease: $IS_PRERELEASE)"
test-before-release:
name: Test Before Release
runs-on: ubuntu-latest
needs: validate-tag
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target
key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
- name: Check code formatting
run: cargo fmt -- --check
- name: Run clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Run tests
run: cargo test --all-features --verbose
- name: Verify version in Cargo.toml
run: |
CARGO_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
if [ "$CARGO_VERSION" != "${{ needs.validate-tag.outputs.version }}" ]; then
echo "❌ Version mismatch: Cargo.toml has $CARGO_VERSION, tag has ${{ needs.validate-tag.outputs.version }}"
exit 1
fi
echo "✅ Version matches: $CARGO_VERSION"
build-artifacts:
name: Build Artifacts
runs-on: ${{ matrix.os }}
needs: [validate-tag, test-before-release]
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact_name: threatflux-cache-linux-x64
- os: windows-latest
target: x86_64-pc-windows-msvc
artifact_name: threatflux-cache-windows-x64
- os: macos-latest
target: x86_64-apple-darwin
artifact_name: threatflux-cache-macos-x64
- os: macos-latest
target: aarch64-apple-darwin
artifact_name: threatflux-cache-macos-arm64
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target
key: ${{ runner.os }}-${{ matrix.target }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
- name: Build library
run: cargo build --release --all-features --target ${{ matrix.target }}
- name: Package artifact (Unix)
if: matrix.os != 'windows-latest'
run: |
mkdir -p artifacts
cd target/${{ matrix.target }}/release
tar -czf ../../../artifacts/${{ matrix.artifact_name }}.tar.gz *.so *.dylib 2>/dev/null || true
cd ../../..
- name: Package artifact (Windows)
if: matrix.os == 'windows-latest'
run: |
mkdir artifacts
cd target/${{ matrix.target }}/release
if (Test-Path "*.dll") {
Compress-Archive -Path "*.dll" -DestinationPath "../../../artifacts/${{ matrix.artifact_name }}.zip"
}
cd ../../..
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: artifacts/*
if-no-files-found: ignore
generate-changelog:
name: Generate Changelog
runs-on: ubuntu-latest
needs: validate-tag
outputs:
changelog: ${{ steps.changelog.outputs.changelog }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate changelog
id: changelog
run: |
# Get the previous tag
PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -v "^v${{ needs.validate-tag.outputs.version }}$" | head -1 || echo "")
if [ -z "$PREVIOUS_TAG" ]; then
# First release
CHANGELOG="## What's New\n\nThis is the initial release of ThreatFlux Cache v${{ needs.validate-tag.outputs.version }}.\n\n"
CHANGELOG="$CHANGELOG### Features\n"
CHANGELOG="$CHANGELOG- Flexible async cache library for Rust\n"
CHANGELOG="$CHANGELOG- Pluggable backends (memory, filesystem)\n"
CHANGELOG="$CHANGELOG- Multiple serialization formats (JSON, bincode)\n"
CHANGELOG="$CHANGELOG- LRU eviction support\n"
CHANGELOG="$CHANGELOG- Compression support with flate2\n"
CHANGELOG="$CHANGELOG- Metrics and tracing integration\n"
CHANGELOG="$CHANGELOG- OpenAPI documentation support\n"
else
# Generate changelog from git commits
CHANGELOG="## Changes since $PREVIOUS_TAG\n\n"
# Get commits since last tag
COMMITS=$(git log --pretty=format:"- %s" $PREVIOUS_TAG..HEAD | grep -E "^- (feat|fix|perf|docs|style|refactor|test|chore)" || true)
if [ -n "$COMMITS" ]; then
CHANGELOG="$CHANGELOG### Commits\n$COMMITS\n\n"
else
CHANGELOG="$CHANGELOG### Changes\n- Various improvements and bug fixes\n\n"
fi
fi
CHANGELOG="$CHANGELOG### Documentation\n"
CHANGELOG="$CHANGELOG- [API Documentation](https://docs.rs/threatflux-cache/${{ needs.validate-tag.outputs.version }})\n"
CHANGELOG="$CHANGELOG- [Crates.io](https://crates.io/crates/threatflux-cache)\n"
# Escape newlines for GitHub Actions
CHANGELOG="${CHANGELOG//'%'/'%25'}"
CHANGELOG="${CHANGELOG//$'\n'/'%0A'}"
CHANGELOG="${CHANGELOG//$'\r'/'%0D'}"
echo "changelog=$CHANGELOG" >> $GITHUB_OUTPUT
publish-crates-io:
name: Publish to Crates.io
runs-on: ubuntu-latest
needs: [validate-tag, test-before-release]
if: needs.validate-tag.outputs.is_prerelease == 'false'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-publish-${{ hashFiles('**/Cargo.lock') }}
- name: Login to crates.io
run: cargo login ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Publish to crates.io
run: |
# Dry run first
cargo publish --dry-run --all-features
# Actually publish
cargo publish --all-features
create-github-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: [validate-tag, generate-changelog, build-artifacts]
if: always() && needs.validate-tag.result == 'success' && needs.generate-changelog.result == 'success'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: release-artifacts
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.validate-tag.outputs.version }}
name: ThreatFlux Cache v${{ needs.validate-tag.outputs.version }}
body: ${{ needs.generate-changelog.outputs.changelog }}
prerelease: ${{ needs.validate-tag.outputs.is_prerelease == 'true' }}
files: |
release-artifacts/**/*
generate_release_notes: true
make_latest: ${{ needs.validate-tag.outputs.is_prerelease == 'false' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
notify-release:
name: Post-Release Notifications
runs-on: ubuntu-latest
needs: [validate-tag, create-github-release, publish-crates-io]
if: always() && needs.create-github-release.result == 'success'
steps:
- name: Release Summary
run: |
echo "🚀 ThreatFlux Cache v${{ needs.validate-tag.outputs.version }} Released!"
echo ""
echo "📦 Crates.io: https://crates.io/crates/threatflux-cache"
echo "📚 Documentation: https://docs.rs/threatflux-cache/${{ needs.validate-tag.outputs.version }}"
echo "🔖 GitHub Release: https://github.com/ThreatFlux/threatflux-cache/releases/tag/v${{ needs.validate-tag.outputs.version }}"
echo ""
if [ "${{ needs.validate-tag.outputs.is_prerelease }}" = "true" ]; then
echo "⚠️ This is a pre-release version"
else
echo "✅ This is a stable release"
fi