hunch 2.0.2

A media filename parser for movies, TV, and anime — built in Rust, inspired by guessit
Documentation
name: Release

on:
  push:
    tags: ["v*"]

env:
  CARGO_TERM_COLOR: always

# Default to least-privilege (#151). Only the jobs that actually need to write
# repository contents (uploading release assets) escalate to `contents: write`
# via per-job overrides below. Read-only jobs (verify-version, test, build,
# publish, homebrew) inherit this default.
permissions:
  contents: read

jobs:
  # Verify that the git tag matches Cargo.toml version.
  verify-version:
    name: Verify Version
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6
      - name: Check tag matches Cargo.toml version
        run: |
          TAG="${GITHUB_REF#refs/tags/v}"
          CRATE_VER=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
          if [ "$TAG" != "$CRATE_VER" ]; then
            echo "::error::Tag v$TAG does not match Cargo.toml version $CRATE_VER"
            exit 1
          fi
          echo "✅ Tag v$TAG matches Cargo.toml ($CRATE_VER)"

  # Cross-platform tests before shipping binaries.
  test:
    name: Test (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8  # stable
      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32  # v2
      - run: cargo test

  build:
    name: Build ${{ matrix.target }}
    needs: [verify-version, test]
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-24.04-arm
          - target: x86_64-apple-darwin
            os: macos-latest
          - target: aarch64-apple-darwin
            os: macos-14
          - target: x86_64-pc-windows-msvc
            os: windows-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8  # stable
        with:
          targets: ${{ matrix.target }}
      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32  # v2
        with:
          key: release-${{ matrix.target }}

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

      - name: Package (unix)
        if: runner.os != 'Windows'
        run: |
          mkdir hunch-${{ matrix.target }}
          cp target/${{ matrix.target }}/release/hunch hunch-${{ matrix.target }}/
          cp README.md LICENSE hunch-${{ matrix.target }}/
          tar czf hunch-${{ matrix.target }}.tar.gz hunch-${{ matrix.target }}

      - name: Package (windows)
        if: runner.os == 'Windows'
        shell: bash
        run: |
          mkdir hunch-${{ matrix.target }}
          cp target/${{ matrix.target }}/release/hunch.exe hunch-${{ matrix.target }}/
          cp README.md LICENSE hunch-${{ matrix.target }}/
          7z a hunch-${{ matrix.target }}.zip hunch-${{ matrix.target }}/

      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7
        with:
          name: hunch-${{ matrix.target }}
          path: |
            hunch-${{ matrix.target }}.tar.gz
            hunch-${{ matrix.target }}.zip
          if-no-files-found: ignore

  publish:
    name: Publish to crates.io
    needs: build
    runs-on: ubuntu-latest
    environment: crates-io
    # NOTE: `continue-on-error` was previously `true` to silently absorb the
    # "already uploaded" error on re-runs. We now detect that case explicitly
    # and fail loudly on any other publish error (#152). Genuine network /
    # auth / metadata failures will surface as workflow failures so the
    # operator can intervene before downstream consumers see drift between
    # the GitHub Release tag and the crates.io version.
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8  # stable
      - name: Publish (idempotent on "already uploaded")
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          set -o pipefail
          if cargo publish 2>&1 | tee /tmp/cargo-publish.log; then
            echo "✅ Published successfully"
            exit 0
          fi
          # Distinguish "this version is already on crates.io" (idempotent
          # success on re-run) from any other failure mode (auth, network,
          # missing metadata, semver drift, etc.).
          if grep -qE 'crate version .* is already uploaded|already exists on crates.io' /tmp/cargo-publish.log; then
            echo "::warning::Version already on crates.io — treating as idempotent success"
            exit 0
          fi
          echo "::error::cargo publish failed for a non-idempotent reason — see log above"
          exit 1

  github-release:
    name: GitHub Release
    needs: build
    runs-on: ubuntu-latest
    # Only this job needs write access — it uploads release assets and
    # creates the Release page (#151). All other jobs inherit the
    # workflow-level `contents: read` default.
    permissions:
      contents: write
    env:
      FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6

      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c  # v8
        with:
          path: artifacts
          merge-multiple: true

      - name: Extract release notes
        id: notes
        run: |
          TAG="${GITHUB_REF#refs/tags/v}"
          if [ -f RELEASE_NOTES.md ] && [ -s RELEASE_NOTES.md ]; then
            cp RELEASE_NOTES.md release_notes.md
            echo "generate=false" >> $GITHUB_OUTPUT
          else
            awk "/^## \\[${TAG}\\]/{found=1; next} /^## \\[/{if(found) exit} found" CHANGELOG.md > release_notes.md
            if [ ! -s release_notes.md ]; then
              echo "generate=true" >> $GITHUB_OUTPUT
            else
              echo "generate=false" >> $GITHUB_OUTPUT
            fi
          fi

      - name: Create GitHub Release
        uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda  # v3.0.0
        with:
          generate_release_notes: ${{ steps.notes.outputs.generate == 'true' }}
          body_path: ${{ steps.notes.outputs.generate != 'true' && 'release_notes.md' || '' }}
          files: |
            artifacts/*.tar.gz
            artifacts/*.zip

  homebrew:
    name: Update Homebrew Tap
    needs: github-release
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c  # v8
        with:
          path: artifacts
          merge-multiple: true

      - name: Compute SHA256 checksums
        id: sha
        run: |
          VERSION="${GITHUB_REF#refs/tags/v}"
          echo "version=${VERSION}" >> $GITHUB_OUTPUT
          echo "aarch64_mac=$(sha256sum artifacts/hunch-aarch64-apple-darwin.tar.gz | cut -d' ' -f1)" >> $GITHUB_OUTPUT
          echo "x86_64_mac=$(sha256sum artifacts/hunch-x86_64-apple-darwin.tar.gz | cut -d' ' -f1)" >> $GITHUB_OUTPUT
          echo "aarch64_linux=$(sha256sum artifacts/hunch-aarch64-unknown-linux-gnu.tar.gz | cut -d' ' -f1)" >> $GITHUB_OUTPUT
          echo "x86_64_linux=$(sha256sum artifacts/hunch-x86_64-unknown-linux-gnu.tar.gz | cut -d' ' -f1)" >> $GITHUB_OUTPUT

      - name: Checkout Homebrew tap
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6
        with:
          repository: lijunzh/homebrew-hunch
          token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
          path: homebrew-hunch

      - name: Write updated formula
        run: |
          VERSION="${{ steps.sha.outputs.version }}"
          cat > homebrew-hunch/Formula/hunch.rb << 'FORMULA'
          class Hunch < Formula
            desc "Fast, offline media filename parser — extract metadata from messy filenames"
            homepage "https://github.com/lijunzh/hunch"
            version "VERSION_PLACEHOLDER"
            license "MIT"

            on_macos do
              if Hardware::CPU.arm?
                url "https://github.com/lijunzh/hunch/releases/download/vVERSION_PLACEHOLDER/hunch-aarch64-apple-darwin.tar.gz"
                sha256 "SHA_AARCH64_MAC"
              else
                url "https://github.com/lijunzh/hunch/releases/download/vVERSION_PLACEHOLDER/hunch-x86_64-apple-darwin.tar.gz"
                sha256 "SHA_X86_64_MAC"
              end
            end

            on_linux do
              if Hardware::CPU.arm?
                url "https://github.com/lijunzh/hunch/releases/download/vVERSION_PLACEHOLDER/hunch-aarch64-unknown-linux-gnu.tar.gz"
                sha256 "SHA_AARCH64_LINUX"
              else
                url "https://github.com/lijunzh/hunch/releases/download/vVERSION_PLACEHOLDER/hunch-x86_64-unknown-linux-gnu.tar.gz"
                sha256 "SHA_X86_64_LINUX"
              end
            end

            def install
              bin.install Dir["**/hunch"].first
            end

            test do
              output = shell_output("#{bin}/hunch 'The.Matrix.1999.1080p.BluRay.x264-GROUP.mkv'")
              assert_match '"title": "The Matrix"', output
              assert_match '"year": 1999', output
            end
          end
          FORMULA

          # Strip leading whitespace from heredoc
          sed -i 's/^          //' homebrew-hunch/Formula/hunch.rb

          # Substitute placeholders
          sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" homebrew-hunch/Formula/hunch.rb
          sed -i "s/SHA_AARCH64_MAC/${{ steps.sha.outputs.aarch64_mac }}/g" homebrew-hunch/Formula/hunch.rb
          sed -i "s/SHA_X86_64_MAC/${{ steps.sha.outputs.x86_64_mac }}/g" homebrew-hunch/Formula/hunch.rb
          sed -i "s/SHA_AARCH64_LINUX/${{ steps.sha.outputs.aarch64_linux }}/g" homebrew-hunch/Formula/hunch.rb
          sed -i "s/SHA_X86_64_LINUX/${{ steps.sha.outputs.x86_64_linux }}/g" homebrew-hunch/Formula/hunch.rb

      - name: Push formula update
        run: |
          cd homebrew-hunch
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add Formula/hunch.rb
          git commit -m "hunch v${{ steps.sha.outputs.version }}"
          git push