name: Release
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-*'
workflow_dispatch:
inputs:
dry_run:
description: 'Perform a dry run without publishing'
required: false
default: false
type: boolean
permissions:
contents: write
packages: write
concurrency:
group: release
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
validate-release:
name: Validate Release
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
version: ${{ steps.version.outputs.version }}
prerelease: ${{ steps.version.outputs.prerelease }}
steps:
- uses: actions/checkout@v4
- name: Extract version info
id: version
run: |
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF#refs/tags/v}"
else
VERSION=$(grep '^version = ' Cargo.toml | head -1 | cut -d'"' -f2)
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
# Check if prerelease
if [[ "$VERSION" == *"-"* ]]; then
echo "prerelease=true" >> $GITHUB_OUTPUT
else
echo "prerelease=false" >> $GITHUB_OUTPUT
fi
echo "📦 Preparing release for version: ${VERSION}"
- name: Verify version consistency
run: |
VERSION="${{ steps.version.outputs.version }}"
CARGO_VERSION=$(grep '^version = ' Cargo.toml | head -1 | cut -d'"' -f2)
if [[ "$CARGO_VERSION" != "$VERSION" ]]; then
echo "❌ Version mismatch: Cargo.toml has $CARGO_VERSION but tag/input is $VERSION"
exit 1
fi
# Run full version check
chmod +x scripts/check-versions.sh
./scripts/check-versions.sh
- name: Check CHANGELOG
run: |
VERSION="${{ steps.version.outputs.version }}"
if ! grep -q "## \[$VERSION\]" CHANGELOG.md; then
echo "❌ No CHANGELOG entry found for version $VERSION"
exit 1
fi
echo "✅ CHANGELOG entry found for $VERSION"
- name: Validate crate dependencies
run: |
# Ensure all workspace crates have matching versions
for crate in crates/*; do
if [[ -f "$crate/Cargo.toml" ]]; then
if ! grep -q 'version.workspace = true' "$crate/Cargo.toml"; then
echo "❌ Crate $(basename $crate) doesn't use workspace version"
exit 1
fi
fi
done
echo "✅ All crates use workspace versioning"
test-release:
name: Release Tests
runs-on: ${{ matrix.os }}
timeout-minutes: 20
needs: validate-release
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
targets: wasm32-unknown-unknown
components: rustfmt, clippy
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-release-
- name: Run tests (unit and integration only, E2E runs locally)
run: |
cargo fmt -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --lib --bins --all-features
- name: Build WASM target
run: cargo build --target wasm32-unknown-unknown --release
- name: Build release binaries
run: cargo build --release --all-features
build-artifacts:
name: Build Release Artifacts
runs-on: ${{ matrix.os }}
timeout-minutes: 30
needs: test-release
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact_name: icarus-linux-amd64
- os: macos-latest
target: x86_64-apple-darwin
artifact_name: icarus-macos-amd64
- os: macos-latest
target: aarch64-apple-darwin
artifact_name: icarus-macos-arm64
- os: windows-latest
target: x86_64-pc-windows-msvc
artifact_name: icarus-windows-amd64
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Build CLI binary
run: cargo build --package icarus-cli --release --target ${{ matrix.target }}
- name: Package Windows binary
if: matrix.os == 'windows-latest'
run: 7z a ${{ matrix.artifact_name }}.zip ./target/${{ matrix.target }}/release/icarus.exe
- name: Package Unix binary
if: matrix.os != 'windows-latest'
run: tar -czf ${{ matrix.artifact_name }}.tar.gz -C ./target/${{ matrix.target }}/release icarus
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: ${{ matrix.artifact_name }}.*
retention-days: 7
github-release:
name: Create GitHub Release
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [validate-release, build-artifacts]
if: github.event.inputs.dry_run != 'true'
steps:
- uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Extract CHANGELOG entry
run: |
VERSION="${{ needs.validate-release.outputs.version }}"
# Extract changelog for this version
awk "/## \[$VERSION\]/{flag=1; next} /## \[/{flag=0} flag" CHANGELOG.md > release-notes.md
# Add installation instructions
cat >> release-notes.md << 'EOF'
## Installation
### Using Cargo
```bash
cargo install icarus-cli
```
### Add to Project
```toml
[dependencies]
icarus = "VERSION"
```
### Binary Downloads
Pre-built binaries are available below for:
- Linux (x86_64)
- macOS (x86_64, ARM64)
- Windows (x86_64)
## Crates Published
EOF
# Add crate links (will be published after this release)
echo "The following crates will be published to crates.io shortly:" >> release-notes.md
echo "- [icarus](https://crates.io/crates/icarus/$VERSION)" >> release-notes.md
echo "- [icarus-cli](https://crates.io/crates/icarus-cli/$VERSION)" >> release-notes.md
echo "- [icarus-core](https://crates.io/crates/icarus-core/$VERSION)" >> release-notes.md
echo "- [icarus-derive](https://crates.io/crates/icarus-derive/$VERSION)" >> release-notes.md
echo "- [icarus-canister](https://crates.io/crates/icarus-canister/$VERSION)" >> release-notes.md
# Replace VERSION placeholder
sed -i "s/VERSION/$VERSION/g" release-notes.md
- name: Create GitHub Release
id: create_release
uses: softprops/action-gh-release@v2
with:
name: v${{ needs.validate-release.outputs.version }}
body_path: release-notes.md
prerelease: ${{ needs.validate-release.outputs.prerelease }}
draft: false files: |
artifacts/**/*.tar.gz
artifacts/**/*.zip
fail_on_unmatched_files: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Release created notification
if: success()
run: |
echo "✅ GitHub Release v${{ needs.validate-release.outputs.version }} created successfully!"
echo "🔗 View on GitHub: https://github.com/${{ github.repository }}/releases/tag/v${{ needs.validate-release.outputs.version }}"
echo "📦 Next step: Publishing to crates.io..."
publish-crates:
name: Publish to crates.io
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [validate-release, github-release]
if: needs.github-release.result == 'success'
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-publish-${{ hashFiles('**/Cargo.lock') }}
- name: Verify GitHub release exists
run: |
VERSION="${{ needs.validate-release.outputs.version }}"
echo "🔍 Verifying GitHub release v${VERSION} exists..."
# Check if release exists using GitHub API
if gh release view "v${VERSION}" --repo "${{ github.repository }}" > /dev/null 2>&1; then
echo "✅ GitHub release v${VERSION} confirmed"
else
echo "❌ GitHub release v${VERSION} not found!"
echo "This should not happen - the previous job should have created it."
exit 1
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish crates in order
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
set -e
# Function to publish and wait for availability
publish_crate() {
local crate_path=$1
local crate_name=$2
local version=$3
echo "📦 Publishing $crate_name v$version..."
cd "$crate_path"
# Check if already published using cargo info (faster than search)
if cargo info "$crate_name" 2>/dev/null | grep -q "version: $version"; then
echo "✅ $crate_name v$version already published"
cd - > /dev/null
return 0
fi
# Publish the crate
cargo publish --no-verify --token "$CARGO_REGISTRY_TOKEN"
cd - > /dev/null
# Wait for crate to be available (increased timeout and using cargo info)
echo "⏳ Waiting for $crate_name to be available on crates.io..."
for i in {1..60}; do
if cargo info "$crate_name" 2>/dev/null | grep -q "version: $version"; then
echo "✅ $crate_name v$version is now available"
return 0
fi
# Show progress every 10 attempts
if [ $((i % 10)) -eq 0 ]; then
echo " Still waiting... (attempt $i/60)"
fi
sleep 2
done
echo "⚠️ Timeout waiting for $crate_name to be available"
echo " The crate was likely published successfully but index is slow to update."
echo " Continuing with next crate..."
# Return success even on timeout - the crate was published
return 0
}
VERSION="${{ needs.validate-release.outputs.version }}"
# Publish in dependency order
publish_crate "crates/icarus-core" "icarus-core" "$VERSION"
publish_crate "crates/icarus-derive" "icarus-derive" "$VERSION"
publish_crate "crates/icarus-canister" "icarus-canister" "$VERSION"
publish_crate "." "icarus" "$VERSION"
publish_crate "cli" "icarus-cli" "$VERSION"
echo "🎉 All crates published successfully!"
release-notification:
name: Release Status Notification
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [validate-release, github-release, publish-crates]
if: always()
steps:
- name: Report final status
run: |
VERSION="${{ needs.validate-release.outputs.version }}"
echo "📊 Release v${VERSION} Status Report:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [[ "${{ needs.github-release.result }}" == "success" ]]; then
echo "✅ GitHub Release: SUCCESS"
echo " 🔗 https://github.com/${{ github.repository }}/releases/tag/v${VERSION}"
else
echo "❌ GitHub Release: FAILED"
fi
if [[ "${{ needs.publish-crates.result }}" == "success" ]]; then
echo "✅ Crates.io: SUCCESS"
echo " 📦 https://crates.io/crates/icarus/${VERSION}"
elif [[ "${{ needs.publish-crates.result }}" == "skipped" ]]; then
echo "⏭️ Crates.io: SKIPPED (GitHub release failed)"
else
echo "❌ Crates.io: FAILED"
echo " ⚠️ Manual intervention may be required"
echo " Run: cargo publish --no-verify"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [[ "${{ needs.github-release.result }}" == "success" ]] && [[ "${{ needs.publish-crates.result }}" == "success" ]]; then
echo "🎉 Release v${VERSION} completed successfully!"
else
echo "⚠️ Release v${VERSION} partially completed. Check the logs above."
fi