etz 0.3.0

CLI for coordinating git worktrees across multi-repo parent directories.
Documentation
name: release

on:
  push:
    tags:
      - "v*"

permissions:
  contents: write

env:
  BINARY_NAME: etz
  CARGO_TERM_COLOR: always
  RELEASE_REPO: snipeship/etz
  HOMEBREW_TAP_REPO: snipeship/homebrew-tap

jobs:
  verify-tag:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.meta.outputs.version }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - id: meta
        name: Validate tag against Cargo version
        shell: bash
        run: |
          set -euo pipefail
          VERSION_FROM_TAG="${GITHUB_REF_NAME#v}"
          VERSION_FROM_CARGO="$(awk -F'"' '
            $0 ~ /^\[package\]/ { in_pkg=1; next }
            $0 ~ /^\[/ && in_pkg { in_pkg=0 }
            in_pkg && $1 ~ /^version = / { print $2; exit }
          ' Cargo.toml)"

          if [ -z "$VERSION_FROM_CARGO" ]; then
            echo "::error::Unable to parse [package].version from Cargo.toml"
            exit 1
          fi

          if [ "$VERSION_FROM_TAG" != "$VERSION_FROM_CARGO" ]; then
            echo "::error::Tag version v${VERSION_FROM_TAG} does not match Cargo.toml version ${VERSION_FROM_CARGO}"
            exit 1
          fi

          echo "version=$VERSION_FROM_CARGO" >> "$GITHUB_OUTPUT"

  test:
    needs: verify-tag
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

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

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2

      - name: Run tests
        run: cargo test --locked

  build:
    needs: [verify-tag, test]
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
          - os: macos-14
            target: x86_64-apple-darwin
          - os: macos-14
            target: aarch64-apple-darwin
    runs-on: ${{ matrix.os }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

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

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2

      - name: Build binary
        shell: bash
        run: cargo build --locked --release --target "${{ matrix.target }}"

      - name: Package release tarball
        shell: bash
        run: |
          set -euo pipefail
          VERSION="${{ needs.verify-tag.outputs.version }}"
          ARCHIVE_NAME="${BINARY_NAME}-${VERSION}-${{ matrix.target }}.tar.gz"
          mkdir -p dist
          tar -C "target/${{ matrix.target }}/release" -czf "dist/${ARCHIVE_NAME}" "${BINARY_NAME}"
          shasum -a 256 "dist/${ARCHIVE_NAME}" > "dist/SHA256SUMS-${{ matrix.target }}"

      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: release-${{ matrix.target }}
          path: |
            dist/*.tar.gz
            dist/SHA256SUMS-${{ matrix.target }}
          if-no-files-found: error

  release:
    needs: [verify-tag, build]
    runs-on: ubuntu-latest
    steps:
      - name: Download release artifacts
        uses: actions/download-artifact@v4
        with:
          path: dist
          merge-multiple: true

      - name: Assemble checksum manifest
        shell: bash
        run: |
          set -euo pipefail
          cat dist/SHA256SUMS-* | sort -k2 > dist/SHA256SUMS

      - name: Publish GitHub release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: v${{ needs.verify-tag.outputs.version }}
          generate_release_notes: true
          files: |
            dist/*.tar.gz
            dist/SHA256SUMS

  publish-cargo:
    needs: [verify-tag, test]
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

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

      - id: already-published
        name: Check crates.io for existing version
        shell: bash
        run: |
          set -euo pipefail
          VERSION="${{ needs.verify-tag.outputs.version }}"
          STATUS="$(curl -sS -o /dev/null -w "%{http_code}" "https://crates.io/api/v1/crates/${BINARY_NAME}/${VERSION}")"
          if [ "$STATUS" = "200" ]; then
            echo "published=true" >> "$GITHUB_OUTPUT"
          else
            echo "published=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Ensure crates.io token is configured
        if: steps.already-published.outputs.published == 'false'
        shell: bash
        run: |
          if [ -z "${CARGO_REGISTRY_TOKEN:-}" ]; then
            echo "::error::Missing CARGO_REGISTRY_TOKEN secret"
            exit 1
          fi
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

      - name: Publish crate
        if: steps.already-published.outputs.published == 'false'
        run: cargo publish --locked --token "${CARGO_REGISTRY_TOKEN}"
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

  publish-homebrew:
    needs: [verify-tag, release]
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Ensure tap token is configured
        shell: bash
        run: |
          if [ -z "${HOMEBREW_TAP_GITHUB_TOKEN:-}" ]; then
            echo "::error::Missing HOMEBREW_TAP_GITHUB_TOKEN secret"
            exit 1
          fi
        env:
          HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}

      - name: Download release artifacts
        uses: actions/download-artifact@v4
        with:
          path: dist
          merge-multiple: true

      - name: Assemble checksum manifest
        shell: bash
        run: |
          set -euo pipefail
          cat dist/SHA256SUMS-* | sort -k2 > dist/SHA256SUMS

      - name: Render formula
        shell: bash
        run: |
          set -euo pipefail
          ./scripts/render-homebrew-formula.sh \
            "${{ needs.verify-tag.outputs.version }}" \
            "${RELEASE_REPO}" \
            "dist/SHA256SUMS" \
            "etz.rb"

      - name: Clone tap repository
        shell: bash
        run: |
          set -euo pipefail
          git clone "https://x-access-token:${HOMEBREW_TAP_GITHUB_TOKEN}@github.com/${HOMEBREW_TAP_REPO}.git" tap
        env:
          HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}

      - name: Copy formula into tap repository
        shell: bash
        run: |
          set -euo pipefail
          mkdir -p tap/Formula
          cp etz.rb tap/Formula/etz.rb

      - name: Commit and push tap update
        shell: bash
        working-directory: tap
        run: |
          set -euo pipefail

          if [ -z "$(git status --porcelain -- Formula/etz.rb)" ]; then
            echo "Formula already up-to-date."
            exit 0
          fi

          DEFAULT_BRANCH="$(git remote show origin | sed -n '/HEAD branch/s/.*: //p')"
          if [ -z "$DEFAULT_BRANCH" ]; then
            echo "::error::Unable to determine tap default branch"
            exit 1
          fi

          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add Formula/etz.rb
          git commit -m "etz ${{ needs.verify-tag.outputs.version }}"
          git push origin "HEAD:${DEFAULT_BRANCH}"