CI/CD #250
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |