yaak 0.1.3

Translate natural language to bash commands using an OpenAI-compatible LLM
name: CI

on:
  push:
    branches: [main]
    tags: ["v*"]
  pull_request:
    branches: [main]

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1

jobs:
  # ──────────────────────────────────────────────
  # Lint: formatting + clippy
  # ──────────────────────────────────────────────
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy

      - uses: Swatinem/rust-cache@v2

      - name: Check formatting
        run: cargo fmt --all -- --check

      - name: Run clippy
        run: cargo clippy --all-targets --all-features -- -D warnings

  # ──────────────────────────────────────────────
  # Test: unit + integration tests
  # ──────────────────────────────────────────────
  test:
    name: Test (${{ matrix.os }})
    needs: lint
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - uses: Swatinem/rust-cache@v2

      - name: Run tests
        run: cargo test --all-features --verbose

  # ──────────────────────────────────────────────
  # Publish: push crate to crates.io
  # ──────────────────────────────────────────────
  publish:
    name: Publish to crates.io
    needs: test
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Verify version matches tag
        shell: bash
        run: |
          CARGO_VERSION="v$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')"
          TAG_VERSION="${GITHUB_REF#refs/tags/}"
          if [ "$CARGO_VERSION" != "$TAG_VERSION" ]; then
            echo "::error::Cargo.toml version ($CARGO_VERSION) does not match tag ($TAG_VERSION)"
            exit 1
          fi

      - name: Publish to crates.io
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: cargo publish --allow-dirty

  # ──────────────────────────────────────────────
  # Build: cross-platform release binaries
  # ──────────────────────────────────────────────
  build:
    name: Build (${{ matrix.target }})
    needs: test
    if: startsWith(github.ref, 'refs/tags/v')
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
            archive: tar.gz

          # TODO: re-enable once cross-compilation OpenSSL issue is resolved
          # - target: aarch64-unknown-linux-gnu
          #   os: ubuntu-latest
          #   archive: tar.gz

          - target: x86_64-apple-darwin
            os: macos-latest
            archive: tar.gz

          - target: aarch64-apple-darwin
            os: macos-latest
            archive: tar.gz

          - target: x86_64-pc-windows-msvc
            os: windows-latest
            archive: zip

    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: Install cross-compilation deps (aarch64-linux)
        if: matrix.target == 'aarch64-unknown-linux-gnu'
        run: |
          sudo apt-get update
          sudo apt-get install -y gcc-aarch64-linux-gnu
          echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV

      - uses: Swatinem/rust-cache@v2
        with:
          key: ${{ matrix.target }}

      - name: Build release binary
        run: cargo build --release --target ${{ matrix.target }}

      - name: Get version from tag
        id: version
        shell: bash
        run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT

      - name: Package (unix)
        if: matrix.archive == 'tar.gz'
        shell: bash
        run: |
          STAGING="yaak-${{ steps.version.outputs.version }}-${{ matrix.target }}"
          mkdir "$STAGING"
          cp target/${{ matrix.target }}/release/yaak "$STAGING/"
          cp README.md LICENSE* "$STAGING/" 2>/dev/null || true
          tar czf "$STAGING.tar.gz" "$STAGING"
          echo "ASSET=$STAGING.tar.gz" >> $GITHUB_ENV

      - name: Package (windows)
        if: matrix.archive == 'zip'
        shell: bash
        run: |
          STAGING="yaak-${{ steps.version.outputs.version }}-${{ matrix.target }}"
          mkdir "$STAGING"
          cp target/${{ matrix.target }}/release/yaak.exe "$STAGING/"
          cp README.md LICENSE* "$STAGING/" 2>/dev/null || true
          7z a "$STAGING.zip" "$STAGING"
          echo "ASSET=$STAGING.zip" >> $GITHUB_ENV

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.target }}
          path: ${{ env.ASSET }}

  # ──────────────────────────────────────────────
  # Release: create GitHub release with all binaries
  # ──────────────────────────────────────────────
  release:
    name: Release
    needs: build
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4

      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts

      - name: List artifacts
        run: find artifacts -type f | sort

      - name: Generate checksums
        shell: bash
        run: |
          cd artifacts
          find . -type f \( -name '*.tar.gz' -o -name '*.zip' \) \
            -exec sh -c 'sha256sum "$1" >> ../SHA256SUMS.txt' _ {} \;
          cd ..
          cat SHA256SUMS.txt

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true
          files: |
            artifacts/**/*.tar.gz
            artifacts/**/*.zip
            SHA256SUMS.txt

  # ──────────────────────────────────────────────
  # Homebrew: update tap formula after release
  # ──────────────────────────────────────────────
  homebrew:
    name: Update Homebrew tap
    needs: release
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Get version from tag
        id: version
        shell: bash
        run: |
          TAG="${GITHUB_REF#refs/tags/}"
          echo "tag=$TAG" >> $GITHUB_OUTPUT
          echo "version=${TAG#v}" >> $GITHUB_OUTPUT

      - name: Download macOS release tarballs and compute SHA256
        id: sha
        shell: bash
        run: |
          VERSION="${{ steps.version.outputs.tag }}"
          BASE_URL="https://github.com/hanneshapke/yaak/releases/download/${VERSION}"

          curl -fSL -o x86_64.tar.gz  "${BASE_URL}/yaak-${VERSION}-x86_64-apple-darwin.tar.gz"
          curl -fSL -o aarch64.tar.gz "${BASE_URL}/yaak-${VERSION}-aarch64-apple-darwin.tar.gz"
          curl -fSL -o linux_x86_64.tar.gz "${BASE_URL}/yaak-${VERSION}-x86_64-unknown-linux-gnu.tar.gz"

          echo "sha_mac_x86=$(sha256sum x86_64.tar.gz | awk '{print $1}')" >> $GITHUB_OUTPUT
          echo "sha_mac_arm=$(sha256sum aarch64.tar.gz | awk '{print $1}')" >> $GITHUB_OUTPUT
          echo "sha_linux_x86=$(sha256sum linux_x86_64.tar.gz | awk '{print $1}')" >> $GITHUB_OUTPUT

      - name: Generate Homebrew formula
        shell: bash
        run: |
          VERSION="${{ steps.version.outputs.version }}"
          TAG="${{ steps.version.outputs.tag }}"
          cat > yaak.rb <<FORMULA
          class Yaak < Formula
            desc "Translate natural language to bash commands using an OpenAI-compatible LLM"
            homepage "https://www.hanneshapke.com/yaak/"
            version "${VERSION}"
            license "Apache-2.0"

            on_macos do
              if Hardware::CPU.arm?
                url "https://github.com/hanneshapke/yaak/releases/download/${TAG}/yaak-${TAG}-aarch64-apple-darwin.tar.gz"
                sha256 "${{ steps.sha.outputs.sha_mac_arm }}"
              else
                url "https://github.com/hanneshapke/yaak/releases/download/${TAG}/yaak-${TAG}-x86_64-apple-darwin.tar.gz"
                sha256 "${{ steps.sha.outputs.sha_mac_x86 }}"
              end
            end

            on_linux do
              if Hardware::CPU.intel?
                url "https://github.com/hanneshapke/yaak/releases/download/${TAG}/yaak-${TAG}-x86_64-unknown-linux-gnu.tar.gz"
                sha256 "${{ steps.sha.outputs.sha_linux_x86 }}"
              end
            end

            def install
              bin.install "yaak"
            end

            test do
              assert_match "yaak", shell_output("#{bin}/yaak --version")
            end
          end
          FORMULA
          # Remove leading whitespace from heredoc
          sed -i 's/^          //' yaak.rb
          cat yaak.rb

      - name: Push formula to Homebrew tap
        uses: dmnemec/copy_file_to_another_repo_action@main
        env:
          API_TOKEN_GITHUB: ${{ secrets.HOMEBREW_TAP_TOKEN }}
        with:
          source_file: yaak.rb
          destination_repo: hanneshapke/homebrew-yaak
          destination_folder: Formula
          destination_branch: main
          user_email: github-actions[bot]@users.noreply.github.com
          user_name: github-actions[bot]
          commit_message: "Update yaak formula to ${{ steps.version.outputs.tag }}"