name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Release tag (e.g., v1.0.0)'
required: true
type: string
test_release:
description: 'Test release (skip some steps)'
required: false
type: boolean
default: false
permissions:
contents: write
pull-requests: write
env:
CARGO_TERM_COLOR: always
jobs:
generate-completions:
name: Generate Shell Completions
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Install dependencies (Ubuntu)
run: |
sudo apt-get update
sudo apt-get install -y pkg-config libssl-dev
- name: Build binary for completion generation
run: cargo build --release
- name: Generate shell completions
run: |
mkdir -p completions
./target/release/ca completions generate bash > completions/ca.bash
./target/release/ca completions generate zsh > completions/_ca
./target/release/ca completions generate fish > completions/ca.fish
./target/release/ca completions generate powershell > completions/ca.ps1
echo "Generated completions:"
ls -la completions/
- name: Upload completions
uses: actions/upload-artifact@v4
with:
name: completions
path: completions/
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
needs: generate-completions
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
name: ca-linux-x64
can-test: true
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
name: ca-linux-arm64
can-test: false
- target: x86_64-apple-darwin
os: macos-latest
name: ca-macos-x64
can-test: true
- target: aarch64-apple-darwin
os: macos-latest
name: ca-macos-arm64
can-test: false
- target: x86_64-pc-windows-msvc
os: windows-latest
name: ca-windows-x64.exe
can-test: true
- target: aarch64-pc-windows-msvc
os: windows-latest
name: ca-windows-arm64.exe
can-test: false
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install dependencies (macOS)
if: matrix.os == 'macos-latest'
run: |
# Install OpenSSL and pkg-config
brew install openssl pkg-config
# Set OpenSSL environment variables for the build
echo "OPENSSL_DIR=$(brew --prefix openssl)" >> $GITHUB_ENV
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl)/lib" >> $GITHUB_ENV
echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl)/include" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH=$(brew --prefix openssl)/lib/pkgconfig:$PKG_CONFIG_PATH" >> $GITHUB_ENV
# Force static linking to avoid dynamic library issues
echo "OPENSSL_STATIC=1" >> $GITHUB_ENV
echo "LIBSSH2_STATIC=1" >> $GITHUB_ENV
- name: Install dependencies (Ubuntu)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y pkg-config libssl-dev
- name: Free disk space (Ubuntu ARM64)
if: matrix.os == 'ubuntu-latest' && matrix.target == 'aarch64-unknown-linux-gnu'
run: |
# ARM64 cross-compilation needs more space - aggressive cleanup
echo "Disk space before cleanup:"
df -h
# Remove large unnecessary packages and files
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
sudo rm -rf /usr/share/swift
sudo rm -rf /usr/local/.ghcup
sudo rm -rf /usr/local/share/boost
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
# Clean apt cache
sudo apt-get clean
# Remove docker images
sudo docker image prune --all --force || true
echo "Disk space after cleanup:"
df -h
- name: Install cross-compilation tools (Ubuntu ARM64)
if: matrix.os == 'ubuntu-latest' && matrix.target == 'aarch64-unknown-linux-gnu'
run: |
# Install cross-compilation toolchain without adding arm64 architecture
# This avoids package repository issues with multi-arch
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
# For OpenSSL, we'll use the vendored version instead of system libraries
# This is more reliable for cross-compilation
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-${{ matrix.target }}-
${{ runner.os }}-cargo-
- name: Configure cross-compilation (Linux ARM64)
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config.toml
echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config.toml
# Force all dependencies to use vendored/static libraries
echo 'OPENSSL_STATIC=1' >> $GITHUB_ENV
echo 'OPENSSL_VENDORED=1' >> $GITHUB_ENV
echo 'LIBSSH2_STATIC=1' >> $GITHUB_ENV
echo 'LIBSSH2_VENDORED=1' >> $GITHUB_ENV
echo 'LIBGIT2_STATIC=1' >> $GITHUB_ENV
echo 'CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc' >> $GITHUB_ENV
- name: Build release binary
run: cargo build --release --target ${{ matrix.target }}
- name: Clean up build artifacts (ARM64 Linux/macOS)
if: matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'aarch64-apple-darwin'
run: |
# Remove intermediate build artifacts to save disk space
# Keep only the final binary
if [ -d "target/${{ matrix.target }}/release" ]; then
find target/${{ matrix.target }}/release -type f ! -name 'ca' ! -name 'ca.exe' -delete
find target/${{ matrix.target }}/release -type d -name '.fingerprint' -exec rm -rf {} + || true
find target/${{ matrix.target }}/release -type d -name 'build' -exec rm -rf {} + || true
find target/${{ matrix.target }}/release -type d -name 'deps' -exec rm -rf {} + || true
fi
- name: Clean up build artifacts (ARM64 Windows)
if: matrix.target == 'aarch64-pc-windows-msvc'
shell: powershell
run: |
# Remove intermediate build artifacts to save disk space
# Keep only the final binary
$targetPath = "target/${{ matrix.target }}/release"
if (Test-Path $targetPath) {
Get-ChildItem $targetPath -File | Where-Object { $_.Name -ne 'ca.exe' } | Remove-Item -Force
Get-ChildItem $targetPath -Directory -Filter '.fingerprint' -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force
Get-ChildItem $targetPath -Directory -Filter 'build' -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force
Get-ChildItem $targetPath -Directory -Filter 'deps' -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force
}
- name: Download completions
uses: actions/download-artifact@v4
with:
name: completions
path: completions
- name: Run tests (if supported)
if: matrix.can-test
continue-on-error: true
run: cargo test --target ${{ matrix.target }}
- name: Create binary package (Unix)
if: matrix.os != 'windows-latest'
run: |
# Create package directory
mkdir -p package
cp target/${{ matrix.target }}/release/ca package/
# Include completions
cp -r completions package/
# Create archive
cd package
tar czf ../${{ matrix.name }}.tar.gz *
cd ..
- name: Create binary package (Windows)
if: matrix.os == 'windows-latest'
run: |
# Create package directory
mkdir package
cp target/${{ matrix.target }}/release/ca.exe package/
# Include completions
Copy-Item -Recurse completions package/
# Create archive
cd package
7z a ../${{ matrix.name }}.zip *
cd ..
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.name }}
path: |
${{ matrix.name }}.tar.gz
${{ matrix.name }}.zip
create-release:
name: Create Release
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Generate release notes
id: release_notes
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ github.event.inputs.tag }}"
else
TAG=${GITHUB_REF#refs/tags/}
fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
# Get previous tag for changelog
PREV_TAG=$(git describe --tags --abbrev=0 $TAG^ 2>/dev/null || echo "")
# Generate changelog
echo "## 🚀 What's New in $TAG" > release_notes.md
echo "" >> release_notes.md
if [ -n "$PREV_TAG" ]; then
echo "### 📝 Changes since $PREV_TAG:" >> release_notes.md
git log --pretty=format:"- %s (%h)" $PREV_TAG..$TAG >> release_notes.md
else
echo "### 📝 Initial Release Features:" >> release_notes.md
echo "- 🔄 **Stacked Diff Workflow** - Chain related commits for better code review" >> release_notes.md
echo "- 🤖 **Smart Conflict Resolution** - Auto-resolves 60-80% of common rebase conflicts" >> release_notes.md
echo "- 🚀 **Smart Force Push** - Preserves review history during rebases" >> release_notes.md
echo "- 🔧 **Bitbucket Integration** - Seamless PR management" >> release_notes.md
echo "- 📊 **Terminal UI** - Interactive stack browser and management" >> release_notes.md
echo "- 🎨 **Visualizations** - ASCII, Mermaid, Graphviz, PlantUML formats" >> release_notes.md
echo "- ⚡ **Git Hooks** - Automated workflow enforcement" >> release_notes.md
echo "- 🛠️ **Shell Completions** - Bash, Zsh, Fish, PowerShell support" >> release_notes.md
fi
echo "" >> release_notes.md
echo "## 📦 Installation" >> release_notes.md
echo "" >> release_notes.md
echo "### Quick Install (Unix)" >> release_notes.md
echo '```bash' >> release_notes.md
echo "# Linux (x64)" >> release_notes.md
echo "curl -L https://github.com/JAManfredi/cascade-cli/releases/download/$TAG/ca-linux-x64.tar.gz | tar -xz" >> release_notes.md
echo "sudo mv ca /usr/local/bin/" >> release_notes.md
echo "" >> release_notes.md
echo "# macOS (x64)" >> release_notes.md
echo "curl -L https://github.com/JAManfredi/cascade-cli/releases/download/$TAG/ca-macos-x64.tar.gz | tar -xz" >> release_notes.md
echo "sudo mv ca /usr/local/bin/" >> release_notes.md
echo '```' >> release_notes.md
echo "" >> release_notes.md
echo "### Platform-Specific Downloads" >> release_notes.md
echo "- **Linux x64**: ca-linux-x64.tar.gz" >> release_notes.md
echo "- **Linux ARM64**: ca-linux-arm64.tar.gz" >> release_notes.md
echo "- **macOS x64**: ca-macos-x64.tar.gz" >> release_notes.md
echo "- **macOS ARM64**: ca-macos-arm64.tar.gz" >> release_notes.md
echo "- **Windows x64**: ca-windows-x64.exe.zip" >> release_notes.md
echo "- **Windows ARM64**: ca-windows-arm64.exe.zip" >> release_notes.md
echo "" >> release_notes.md
echo "## 📖 Documentation" >> release_notes.md
echo "- [User Manual](https://github.com/JAManfredi/cascade-cli/blob/main/docs/USER_MANUAL.md)" >> release_notes.md
echo "- [Installation Guide](https://github.com/JAManfredi/cascade-cli/blob/main/docs/INSTALLATION.md)" >> release_notes.md
echo "- [30-Minute Onboarding](https://github.com/JAManfredi/cascade-cli/blob/main/docs/ONBOARDING.md)" >> release_notes.md
- name: Prepare release assets
run: |
mkdir -p release-assets
find artifacts -name "*.tar.gz" -exec cp {} release-assets/ \;
find artifacts -name "*.zip" -exec cp {} release-assets/ \;
ls -la release-assets/
- name: Test Unix releases
run: |
cd release-assets
# Only test the Linux x64 binary on Linux runners
if [ -f "ca-linux-x64.tar.gz" ]; then
tar -xzf "ca-linux-x64.tar.gz"
./ca --version
./ca --help
rm -f ca # Clean up
fi
- name: Test Windows releases
if: runner.os == 'Windows'
shell: powershell
run: |
cd release-assets
if ("${{ github.event_name }}" -eq "workflow_dispatch") {
$TAG = "${{ github.event.inputs.tag }}"
} else {
$TAG = "${env:GITHUB_REF}".Substring(10) # Remove 'refs/tags/'
}
$target = "ca-windows-x64.exe.zip"
if (Test-Path $target) {
Expand-Archive -Path $target -DestinationPath "."
.\ca.exe --version
.\ca.exe --help
}
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.release_notes.outputs.tag }}
name: 'Cascade CLI ${{ steps.release_notes.outputs.tag }}'
body_path: release_notes.md
files: release-assets/*
draft: false
prerelease: false
make_latest: true
trigger-homebrew-tap-update:
name: Trigger Homebrew Tap Update
needs: create-release
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/') && !github.event.inputs.test_release
steps:
- name: Get release info
id: release_info
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ github.event.inputs.tag }}"
else
TAG=${GITHUB_REF#refs/tags/}
fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
- name: Trigger Homebrew Tap Update Workflow
uses: actions/github-script@v7
with:
github-token: ${{ secrets.TAP_GITHUB_TOKEN }}
script: |
const tag = '${{ steps.release_info.outputs.tag }}';
console.log(`Triggering Homebrew tap update for version ${tag}`);
await github.rest.actions.createWorkflowDispatch({
owner: 'JAManfredi',
repo: 'cascade-cli',
workflow_id: 'update-homebrew-tap.yml',
ref: 'master',
inputs: {
version: tag
}
});
console.log('✅ Homebrew tap update workflow triggered successfully');
test-installation:
name: Test Installation
needs: create-release
runs-on: ${{ matrix.os }}
if: (startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch') && !github.event.inputs.test_release
strategy:
matrix:
include:
- os: ubuntu-latest
target: ca-linux-x64.tar.gz
- os: macos-latest
target: ca-macos-x64.tar.gz
- os: windows-latest
target: ca-windows-x64.exe.zip
steps:
- name: Download and test binary (Unix)
if: matrix.os != 'windows-latest'
run: |
# Handle both tag push and workflow_dispatch events
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ github.event.inputs.tag }}"
else
TAG=${GITHUB_REF#refs/tags/}
fi
echo "Testing release with tag: $TAG"
echo "Download URL: https://github.com/JAManfredi/cascade-cli/releases/download/$TAG/${{ matrix.target }}"
# Wait a bit for release to be fully published
sleep 10
# Download with better error handling
curl -L -f --retry 3 --retry-delay 10 \
"https://github.com/JAManfredi/cascade-cli/releases/download/$TAG/${{ matrix.target }}" \
-o "${{ matrix.target }}"
# Verify it's actually a gzip file
file "${{ matrix.target }}"
# Extract and test
tar -xzf "${{ matrix.target }}"
./ca --version
./ca --help
- name: Download and test binary (Windows)
if: matrix.os == 'windows-latest'
run: |
# Handle both tag push and workflow_dispatch events
if ("${{ github.event_name }}" -eq "workflow_dispatch") {
$TAG = "${{ github.event.inputs.tag }}"
} else {
$TAG = $env:GITHUB_REF -replace 'refs/tags/', ''
}
Write-Host "Testing release with tag: $TAG"
Write-Host "Download URL: https://github.com/JAManfredi/cascade-cli/releases/download/$TAG/${{ matrix.target }}"
# Wait a bit for release to be fully published
Start-Sleep -Seconds 10
# Download with error handling
try {
Invoke-WebRequest -Uri "https://github.com/JAManfredi/cascade-cli/releases/download/$TAG/${{ matrix.target }}" -OutFile "ca.zip" -UseBasicParsing
Expand-Archive -Path "ca.zip" -DestinationPath "."
.\ca.exe --version
.\ca.exe --help
} catch {
Write-Error "Failed to download or extract: $_"
exit 1
}
publish-crates:
name: Publish to crates.io
needs: create-release
runs-on: ubuntu-latest
if: (startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch') && !github.event.inputs.test_release
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Publish to crates.io
run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }}