name: Release

on:
  push:
    tags:
      - "v[0-9]+.[0-9]+.[0-9]+"
  workflow_dispatch:
    inputs:
      tag:
        description: "Release tag (e.g., v0.2.0)"
        required: true

permissions:
  contents: write

jobs:
  check:
    name: Pre-release checks
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v6
        with:
          ref: ${{ github.event.inputs.tag || github.ref }}

      - uses: dtolnay/rust-toolchain@master
        with:
          toolchain: "1.95"
          components: clippy

      - name: Install Linux dependencies
        run: sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev

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

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

  build:
    name: Build ${{ matrix.target }}
    needs: check
    runs-on: ${{ matrix.runner }}
    strategy:
      matrix:
        include:
          - target: aarch64-apple-darwin
            runner: macos-latest
          - target: x86_64-apple-darwin
            runner: macos-latest
          - target: x86_64-unknown-linux-gnu
            runner: ubuntu-22.04
          - target: aarch64-unknown-linux-gnu
            runner: ubuntu-22.04-arm
          - target: x86_64-unknown-linux-musl
            runner: ubuntu-22.04
          - target: aarch64-unknown-linux-musl
            runner: ubuntu-22.04-arm

    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          ref: ${{ github.event.inputs.tag || github.ref }}

      - name: Derive version
        id: version
        run: |
          TAG="${{ github.event.inputs.tag || github.ref_name }}"
          echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
          echo "tag=${TAG}" >> "$GITHUB_OUTPUT"

      - name: Verify version matches tag
        run: |
          CARGO_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
          TAG_VERSION="${{ steps.version.outputs.version }}"
          if [ "$CARGO_VERSION" != "$TAG_VERSION" ]; then
            echo "::error::Cargo.toml version ($CARGO_VERSION) does not match tag ($TAG_VERSION)"
            exit 1
          fi

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

      - name: Install Linux dependencies
        if: runner.os == 'Linux'
        run: sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev

      - name: Install musl tooling
        if: contains(matrix.target, 'musl')
        run: sudo apt-get install -y musl-tools

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

      - name: Package binary
        shell: bash
        run: |
          VERSION="${{ steps.version.outputs.version }}"
          ARCHIVE="purple-${VERSION}-${{ matrix.target }}.tar.gz"
          cd target/${{ matrix.target }}/release
          tar -czf ../../../"${ARCHIVE}" purple
          cd ../../..
          echo "ARCHIVE=${ARCHIVE}" >> "$GITHUB_ENV"
          if command -v sha256sum > /dev/null 2>&1; then
            sha256sum "${ARCHIVE}" > "${ARCHIVE}.sha256"
          else
            shasum -a 256 "${ARCHIVE}" > "${ARCHIVE}.sha256"
          fi

      - name: Verify archive
        run: |
          ls -lh "${{ env.ARCHIVE }}"
          cat "${{ env.ARCHIVE }}.sha256"

      - name: Set up Node.js (for mcpb pack)
        uses: actions/setup-node@v6
        with:
          node-version: "20"

      - name: Build .mcpb bundle
        shell: bash
        env:
          VERSION: ${{ steps.version.outputs.version }}
          TARGET: ${{ matrix.target }}
        run: |
          scripts/build-mcpb.sh "$VERSION" "$TARGET" "target/${TARGET}/release/purple"
          MCPB="purple-${VERSION}-${TARGET}.mcpb"
          echo "MCPB=${MCPB}" >> "$GITHUB_ENV"
          if command -v sha256sum > /dev/null 2>&1; then
            sha256sum "${MCPB}" > "${MCPB}.sha256"
          else
            shasum -a 256 "${MCPB}" > "${MCPB}.sha256"
          fi

      - name: Smoke-test .mcpb bundle
        # Skip when the bundle binary cannot run natively on this runner.
        # x86_64-apple-darwin cross-compiles on the arm64 macos-latest, so
        # there is no Intel host to execute the binary. The other 5 targets
        # plus the cross-platform mcp_e2e integration test cover the same
        # bug class.
        if: matrix.target != 'x86_64-apple-darwin'
        shell: bash
        env:
          MCPB: ${{ env.MCPB }}
        run: |
          set -euo pipefail
          # Unpack the bundle to a temp dir, exercise the binary inside it
          # against a literal ${HOME} arg (the case Claude Desktop does NOT
          # substitute) and verify the JSON-RPC tools/list returns the three
          # read-only tools. This is the integration check that catches the
          # bug class where the bundle ships but silently returns empty
          # results in the client.
          WORK="$(mktemp -d)"
          unzip -q "$MCPB" -d "$WORK"
          FAKE_HOME="$(mktemp -d)"
          mkdir -p "$FAKE_HOME/.ssh" "$FAKE_HOME/.purple"
          printf 'Host smoke\n  HostName 10.99.0.1\n  User test\n' \
            > "$FAKE_HOME/.ssh/config"
          OUT="$(mktemp)"
          printf '%s\n%s\n%s\n' \
            '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"1"}}}' \
            '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
            '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
            | HOME="$FAKE_HOME" "$WORK/server/purple" \
                --config '${HOME}/.ssh/config' \
                mcp --read-only \
                --audit-log '${HOME}/.purple/mcp-audit.log' \
            > "$OUT"
          # Last JSON-RPC line is the tools/list response. Expect 3 tools.
          TOOL_COUNT="$(tail -1 "$OUT" | python3 -c 'import sys,json; r=json.loads(sys.stdin.read()); print(len(r["result"]["tools"]))')"
          if [ "$TOOL_COUNT" != "3" ]; then
            echo "::error::.mcpb smoke test failed: expected 3 tools, got $TOOL_COUNT"
            cat "$OUT"
            exit 1
          fi
          # Audit log must have been created at the expanded path.
          if [ ! -f "$FAKE_HOME/.purple/mcp-audit.log" ]; then
            echo "::error::audit log not written; \${HOME} not expanded"
            exit 1
          fi
          echo "Smoke test passed: 3 tools returned, audit log written"

      - name: Upload artifacts
        uses: actions/upload-artifact@v7
        with:
          name: purple-${{ matrix.target }}
          path: |
            ${{ env.ARCHIVE }}
            ${{ env.ARCHIVE }}.sha256
            ${{ env.MCPB }}
            ${{ env.MCPB }}.sha256

  release:
    name: Create GitHub Release
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          ref: ${{ github.event.inputs.tag || github.ref }}

      - name: Derive tag
        id: version
        run: |
          TAG="${{ github.event.inputs.tag || github.ref_name }}"
          echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
          echo "tag=${TAG}" >> "$GITHUB_OUTPUT"

      - name: Extract changelog
        id: changelog
        run: |
          VERSION="${{ steps.version.outputs.version }}"
          # Extract the section for this version from CHANGELOG.md
          # Matches from "## X.Y.Z" until the next "## " or end of file
          # Uses awk -v to avoid regex injection (dots in semver escaped)
          NOTES=$(awk -v ver="$VERSION" 'BEGIN{gsub(/\./,"\\.",ver)} $0 ~ "^## "ver"( - .*)?$"{found=1; next} found && /^## /{exit} found' CHANGELOG.md)
          # Check for missing or whitespace-only entries
          if [ -z "$(printf '%s' "$NOTES" | tr -d '[:space:]')" ]; then
            echo "::error::No changelog entry found for ${VERSION}. Add a '## ${VERSION}' section to CHANGELOG.md."
            exit 1
          fi
          # Write to file to preserve newlines (trailing newline trimmed)
          printf '%s' "$NOTES" > /tmp/release_notes.md

      - name: Download all artifacts
        uses: actions/download-artifact@v8

      - name: Create release
        uses: softprops/action-gh-release@v3
        with:
          tag_name: ${{ steps.version.outputs.tag }}
          body_path: /tmp/release_notes.md
          files: |
            purple-*/*.tar.gz
            purple-*/*.mcpb
            purple-*/*.sha256

  update-homebrew:
    name: Update Homebrew Tap
    needs: release
    runs-on: ubuntu-latest
    steps:
      - name: Get version from tag
        id: version
        run: |
          TAG="${{ github.event.inputs.tag || github.ref_name }}"
          echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"

      - name: Download release tarballs and compute SHA256
        id: sha
        run: |
          VERSION="${{ steps.version.outputs.version }}"
          BASE="https://github.com/erickochen/purple/releases/download/v${VERSION}"
          curl -fSL -o arm64.tar.gz "${BASE}/purple-${VERSION}-aarch64-apple-darwin.tar.gz"
          curl -fSL -o x86_64.tar.gz "${BASE}/purple-${VERSION}-x86_64-apple-darwin.tar.gz"
          echo "sha256_arm64=$(sha256sum arm64.tar.gz | awk '{print $1}')" >> "$GITHUB_OUTPUT"
          echo "sha256_x86_64=$(sha256sum x86_64.tar.gz | awk '{print $1}')" >> "$GITHUB_OUTPUT"

      - name: Checkout homebrew-purple tap
        uses: actions/checkout@v6
        with:
          repository: erickochen/homebrew-purple
          token: ${{ secrets.HOMEBREW_PURPLE_TAP_TOKEN }}

      - name: Update formula
        env:
          VERSION: ${{ steps.version.outputs.version }}
          SHA256_ARM64: ${{ steps.sha.outputs.sha256_arm64 }}
          SHA256_X86_64: ${{ steps.sha.outputs.sha256_x86_64 }}
        run: |
          mkdir -p Formula
          cat > Formula/purple.rb << EOF
          class Purple < Formula
            desc "Open-source terminal SSH manager and SSH config editor"
            homepage "https://github.com/erickochen/purple"
            version "${VERSION}"
            license "MIT"

            depends_on :macos

            on_macos do
              on_arm do
                url "https://github.com/erickochen/purple/releases/download/v${VERSION}/purple-${VERSION}-aarch64-apple-darwin.tar.gz"
                sha256 "${SHA256_ARM64}"
              end
              on_intel do
                url "https://github.com/erickochen/purple/releases/download/v${VERSION}/purple-${VERSION}-x86_64-apple-darwin.tar.gz"
                sha256 "${SHA256_X86_64}"
              end
            end

            def install
              bin.install "purple"
            end

            test do
              assert_match version.to_s, shell_output("#{bin}/purple --version")
            end
          end
          EOF

      - name: Commit and push
        env:
          VERSION: ${{ steps.version.outputs.version }}
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add Formula/purple.rb
          git diff --cached --quiet && echo "No changes to commit" && exit 0
          git commit -m "purple ${VERSION}"
          git push
