git-ai 1.2.1

Git AI: Automates commit messages using ChatGPT. Stage your files, and Git AI generates the messages.
Documentation
name: CD

on:
  push:
    branches:
      - main
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: false

permissions:
  contents: read

env:
  CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
  GH_TOKEN: ${{ secrets.GH_TOKEN }}
  ACTIONS_RUNTIME_TOKEN: dummy
  CARGO_TERM_COLOR: always

jobs:
  artifact:
    runs-on: ${{ matrix.os }}
    continue-on-error: false
    permissions:
      contents: read
    strategy:
      matrix:
        include:
          - os: macos-latest
            target: x86_64-apple-darwin
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
          - os: macos-latest
            target: aarch64-apple-darwin
          - os: ubuntu-latest
            target: x86_64-unknown-linux-musl
    steps:
      - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
        with:
          persist-credentials: false
      - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
        with:
          cache-on-failure: true

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master
        with:
          toolchain: nightly
          targets: ${{ matrix.target }}

      # rust-toolchain.toml pins nightly-2026-06-15, which overrides the toolchain dtolnay
      # installs the target for. Add the target's std to the *pinned* toolchain so cross
      # builds don't fail with "can't find crate for `core`".
      - name: Add target std to the pinned toolchain
        env:
          TARGET: ${{ matrix.target }}
        run: |
          rustup show
          rustup target add "$TARGET"

      - name: Add x86_64-unknown-linux-musl target
        if: matrix.target == 'x86_64-unknown-linux-musl'
        run: |
          rustup target add x86_64-unknown-linux-musl
          sudo apt-get update && sudo apt-get install -y musl-tools

      - name: Install Dependencies for musl Target
        if: matrix.target == 'x86_64-unknown-linux-musl'
        run: |
          sudo apt-get update
          sudo apt-get install -y musl-tools musl-dev perl make pkg-config libssl-dev
          # Set up environment for musl compilation
          echo "CC_x86_64_unknown_linux_musl=musl-gcc" >> $GITHUB_ENV
          echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=musl-gcc" >> $GITHUB_ENV

      - name: Install Dependencies for Linux Target
        if: matrix.target == 'x86_64-unknown-linux-gnu'
        run: |
          sudo apt-get update
          sudo apt-get install -y pkg-config libssl-dev

      - name: Build for target
        run: |
          cargo build \
            -Z unstable-options \
            --profile release-with-debug \
            --artifact-dir bin \
            --target ${{ matrix.target }}

      - name: Upload and compress artifacts
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
        with:
          name: git-ai-${{ matrix.target }}
          if-no-files-found: error
          path: bin/git-*

  release:
    runs-on: ubuntu-latest
    needs: artifact
    # contents: write is required: this job pushes the release git tag and
    # publishes a GitHub release. It never commits to main.
    permissions:
      contents: write
    steps:
      # persist-credentials is intentionally left enabled (default): the later
      # "git push origin v<version>" tag-push step relies on the checkout-provided token.
      - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 # zizmor: ignore[artipacked]

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master
        with:
          toolchain: nightly

      - name: Configure git user name
        env:
          GH_ACTOR: ${{ github.actor }}
        run: git config user.name "$GH_ACTOR"

      - name: Configure git email
        env:
          GH_ACTOR: ${{ github.actor }}
        run: git config user.email "${GH_ACTOR}@users.noreply.github.com"

      # Deliberate versioning: publish the version committed in Cargo.toml (no auto-bump),
      # and only when that version has not been released yet. We push ONLY the tag (never a
      # commit to main), so the branch protection ruleset does not apply and no bypass is needed.
      - name: Determine version from Cargo.toml
        id: app
        run: echo "version=$(grep -m1 '^version' Cargo.toml | sed -E 's/.*\"([^\"]+)\".*/\1/')" >> "$GITHUB_OUTPUT"

      - name: Check whether this version is already released
        id: check
        env:
          APP_VERSION: ${{ steps.app.outputs.version }}
        run: |
          if git ls-remote --tags origin "refs/tags/v${APP_VERSION}" | grep -q .; then
            echo "release=false" >> "$GITHUB_OUTPUT"
            echo "v${APP_VERSION} already released; skipping publish."
          else
            echo "release=true" >> "$GITHUB_OUTPUT"
          fi

      - name: Publish to crates.io
        if: steps.check.outputs.release == 'true' && github.ref == 'refs/heads/main'
        # zizmor: ignore[use-trusted-publishing]
        # Intentionally uses CARGO_REGISTRY_TOKEN; migrating crates.io to OIDC
        # trusted publishing is out of scope for this change.
        run: cargo publish

      - name: Verify packaging (dry run)
        if: github.ref != 'refs/heads/main'
        # zizmor: ignore[use-trusted-publishing]
        run: cargo publish --dry-run

      - name: Tag and push the release tag
        if: steps.check.outputs.release == 'true' && github.ref == 'refs/heads/main'
        env:
          APP_VERSION: ${{ steps.app.outputs.version }}
        run: |
          git tag "v${APP_VERSION}"
          git push origin "v${APP_VERSION}"

      - name: Download all artifacts
        env:
          RUN_ID: ${{ github.run_id }}
        run: gh run download "$RUN_ID"

      - name: Zip each downloaded directory
        run: |
          for dir in $(ls -d git-ai-*); do
            tar -czf ${dir}.tar.gz ${dir}
          done

      - name: Uploads compressed artifacts
        if: steps.check.outputs.release == 'true' && github.ref == 'refs/heads/main'
        uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
        with:
          tag_name: v${{ steps.app.outputs.version }}
          fail_on_unmatched_files: true
          files: git-ai-*.tar.gz