name: Release

on:
  push:
    tags: ["v*"]
  # Manual re-publish of the Docker image for an existing release, without
  # rebuilding binaries or cutting a new version. Runs only the docker (+
  # homebrew) jobs against the given tag's already-published release assets.
  workflow_dispatch:
    inputs:
      tag:
        description: "Existing release tag to (re)build + push the Docker image for, e.g. v0.6.9"
        required: true
        type: string

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always

jobs:
  build:
    # Binaries are only built when a tag is pushed. A manual dispatch reuses
    # the existing release's binaries, so it skips this job entirely.
    if: github.event_name == 'push'
    permissions:
      contents: read
    name: Build ${{ matrix.target }}
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-apple-darwin
            os: macos-latest
          - target: aarch64-apple-darwin
            os: macos-latest
          # Linux targets pin to ubuntu-22.04 (glibc 2.35), NOT ubuntu-latest
          # (24.04 = glibc 2.39): glibc is forward- but not backward-compatible,
          # so binaries built on 24.04 require GLIBC_2.38 and won't start on
          # older LTS distros (Debian 12, Ubuntu 22.04). The aarch64 cross
          # toolchain tracks the runner's distro too, so this lowers both
          # targets. See #73. Don't bump to ubuntu-latest.
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-22.04
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-22.04
          # musl static builds: glibc-independent, run on ANY Linux (Alpine,
          # Amazon Linux 2023, RHEL 9, old distros) where the gnu builds can't.
          # cargo-zigbuild links statically, so the host glibc is irrelevant —
          # ubuntu-latest is fine. See #73.
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
          - target: aarch64-unknown-linux-musl
            os: ubuntu-latest
          - target: x86_64-pc-windows-msvc
            os: windows-latest

    steps:
      - uses: actions/checkout@v5

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

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

      # Cross-compilation support for aarch64-linux
      # boring-sys2 builds BoringSSL from C source via cmake — needs cross-compiler + cmake
      - name: Install cross-compilation tools
        if: matrix.target == 'aarch64-unknown-linux-gnu'
        run: |
          sudo apt-get update
          sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu cmake
          echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
          echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
          echo "CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++" >> $GITHUB_ENV
          echo "CMAKE_SYSTEM_NAME=Linux" >> $GITHUB_ENV
          echo "CMAKE_SYSTEM_PROCESSOR=aarch64" >> $GITHUB_ENV

      # BoringSSL build tools for native targets
      - name: Install cmake
        if: matrix.target != 'aarch64-unknown-linux-gnu' && runner.os == 'Linux'
        run: sudo apt-get update && sudo apt-get install -y cmake

      - name: Install NASM (Windows)
        if: runner.os == 'Windows'
        run: |
          choco install nasm -y
          echo "C:\Program Files\NASM" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append

      # musl static builds use zig as the C/C++ cross-compiler for BoringSSL,
      # driven by cargo-zigbuild. Build scripts (bindgen) still run as the glibc
      # host so libclang loads fine; the linked output is fully static and runs
      # on any Linux regardless of glibc. (A native Alpine build can't do this —
      # its static build scripts can't dlopen libclang.)
      - name: Setup zig + cargo-zigbuild (musl)
        if: contains(matrix.target, 'musl')
        run: |
          python3 -m pip install --user --break-system-packages ziglang
          mkdir -p "$HOME/.local/bin"
          printf '#!/bin/sh\nexec python3 -m ziglang "$@"\n' > "$HOME/.local/bin/zig"
          chmod +x "$HOME/.local/bin/zig"
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"
          "$HOME/.local/bin/zig" version
          cargo install cargo-zigbuild --locked

      - name: Build
        shell: bash
        run: |
          if [[ "${{ matrix.target }}" == *-musl ]]; then
            cargo zigbuild --release --target ${{ matrix.target }}
          else
            cargo build --release --target ${{ matrix.target }}
          fi

      - name: Package
        shell: bash
        run: |
          tag="${GITHUB_REF#refs/tags/}"
          staging="webclaw-${tag}-${{ matrix.target }}"
          mkdir "$staging"
          # Fail loud if any binary is missing. A silent `|| true` on the
          # copy was how v0.4.0 shipped tarballs that lacked webclaw-server —
          # don't repeat that mistake. If a future binary gets renamed or
          # removed, this step should scream, not quietly publish an
          # incomplete release.

          if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
            cp target/${{ matrix.target }}/release/webclaw.exe "$staging/"
            cp target/${{ matrix.target }}/release/webclaw-mcp.exe "$staging/"
            cp target/${{ matrix.target }}/release/webclaw-server.exe "$staging/"
            cp README.md LICENSE "$staging/"
            7z a -tzip "$staging.zip" "$staging"
            echo "ASSET=$staging.zip" >> $GITHUB_ENV
          else
            cp target/${{ matrix.target }}/release/webclaw "$staging/"
            cp target/${{ matrix.target }}/release/webclaw-mcp "$staging/"
            cp target/${{ matrix.target }}/release/webclaw-server "$staging/"
            cp README.md LICENSE "$staging/"
            tar czf "$staging.tar.gz" "$staging"
            echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV
          fi

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

  release:
    name: Release
    if: github.event_name == 'push'
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/download-artifact@v5
        with:
          path: artifacts

      - name: Compute checksums
        run: |
          cd artifacts
          find . -name '*.tar.gz' -exec mv {} . \;
          find . -name '*.zip' -exec mv {} . \;
          sha256sum *.tar.gz *.zip > SHA256SUMS 2>/dev/null || sha256sum * > SHA256SUMS
          cat SHA256SUMS

      - name: Create GitHub Release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          tag="${GITHUB_REF#refs/tags/}"
          # SemVer prerelease tags (e.g. v1.2.3-rc1) contain a hyphen — mark
          # them as prereleases so they never surface as the stable release.
          prerelease=""
          case "$tag" in *-*) prerelease="--prerelease" ;; esac
          gh release create "$tag" \
            artifacts/*.tar.gz \
            artifacts/*.zip \
            artifacts/SHA256SUMS \
            --repo "$GITHUB_REPOSITORY" \
            $prerelease \
            --generate-notes

  docker:
    name: Docker
    needs: release
    # Runs after a successful release on tag push, or standalone via
    # workflow_dispatch to (re)publish an existing tag's image. `always()` lets
    # it run even though `release` is skipped on a manual dispatch.
    if: ${{ always() && (github.event_name == 'workflow_dispatch' || needs.release.result == 'success') }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v5

      - uses: docker/setup-qemu-action@v3
        with:
          platforms: arm64

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # The pushed tag, or the workflow_dispatch input for a manual re-publish.
      - name: Resolve tag
        id: tag
        run: echo "tag=${{ github.event.inputs.tag || github.ref_name }}" >> "$GITHUB_OUTPUT"

      # Download pre-built binaries into TARGETARCH-named dirs (amd64/arm64) so
      # a single multi-platform build picks the matching binary per platform.
      - name: Download release binaries
        run: |
          tag="${{ steps.tag.outputs.tag }}"
          declare -A arch=( [x86_64-unknown-linux-gnu]=amd64 [aarch64-unknown-linux-gnu]=arm64 )
          for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
            dir="webclaw-${tag}-${target}"
            curl -sSL "https://github.com/0xMassi/webclaw/releases/download/${tag}/${dir}.tar.gz" -o "${target}.tar.gz"
            tar xzf "${target}.tar.gz"
            a="${arch[$target]}"
            mkdir -p "binaries-${a}"
            cp "${dir}/webclaw" "${dir}/webclaw-mcp" "${dir}/webclaw-server" "binaries-${a}/"
            chmod +x "binaries-${a}"/*
          done
          ls -laR binaries-*/

      # One atomic multi-platform build + push. buildx assembles a single
      # manifest list and pushes it in one shot, so there is no separate
      # `imagetools create` step to race GHCR's read-after-write (that is what
      # failed before: "v0.6.9-arm64: not found"). Provenance/SBOM attestations
      # are disabled so each platform entry stays a plain image manifest.
      - name: Build and push
        run: |
          tag="${{ steps.tag.outputs.tag }}"
          # Only move :latest for stable tags. Prereleases (hyphenated, e.g.
          # v1.2.3-rc1) still publish :${tag} for testing but must not clobber
          # :latest.
          latest=""
          case "$tag" in *-*) ;; *) latest="-t ghcr.io/0xmassi/webclaw:latest" ;; esac
          docker buildx build -f Dockerfile.ci \
            --platform linux/amd64,linux/arm64 \
            --provenance=false --sbom=false \
            -t "ghcr.io/0xmassi/webclaw:${tag}" \
            $latest \
            --push .

  homebrew:
    name: Update Homebrew
    needs: [release, docker]
    # Runs once Docker succeeds, on both tag push and manual re-publish.
    # Skipped for prereleases (hyphenated tags like v1.2.3-rc1) so the formula
    # keeps pointing at the latest stable, never an rc.
    if: ${{ always() && needs.docker.result == 'success' && !contains(github.event.inputs.tag || github.ref_name, '-') }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - name: Compute all checksums and update formula
        env:
          COMMITTER_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
        run: |
          tag="${{ github.event.inputs.tag || github.ref_name }}"
          base="https://github.com/0xMassi/webclaw/releases/download/${tag}"

          # Download all tarballs (Linux + macOS) and compute SHAs
          for target in aarch64-apple-darwin x86_64-apple-darwin aarch64-unknown-linux-gnu x86_64-unknown-linux-gnu; do
            curl -sSL "${base}/webclaw-${tag}-${target}.tar.gz" -o "${target}.tar.gz"
          done

          SHA_MAC_ARM=$(sha256sum aarch64-apple-darwin.tar.gz | cut -d' ' -f1)
          SHA_MAC_X86=$(sha256sum x86_64-apple-darwin.tar.gz | cut -d' ' -f1)
          SHA_LINUX_ARM=$(sha256sum aarch64-unknown-linux-gnu.tar.gz | cut -d' ' -f1)
          SHA_LINUX_X86=$(sha256sum x86_64-unknown-linux-gnu.tar.gz | cut -d' ' -f1)

          echo "macOS arm64:  $SHA_MAC_ARM"
          echo "macOS x86_64: $SHA_MAC_X86"
          echo "Linux arm64:  $SHA_LINUX_ARM"
          echo "Linux x86_64: $SHA_LINUX_X86"

          # Generate formula
          cat > webclaw.rb << FORMULA
          class Webclaw < Formula
            desc "The fastest web scraper for AI agents. 67% fewer tokens. Sub-ms extraction."
            homepage "https://webclaw.io"
            license "AGPL-3.0"
            version "${tag#v}"

            on_macos do
              if Hardware::CPU.arm?
                url "${base}/webclaw-${tag}-aarch64-apple-darwin.tar.gz"
                sha256 "${SHA_MAC_ARM}"
              else
                url "${base}/webclaw-${tag}-x86_64-apple-darwin.tar.gz"
                sha256 "${SHA_MAC_X86}"
              end
            end

            on_linux do
              if Hardware::CPU.arm?
                url "${base}/webclaw-${tag}-aarch64-unknown-linux-gnu.tar.gz"
                sha256 "${SHA_LINUX_ARM}"
              else
                url "${base}/webclaw-${tag}-x86_64-unknown-linux-gnu.tar.gz"
                sha256 "${SHA_LINUX_X86}"
              end
            end

            def install
              bin.install "webclaw"
              bin.install "webclaw-mcp"
              bin.install "webclaw-server"
            end

            test do
              assert_match "webclaw", shell_output("#{bin}/webclaw --version")
            end
          end
          FORMULA

          # Remove leading whitespace from heredoc
          sed -i 's/^          //' webclaw.rb

          # Push to homebrew tap
          git clone "https://x-access-token:${COMMITTER_TOKEN}@github.com/0xMassi/homebrew-webclaw.git" tap
          cp webclaw.rb tap/Formula/webclaw.rb
          cd tap
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add Formula/webclaw.rb
          git diff --cached --quiet || git commit -m "Update webclaw to ${tag}"
          git push
