Skip to content

CI/CD

CI/CD #250

Workflow file for this run

# This workflow handles continuous integration and deployment for the Git-Iris project.
# It runs tests and builds for all pushes, and creates releases for tagged pushes.
name: CI/CD
on:
push:
branches:
- main # Trigger on pushes to main branch
tags:
- "v*.*.*" # Trigger on version tags
pull_request:
branches:
- main # Trigger on pull requests to main branch
workflow_dispatch:
inputs:
tag:
description: "Tag to build/release (e.g., v1.2.3)"
required: false
release_run_id:
description: "Run ID of the release workflow (for artifact download)"
required: false
jobs:
# This job runs for all pushes and pull requests
build-and-test:
name: 🧪 Build & Test (${{ matrix.build }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- build: linux-amd64
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
cmd: cargo
steps:
- name: 📥 Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🦀 Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: 📦 Rust cache
uses: Swatinem/rust-cache@v2
with:
# Let action generate cache keys automatically
workspaces: ". -> target"
- name: 🔨 Build
run: |
echo "::group::Building Git-Iris (${{ matrix.build }})"
${{ matrix.cmd }} build --verbose --locked --target ${{ matrix.target }}
echo "::endgroup::"
- name: 🧪 Run tests
run: |
echo "::group::Running tests (${{ matrix.build }})"
${{ matrix.cmd }} test --verbose --locked --target ${{ matrix.target }}
echo "::endgroup::"
# Test Docker image building and functionality
docker-build-and-test:
name: 🐳 Docker Build & Test
runs-on: ubuntu-latest
needs: build-and-test
env:
IMAGE_NAME: git-iris-test
steps:
- name: 📥 Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🔍 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 🔨 Build Docker image
working-directory: ./docker
run: |
echo "::group::Building Docker image"
./build.sh $IMAGE_NAME
echo "::endgroup::"
- name: 🧪 Test Docker image
working-directory: ./docker
run: |
echo "::group::Testing Docker image"
# Run tests without auto-commit to avoid permission issues in CI
CI=true ./test-image.sh $IMAGE_NAME
echo "::endgroup::"
# The following jobs only run on tag pushes (i.e., releases)
build-artifacts:
name: 📦 Build Artifacts (${{ matrix.build }})
if: startsWith(github.ref, 'refs/tags/')
needs: build-and-test
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- build: linux-amd64
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
binary_name: git-iris
- build: linux-arm64
os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
binary_name: git-iris
- build: windows-gnu
os: windows-latest
target: x86_64-pc-windows-gnu
binary_name: git-iris.exe
- build: macos-arm64
os: macos-latest
target: aarch64-apple-darwin
binary_name: git-iris
steps:
- name: 📥 Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🦀 Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: 📦 Rust cache
uses: Swatinem/rust-cache@v2
with:
# Let action generate cache keys automatically
workspaces: ". -> target/${{ matrix.target }}/release"
# Additional cache suffix for release builds
cache-on-failure: true
- name: 🔨 Build release binary
run: |
echo "::group::Building release binary (${{ matrix.build }})"
cargo build --verbose --locked --release --target ${{ matrix.target }}
echo "::endgroup::"
- name: 📤 Upload artifact
uses: actions/upload-artifact@v4
with:
name: git-iris-${{ matrix.build }}
path: ./target/${{ matrix.target }}/release/${{ matrix.binary_name }}
if-no-files-found: error
retention-days: 1
build-packages:
name: 📦 Build Packages
if: startsWith(github.ref, 'refs/tags/')
needs: build-and-test
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- build: linux-amd64
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
arch_suffix: amd64
rpm_arch: x86_64
rust_flags: "-C target-cpu=x86-64"
outputs:
version: ${{ steps.get_version.outputs.VERSION }}
steps:
- name: 📥 Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🏷️ Get version
id: get_version
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
- name: 🦀 Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: 📦 Setup cross-compilation (ARM64)
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
- name: 📦 Rust cache
uses: Swatinem/rust-cache@v2
with:
# Let action generate cache keys automatically
workspaces: "."
cache-on-failure: true
# Build DEB package
- name: 📦 Install cargo-deb
run: cargo install cargo-deb
- name: 🔨 Build .deb package
env:
RUSTFLAGS: ${{ matrix.rust_flags }}
run: |
echo "::group::Building .deb package for ${{ matrix.target }}"
cargo deb --target ${{ matrix.target }}
echo "::endgroup::"
- name: 📤 Upload DEB artifact
uses: actions/upload-artifact@v4
with:
name: git-iris-deb-${{ matrix.arch_suffix }}
path: ./target/${{ matrix.target }}/debian/git-iris_${{ steps.get_version.outputs.VERSION }}-1_${{ matrix.arch_suffix }}.deb
if-no-files-found: error
retention-days: 1
# Build RPM package
- name: 📦 Install cargo-generate-rpm
run: cargo install cargo-generate-rpm
- name: 🔨 Build Release Binary for RPM
env:
RUSTFLAGS: ${{ matrix.rust_flags }}
run: cargo build --release --target ${{ matrix.target }}
- name: 🔨 Build .rpm package
env:
RUSTFLAGS: ${{ matrix.rust_flags }}
run: |
echo "::group::Building .rpm package for ${{ matrix.target }}"
cargo generate-rpm --target ${{ matrix.target }}
echo "::endgroup::"
- name: 📤 Upload RPM artifact
uses: actions/upload-artifact@v4
with:
name: git-iris-rpm-${{ matrix.arch_suffix }}
path: ./target/${{ matrix.target }}/generate-rpm/git-iris-${{ steps.get_version.outputs.VERSION }}-1.${{ matrix.rpm_arch }}.rpm
if-no-files-found: error
retention-days: 1
# Upload man page as artifact (only once)
- name: 📤 Upload man page artifact
if: matrix.build == 'linux-amd64'
uses: actions/upload-artifact@v4
with:
name: git-iris-man
path: ./git-iris.1
if-no-files-found: error
retention-days: 1
# Publish Docker image for releases
docker-publish:
name: 🐳 Publish Docker Image
if: startsWith(github.ref, 'refs/tags/')
needs: [build-and-test, docker-build-and-test, build-packages]
runs-on: ubuntu-latest
env:
REGISTRY: docker.io
IMAGE_NAME: hyperb1iss/git-iris
steps:
- name: 📥 Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🏷️ Get version
id: get_version
run: |
VERSION=${GITHUB_REF_NAME#v}
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "Building Docker image for version $VERSION"
- name: 🔑 Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: 🔍 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 🔨 Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/Dockerfile
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.get_version.outputs.VERSION }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: 🎉 Docker image published
run: |
echo "✨ Successfully published Docker images:"
echo " • ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
echo " • ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.get_version.outputs.VERSION }}"
# Add crates.io publishing
cargo-publish:
name: 📦 Publish to crates.io
if: startsWith(github.ref, 'refs/tags/')
needs: [build-artifacts, build-packages]
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🦀 Install Rust
uses: dtolnay/rust-toolchain@stable
- name: 📦 Rust cache
uses: Swatinem/rust-cache@v2
- name: 🔑 Setup crates.io token
run: cargo login ${{ secrets.CRATES_IO_TOKEN }}
- name: 📤 Publish to crates.io
run: |
echo "::group::Publishing to crates.io"
cargo publish
echo "::endgroup::"
- name: 🎉 crates.io publish successful
run: echo "✨ Successfully published to crates.io"
create-release:
name: 🚀 Create GitHub Release
if: startsWith(github.ref, 'refs/tags/')
needs: [build-artifacts, build-packages, docker-publish, cargo-publish]
runs-on: ubuntu-latest
permissions:
contents: write
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: 🔨 Prepare release assets
run: |
echo "::group::Preparing release assets"
mkdir -p release-assets
# Rename binaries with platform suffix from artifact directory names
for dir in ./artifacts/git-iris-*/; do
artifact_name=$(basename "$dir")
if [ -f "$dir/git-iris" ]; then
cp "$dir/git-iris" "./release-assets/${artifact_name}"
elif [ -f "$dir/git-iris.exe" ]; then
cp "$dir/git-iris.exe" "./release-assets/${artifact_name}.exe"
fi
done
# Copy non-binary artifacts (deb, rpm, man page) as-is
find ./artifacts -type f \( -name "*.deb" -o -name "*.rpm" -o -name "*.1" \) -exec cp {} ./release-assets/ \;
ls -la ./release-assets
echo "::endgroup::"
- name: 🏷️ Get previous tag
id: prev_tag
run: |
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
echo "tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT
echo "Previous tag: $PREVIOUS_TAG"
- name: 🔧 Prepare git-iris binary
run: |
chmod +x ./artifacts/git-iris-linux-amd64/git-iris
echo "Binary ready: ./artifacts/git-iris-linux-amd64/git-iris"
./artifacts/git-iris-linux-amd64/git-iris --version
- name: 📥 Download release notes from release workflow
id: download_notes
if: ${{ inputs.release_run_id != '' }}
uses: actions/download-artifact@v4
with:
name: release-notes
run-id: ${{ inputs.release_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: 📝 Generate release notes (fallback)
if: steps.download_notes.outcome != 'success' && steps.prev_tag.outputs.tag != ''
uses: ./
with:
command: release-notes
from: ${{ steps.prev_tag.outputs.tag }}
to: ${{ github.ref_name }}
version-name: ${{ github.ref_name }}
provider: anthropic
model: claude-opus-4-5-20251101
api-key: ${{ secrets.ANTHROPIC_API_KEY }}
output-file: RELEASE_NOTES.md
binary-path: ./artifacts/git-iris-linux-amd64/git-iris
- name: 📝 Fallback release notes (no tags)
if: steps.prev_tag.outputs.tag == ''
run: |
echo "# Release ${{ github.ref_name }}" > RELEASE_NOTES.md
echo "" >> RELEASE_NOTES.md
echo "See [commit history](https://github.com/hyperb1iss/git-iris/commits/${{ github.ref_name }}) for changes." >> RELEASE_NOTES.md
- name: 🚀 Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: Release ${{ github.ref_name }}
draft: false
prerelease: false
files: ./release-assets/*
body_path: RELEASE_NOTES.md
# Auto-update major version tag (v1, v2, etc.) for GitHub Action users
update-major-tag:
name: 🏷️ Update Major Version Tag
if: startsWith(github.ref, 'refs/tags/v')
needs: create-release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: 📥 Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🏷️ Update major version tag
run: |
# Extract major version from tag (v1.2.3 -> v1)
FULL_TAG="${GITHUB_REF_NAME}"
MAJOR_TAG=$(echo "$FULL_TAG" | sed -E 's/^(v[0-9]+).*/\1/')
echo "Full tag: $FULL_TAG"
echo "Major tag: $MAJOR_TAG"
# Configure git
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Force update the major version tag
git tag -fa "$MAJOR_TAG" -m "Update $MAJOR_TAG to $FULL_TAG"
git push origin "$MAJOR_TAG" --force
echo "✨ Updated $MAJOR_TAG to point to $FULL_TAG"
echo "Users can now use: uses: hyperb1iss/git-iris@$MAJOR_TAG"
# Update Homebrew tap with new release
update-homebrew:
name: 🍺 Update Homebrew Tap
if: startsWith(github.ref, 'refs/tags/v')
needs: create-release
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout tap repo
uses: actions/checkout@v4
with:
repository: hyperb1iss/homebrew-tap
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: homebrew-tap
- name: 🔢 Calculate SHA256 checksums
id: sha
run: |
VERSION="${GITHUB_REF_NAME#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
MACOS_ARM64=$(curl -fsSL "https://github.com/hyperb1iss/git-iris/releases/download/${GITHUB_REF_NAME}/git-iris-macos-arm64" | shasum -a 256 | cut -d' ' -f1)
LINUX_ARM64=$(curl -fsSL "https://github.com/hyperb1iss/git-iris/releases/download/${GITHUB_REF_NAME}/git-iris-linux-arm64" | shasum -a 256 | cut -d' ' -f1)
LINUX_AMD64=$(curl -fsSL "https://github.com/hyperb1iss/git-iris/releases/download/${GITHUB_REF_NAME}/git-iris-linux-amd64" | shasum -a 256 | cut -d' ' -f1)
SOURCE=$(curl -fsSL "https://github.com/hyperb1iss/git-iris/archive/refs/tags/${GITHUB_REF_NAME}.tar.gz" | shasum -a 256 | cut -d' ' -f1)
echo "macos_arm64=$MACOS_ARM64" >> $GITHUB_OUTPUT
echo "linux_arm64=$LINUX_ARM64" >> $GITHUB_OUTPUT
echo "linux_amd64=$LINUX_AMD64" >> $GITHUB_OUTPUT
echo "source=$SOURCE" >> $GITHUB_OUTPUT
- name: 📝 Generate formula
run: |
cat > homebrew-tap/Formula/git-iris.rb << 'EOF'
# typed: false
# frozen_string_literal: true
class GitIris < Formula
desc "An intelligent agent that understands your code and crafts perfect Git artifacts"
homepage "https://github.com/hyperb1iss/git-iris"
license "Apache-2.0"
version "${{ steps.sha.outputs.version }}"
on_macos do
on_arm do
url "https://github.com/hyperb1iss/git-iris/releases/download/v#{version}/git-iris-macos-arm64"
sha256 "${{ steps.sha.outputs.macos_arm64 }}"
end
on_intel do
url "https://github.com/hyperb1iss/git-iris/archive/refs/tags/v#{version}.tar.gz"
sha256 "${{ steps.sha.outputs.source }}"
depends_on "rust" => :build
end
end
on_linux do
on_arm do
url "https://github.com/hyperb1iss/git-iris/releases/download/v#{version}/git-iris-linux-arm64"
sha256 "${{ steps.sha.outputs.linux_arm64 }}"
end
on_intel do
url "https://github.com/hyperb1iss/git-iris/releases/download/v#{version}/git-iris-linux-amd64"
sha256 "${{ steps.sha.outputs.linux_amd64 }}"
end
end
def install
if build.with?("rust")
system "cargo", "install", *std_cargo_args
else
bin.install Dir["git-iris*"].first => "git-iris"
end
end
test do
assert_match "git-iris #{version}", shell_output("#{bin}/git-iris --version")
end
end
EOF
- name: 🚀 Push to tap
run: |
cd homebrew-tap
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/git-iris.rb
git commit -m "git-iris ${{ steps.sha.outputs.version }}"
git push