name: Release

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

permissions:
  contents: write
  id-token: write

env:
  GITHUB_ENVIRONMENT: production

jobs:
  # Generate AI-powered release notes using Claude API
  generate-notes:
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    outputs:
      notes: ${{ steps.generate.outputs.notes }}
      notes_file: ${{ steps.generate.outputs.notes_file }}

    steps:
      - name: Checkout
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1
        with:
          fetch-depth: 0  # Full history for git log

      - name: Get previous tag
        id: prev_tag
        run: |
          # Get the tag before the current one
          PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
          echo "tag=$PREV_TAG" >> $GITHUB_OUTPUT
          if [ -n "$PREV_TAG" ]; then
            echo "Previous tag: $PREV_TAG"
          else
            echo "No previous tag found (first release)"
          fi

      - name: Collect commits
        id: commits
        run: |
          PREV_TAG="${{ steps.prev_tag.outputs.tag }}"
          MAX_COMMITS=200

          if [ -z "$PREV_TAG" ]; then
            # First release - get all commits
            echo "Collecting all commits (first release)"
            COMMITS=$(git log --pretty=format:"- %s" --no-merges | head -n $MAX_COMMITS)
            TOTAL=$(git rev-list --count --no-merges HEAD)
          else
            # Get commits since previous tag
            echo "Collecting commits from $PREV_TAG to HEAD"
            COMMITS=$(git log "${PREV_TAG}..HEAD" --pretty=format:"- %s" --no-merges | head -n $MAX_COMMITS)
            TOTAL=$(git rev-list --count --no-merges "${PREV_TAG}..HEAD")
          fi

          # Filter out noise (merge commits, version bumps)
          COMMITS=$(echo "$COMMITS" | grep -v -E "^- (Merge branch|Merge pull request|Bump version|chore\(deps\)|chore\(release\))" || echo "$COMMITS")

          # Add truncation note if needed
          if [ "$TOTAL" -gt "$MAX_COMMITS" ]; then
            COMMITS="$COMMITS

          (Showing $MAX_COMMITS most recent of $TOTAL commits)"
            echo "::warning::Truncated to $MAX_COMMITS commits (total: $TOTAL)"
          fi

          # Output commits using heredoc for multiline
          echo "commits<<EOF" >> $GITHUB_OUTPUT
          echo "$COMMITS" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

          echo "Collected $(echo "$COMMITS" | wc -l) commits"

      - name: Generate release notes with Claude
        id: generate
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          # Pass via env to avoid shell injection from commit messages containing
          # quotes, backticks, or other shell metacharacters (see release run 24950486759).
          VERSION: ${{ github.ref_name }}
          COMMITS: ${{ steps.commits.outputs.commits }}
        run: |

          # Check if API key is configured
          if [ -z "$ANTHROPIC_API_KEY" ]; then
            echo "::warning::ANTHROPIC_API_KEY not configured, using fallback message"
            NOTES="Release notes could not be generated automatically (API key not configured). See commit history for changes."
            echo "notes<<EOF" >> $GITHUB_OUTPUT
            echo "$NOTES" >> $GITHUB_OUTPUT
            echo "EOF" >> $GITHUB_OUTPUT
            exit 0
          fi

          # Build the prompt
          PROMPT="Generate concise release notes for version ${VERSION} of MCPProxy (Smart MCP Proxy).

          MCPProxy is a smart proxy for AI agents using the Model Context Protocol (MCP). It provides intelligent tool discovery, token savings, and security quarantine for MCP servers. MCPProxy has two editions: Personal (desktop app) and Teams (multi-user server). Note which changes affect which edition if applicable.

          Commits since last release:
          ${COMMITS}

          Requirements:
          - Maximum 400 words
          - Use markdown format
          - DO NOT include a title or header like 'MCPProxy vX.X.X Release Notes' - GitHub already shows the version
          - Start directly with a brief 1-2 sentence summary of this release
          - Group changes into sections (use only sections that have content):
            - **New Features** - New functionality (feat: commits)
            - **Bug Fixes** - Fixed issues (fix: commits)
            - **Breaking Changes** - Changes requiring user action
            - **Improvements** - Enhancements to existing features
          - Skip internal changes (chore:, docs:, test:, ci: commits) unless significant
          - Use bullet points for each change
          - Be specific but brief
          - If there are no meaningful changes, say 'Minor internal improvements and maintenance updates.'"

          # Call Claude API using jq for proper JSON escaping
          PAYLOAD=$(jq -n \
            --arg model "claude-sonnet-4-5-20250929" \
            --argjson max_tokens 1024 \
            --arg prompt "$PROMPT" \
            '{
              model: $model,
              max_tokens: $max_tokens,
              messages: [{
                role: "user",
                content: $prompt
              }]
            }')

          echo "Calling Claude API..."
          RESPONSE=$(curl -s --max-time 30 \
            https://api.anthropic.com/v1/messages \
            -H "x-api-key: $ANTHROPIC_API_KEY" \
            -H "content-type: application/json" \
            -H "anthropic-version: 2023-06-01" \
            -d "$PAYLOAD" 2>&1) || {
            echo "::warning::Claude API request failed (timeout or network error)"
            NOTES="Release notes could not be generated automatically. See commit history for changes."
            echo "notes<<EOF" >> $GITHUB_OUTPUT
            echo "$NOTES" >> $GITHUB_OUTPUT
            echo "EOF" >> $GITHUB_OUTPUT
            exit 0
          }

          # Check for API errors
          ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // empty' 2>/dev/null || echo "")
          if [ -n "$ERROR_MSG" ]; then
            echo "::warning::Claude API error: $ERROR_MSG"
            NOTES="Release notes could not be generated automatically. See commit history for changes."
            echo "notes<<EOF" >> $GITHUB_OUTPUT
            echo "$NOTES" >> $GITHUB_OUTPUT
            echo "EOF" >> $GITHUB_OUTPUT
            exit 0
          fi

          # Extract the generated notes
          NOTES=$(echo "$RESPONSE" | jq -r '.content[0].text // empty' 2>/dev/null || echo "")

          if [ -z "$NOTES" ]; then
            echo "::warning::Failed to extract release notes from API response"
            NOTES="Release notes could not be generated automatically. See commit history for changes."
          else
            echo "✅ Release notes generated successfully"
          fi

          # Output notes
          echo "notes<<EOF" >> $GITHUB_OUTPUT
          echo "$NOTES" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

          # Save to file for artifact
          NOTES_FILE="RELEASE_NOTES-${VERSION}.md"
          echo "$NOTES" > "$NOTES_FILE"
          echo "notes_file=$NOTES_FILE" >> $GITHUB_OUTPUT

      - name: Upload release notes artifact
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02  # v4.6.2
        with:
          name: release-notes
          path: RELEASE_NOTES-${{ github.ref_name }}.md
          if-no-files-found: ignore

  build:
    environment: production
    # Only run on version tags
    if: startsWith(github.ref, 'refs/tags/v')
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            goos: linux
            goarch: amd64
            cgo: "0"
            name: mcpproxy-linux-amd64
            archive_format: tar.gz
          - os: ubuntu-latest
            goos: linux
            goarch: arm64
            cgo: "0"
            name: mcpproxy-linux-arm64
            archive_format: tar.gz
          - os: windows-latest
            goos: windows
            goarch: amd64
            cgo: "1"
            name: mcpproxy-windows-amd64.exe
            archive_format: zip
          - os: windows-latest
            goos: windows
            goarch: arm64
            cgo: "1"
            name: mcpproxy-windows-arm64.exe
            archive_format: zip
          - os: macos-15
            goos: darwin
            goarch: amd64
            cgo: "1"
            name: mcpproxy-darwin-amd64
            archive_format: tar.gz
          - os: macos-15
            goos: darwin
            goarch: arm64
            cgo: "1"
            name: mcpproxy-darwin-arm64
            archive_format: tar.gz
          # Server edition (Linux only) — uncomment when server MVP is ready
          # - os: ubuntu-latest
          #   goos: linux
          #   goarch: amd64
          #   cgo: "0"
          #   name: mcpproxy-server-linux-amd64
          #   archive_format: tar.gz
          #   edition: server
          # - os: ubuntu-latest
          #   goos: linux
          #   goarch: arm64
          #   cgo: "0"
          #   name: mcpproxy-server-linux-arm64
          #   archive_format: tar.gz
          #   edition: server

    runs-on: ${{ matrix.os }}

    steps:
      - name: Checkout
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1
        with:
          fetch-depth: 0

      - name: Download release notes artifact
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093  # v4.3.0
        with:
          name: release-notes
          path: .
        continue-on-error: true  # Don't fail if notes not yet available

      - name: Set up Go
        uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff  # v5.6.0
        with:
          go-version: "1.25"

      - name: Cache Go modules and build
        uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830  # v4.3.0
        with:
          path: |
            ~/.cache/go-build
            ~/go/pkg/mod
            ~/AppData/Local/go-build
          key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-1.25-

      - name: Download dependencies
        run: go mod download

      - name: Setup Node.js
        uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020  # v4.4.0
        with:
          node-version: '20'

      - name: Install frontend dependencies
        run: cd frontend && npm ci

      - name: Build frontend
        run: cd frontend && npm run build

      - name: Copy frontend dist to embed location
        shell: bash
        run: |
          rm -rf web/frontend
          mkdir -p web/frontend/dist
          cp -r frontend/dist/. web/frontend/dist/
          # Recreate the tracked .gitkeep so //go:embed all:frontend/dist
          # always has something to embed (matches the Makefile frontend-build target).
          touch web/frontend/dist/.gitkeep

      - name: Import Code-Signing Certificates (macOS)
        if: matrix.goos == 'darwin'
        run: |
          set -euo pipefail

          echo "📦 Preparing isolated keychain for code signing"
          UNIQUE_ID="${{ matrix.goos }}-${{ matrix.goarch }}-$$-$(date +%s)"
          TEMP_KEYCHAIN="mcpproxy-build-${UNIQUE_ID}.keychain"

          security create-keychain -p "temp123" "$TEMP_KEYCHAIN"
          security list-keychains -s "$TEMP_KEYCHAIN" ~/Library/Keychains/login.keychain-db /Library/Keychains/System.keychain
          security unlock-keychain -p "temp123" "$TEMP_KEYCHAIN"
          security set-keychain-settings -t 3600 -l "$TEMP_KEYCHAIN"

          if [ -z "${{ secrets.APPLE_DEVELOPER_ID_CERT }}" ] || [ -z "${{ secrets.APPLE_DEVELOPER_ID_CERT_PASSWORD }}" ]; then
            echo "❌ APPLE_DEVELOPER_ID_CERT and APPLE_DEVELOPER_ID_CERT_PASSWORD secrets are required"
            exit 1
          fi

          echo "${{ secrets.APPLE_DEVELOPER_ID_CERT }}" | base64 -d > developer-id.p12
          security import developer-id.p12 \
            -k "$TEMP_KEYCHAIN" \
            -P "${{ secrets.APPLE_DEVELOPER_ID_CERT_PASSWORD }}" \
            -T /usr/bin/codesign \
            -T /usr/bin/productbuild \
            -T /usr/bin/productsign \
            -T /usr/bin/security
          rm -f developer-id.p12

          echo "🔍 Checking for separate Developer ID Installer certificate"
          INSTALLER_ID=$(security find-identity -v -p basic "$TEMP_KEYCHAIN" | grep "Developer ID Installer" || true)

          if [ -z "$INSTALLER_ID" ]; then
            if [ -z "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERT }}" ] || [ -z "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERT_PASSWORD }}" ]; then
              echo "❌ Developer ID Installer identity not found in APPLE_DEVELOPER_ID_CERT"
              echo "   Provide APPLE_DEVELOPER_ID_INSTALLER_CERT and password secrets"
              exit 1
            fi

            echo "Importing dedicated Developer ID Installer certificate"
            echo "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERT }}" | base64 -d > developer-id-installer.p12
            security import developer-id-installer.p12 \
              -k "$TEMP_KEYCHAIN" \
              -P "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERT_PASSWORD }}" \
              -T /usr/bin/productsign \
              -T /usr/bin/productbuild \
              -T /usr/bin/codesign \
              -T /usr/bin/security
            rm -f developer-id-installer.p12
          fi

          security set-key-partition-list -S apple-tool:,apple: -s -k "temp123" "$TEMP_KEYCHAIN"

          APP_CERT_IDENTITY=$(security find-identity -v -p codesigning "$TEMP_KEYCHAIN" | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"')
          PKG_CERT_IDENTITY=$(security find-identity -v -p basic "$TEMP_KEYCHAIN" | grep "Developer ID Installer" | head -1 | grep -o '"[^"]*"' | tr -d '"')

          if [ -z "$APP_CERT_IDENTITY" ]; then
            echo "❌ Developer ID Application identity not found after import"
            exit 1
          fi

          if [ -z "$PKG_CERT_IDENTITY" ]; then
            echo "❌ Developer ID Installer identity not found after import"
            exit 1
          fi

          echo "✅ Using Developer ID Application: $APP_CERT_IDENTITY"
          echo "✅ Using Developer ID Installer: $PKG_CERT_IDENTITY"

          echo "APP_CERT_IDENTITY=$APP_CERT_IDENTITY" >> "$GITHUB_ENV"
          echo "PKG_CERT_IDENTITY=$PKG_CERT_IDENTITY" >> "$GITHUB_ENV"
          echo "$TEMP_KEYCHAIN" > .keychain_name

          echo "=== Available signing identities in temporary keychain ==="
          security find-identity -v "$TEMP_KEYCHAIN"

          echo "✅ Certificate import completed"

      - name: Build binary and create archives
        shell: bash
        env:
          CGO_ENABLED: ${{ matrix.cgo }}
          GOOS: ${{ matrix.goos }}
          GOARCH: ${{ matrix.goarch }}
          # ✅ Force minimum supported macOS version for compatibility (13.0 for Swift tray app)
          MACOSX_DEPLOYMENT_TARGET: "13.0"
          # Defensive CGO flags to ensure proper deployment target
          CGO_CFLAGS: "-mmacosx-version-min=13.0"
          CGO_LDFLAGS: "-mmacosx-version-min=13.0"
        run: |
          VERSION=${GITHUB_REF#refs/tags/}
          LDFLAGS="-s -w -X main.version=${VERSION} -X github.com/smart-mcp-proxy/mcpproxy-go/internal/httpapi.buildVersion=${VERSION}"

          # Determine clean binary name and build flags
          EDITION="${{ matrix.edition }}"
          BUILD_TAGS=""
          if [ "$EDITION" = "server" ]; then
            CLEAN_BINARY="mcpproxy-server"
            BUILD_TAGS="-tags server"
          elif [ "${{ matrix.goos }}" = "windows" ]; then
            CLEAN_BINARY="mcpproxy.exe"
          else
            CLEAN_BINARY="mcpproxy"
          fi

          # Create clean core binary for archive
          go build ${BUILD_TAGS} -ldflags "${LDFLAGS}" -o ${CLEAN_BINARY} ./cmd/mcpproxy

          # Build tray binary for platforms with GUI support (macOS and Windows, personal only)
          if [ "$EDITION" != "server" ] && { [ "${{ matrix.goos }}" = "darwin" ] || [ "${{ matrix.goos }}" = "windows" ]; }; then
            echo "Building mcpproxy-tray for ${{ matrix.goos }}..."

            # Determine tray binary name
            if [ "${{ matrix.goos }}" = "windows" ]; then
              TRAY_BINARY="mcpproxy-tray.exe"
            else
              TRAY_BINARY="mcpproxy-tray"
            fi

            go build -ldflags "${LDFLAGS}" -o ${TRAY_BINARY} ./cmd/mcpproxy-tray
          fi

          # Build Swift tray app (macOS only — replaces Go tray in .app bundle for DMG/PKG)
          if [ "$EDITION" != "server" ] && [ "${{ matrix.goos }}" = "darwin" ]; then
            chmod +x scripts/build-swift-app.sh
            OUTPUT_DIR="$(pwd)/swift-build"
            mkdir -p "$OUTPUT_DIR"
            ./scripts/build-swift-app.sh "${VERSION}" "${{ matrix.goarch }}" "$OUTPUT_DIR"
            SWIFT_APP="$OUTPUT_DIR/MCPProxy.app"
            if [ -d "$SWIFT_APP" ]; then
              echo "SWIFT_APP_PATH=$SWIFT_APP" >> $GITHUB_ENV
            else
              echo "❌ Swift app build failed"
              exit 1
            fi
          fi

          # Code sign macOS binaries
          if [ "${{ matrix.goos }}" = "darwin" ]; then
            echo "Code signing macOS binary..."

            # Debug: List all available certificates
            echo "Available certificates:"
            security find-identity -v -p codesigning

            # Find the Developer ID certificate identity
            CERT_IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"')

            # Verify we found a valid certificate
            if [ -n "${CERT_IDENTITY}" ]; then
              echo "✅ Found Developer ID certificate: ${CERT_IDENTITY}"
            else
              echo "❌ No Developer ID certificate found, using team ID as fallback"
              CERT_IDENTITY="${{ secrets.APPLE_TEAM_ID }}"
              echo "⚠️  Using fallback identity: ${CERT_IDENTITY}"
            fi

            # Validate entitlements file formatting (Apple's recommendation)
            echo "=== Validating entitlements file ==="
            if [ -f "scripts/entitlements.plist" ]; then
              echo "Validating entitlements formatting with plutil..."
              if plutil -lint scripts/entitlements.plist; then
                echo "✅ Entitlements file is properly formatted"
              else
                echo "❌ Entitlements file has formatting issues"
                exit 1
              fi

              # Convert to XML format if needed (Apple's recommendation)
              plutil -convert xml1 scripts/entitlements.plist
              echo "✅ Entitlements converted to XML format"
            else
              echo "⚠️  No entitlements file found"
            fi

            # Sign both binaries with proper Developer ID certificate, hardened runtime, and timestamp
            echo "=== Signing binaries with hardened runtime ==="

            # Install GNU coreutils for timeout command (macOS compatibility)
            if ! command -v timeout &> /dev/null; then
              echo "Installing GNU coreutils for timeout command..."
              brew install coreutils
              # Use gtimeout from coreutils
              TIMEOUT_CMD="gtimeout"
            else
              TIMEOUT_CMD="timeout"
            fi

            # Sign core binary
            echo "Signing core binary: ${CLEAN_BINARY}"
            SIGN_SUCCESS=false
            for attempt in 1 2 3; do
              echo "Core binary signing attempt $attempt/3..."

              # Use timeout command to prevent hanging (max 5 minutes per attempt)
              if $TIMEOUT_CMD 300 codesign --force \
                --options runtime \
                --entitlements scripts/entitlements.plist \
                --sign "${CERT_IDENTITY}" \
                --timestamp \
                ${CLEAN_BINARY}; then

                SIGN_SUCCESS=true
                echo "✅ Core binary signing succeeded on attempt $attempt"
                break
              else
                echo "❌ Core binary signing attempt $attempt failed or timed out"
                if [ $attempt -lt 3 ]; then
                  echo "Retrying in 10 seconds..."
                  sleep 10
                fi
              fi
            done

            if [ "$SIGN_SUCCESS" != "true" ]; then
              echo "❌ All core binary signing attempts failed"
              exit 1
            fi

            # Sign tray binary
            echo "Signing tray binary: mcpproxy-tray"
            TRAY_SIGN_SUCCESS=false
            for attempt in 1 2 3; do
              echo "Tray binary signing attempt $attempt/3..."

              # Use timeout command to prevent hanging (max 5 minutes per attempt)
              if $TIMEOUT_CMD 300 codesign --force \
                --options runtime \
                --entitlements scripts/entitlements.plist \
                --sign "${CERT_IDENTITY}" \
                --timestamp \
                mcpproxy-tray; then

                TRAY_SIGN_SUCCESS=true
                echo "✅ Tray binary signing succeeded on attempt $attempt"
                break
              else
                echo "❌ Tray binary signing attempt $attempt failed or timed out"
                if [ $attempt -lt 3 ]; then
                  echo "Retrying in 10 seconds..."
                  sleep 10
                fi
              fi
            done

            if [ "$TRAY_SIGN_SUCCESS" != "true" ]; then
              echo "❌ All tray binary signing attempts failed"
              exit 1
            fi

            # Verify signing, hardened runtime, and timestamp using Apple's recommended methods
            echo "=== Verifying binary signatures (Apple's recommended verification) ==="

            # Verify core binary
            echo "=== Core binary verification ==="
            codesign --verify --verbose ${CLEAN_BINARY}
            echo "Core binary basic verification: $?"

            # Apple's recommended strict verification for notarization
            echo "=== Core binary strict verification (matches notarization requirements) ==="
            if codesign -vvv --deep --strict ${CLEAN_BINARY}; then
              echo "✅ Core binary strict verification PASSED - ready for notarization"
            else
              echo "❌ Core binary strict verification FAILED - will not pass notarization"
              exit 1
            fi

            # Verify tray binary
            echo "=== Tray binary verification ==="
            codesign --verify --verbose mcpproxy-tray
            echo "Tray binary basic verification: $?"

            # Apple's recommended strict verification for notarization
            echo "=== Tray binary strict verification (matches notarization requirements) ==="
            if codesign -vvv --deep --strict mcpproxy-tray; then
              echo "✅ Tray binary strict verification PASSED - ready for notarization"
            else
              echo "❌ Strict verification FAILED - will not pass notarization"
              exit 1
            fi

            # Check for secure timestamp (Apple's recommended check)
            echo "=== Checking for secure timestamp ==="
            TIMESTAMP_CHECK=$(codesign -dvv ${CLEAN_BINARY} 2>&1)
            if echo "$TIMESTAMP_CHECK" | grep -q "Timestamp="; then
              echo "✅ Secure timestamp present:"
              echo "$TIMESTAMP_CHECK" | grep "Timestamp="
            else
              echo "❌ No secure timestamp found"
              echo "Full output:"
              echo "$TIMESTAMP_CHECK"
            fi

            # Display detailed signature info
            codesign --display --verbose=4 ${CLEAN_BINARY}

            # Check entitlements formatting (Apple's recommendation)
            echo "=== Checking entitlements formatting ==="
            codesign --display --entitlements - ${CLEAN_BINARY} | head -10

            # Verify with spctl (Gatekeeper assessment) - expected to fail before notarization
            echo "=== Gatekeeper assessment (expected to fail before notarization) ==="
            if spctl --assess --verbose ${CLEAN_BINARY}; then
              echo "✅ Gatekeeper assessment: PASSED (unexpected but good!)"
            else
              echo "⚠️  Gatekeeper assessment: REJECTED (expected - binary needs notarization)"
              echo "This is normal - the binary will pass after Apple completes notarization"
            fi

            echo "✅ Binary signed successfully with hardened runtime and timestamp"
          fi

          # Create archive with version info
          ARCHIVE_BASE="mcpproxy-${VERSION#v}-${{ matrix.goos }}-${{ matrix.goarch }}"
          LATEST_ARCHIVE_BASE="mcpproxy-latest-${{ matrix.goos }}-${{ matrix.goarch }}"

          # Determine files to include in archive
          FILES_TO_ARCHIVE="${CLEAN_BINARY}"

          # Add tray binary if it exists (Windows and macOS)
          if [ "${{ matrix.goos }}" = "windows" ] && [ -f "mcpproxy-tray.exe" ]; then
            FILES_TO_ARCHIVE="${FILES_TO_ARCHIVE} mcpproxy-tray.exe"
            echo "Including mcpproxy-tray.exe in archive"
          elif [ "${{ matrix.goos }}" = "darwin" ] && [ -f "mcpproxy-tray" ]; then
            FILES_TO_ARCHIVE="${FILES_TO_ARCHIVE} mcpproxy-tray"
            echo "Including mcpproxy-tray in archive"
          fi

          if [ "${{ matrix.archive_format }}" = "zip" ]; then
            # Create ZIP archive (Windows)
            # Use PowerShell Compress-Archive on Windows since zip command isn't available
            if [ "${{ matrix.goos }}" = "windows" ]; then
              # Convert space-separated list to comma-separated for PowerShell
              PS_FILES=$(echo ${FILES_TO_ARCHIVE} | sed 's/ /,/g')
              powershell -Command "Compress-Archive -Path ${PS_FILES} -DestinationPath '${ARCHIVE_BASE}.zip'"
              powershell -Command "Compress-Archive -Path ${PS_FILES} -DestinationPath '${LATEST_ARCHIVE_BASE}.zip'"
            else
              # Create versioned archive
              zip "${ARCHIVE_BASE}.zip" ${FILES_TO_ARCHIVE}
              # Create latest archive
              zip "${LATEST_ARCHIVE_BASE}.zip" ${FILES_TO_ARCHIVE}
            fi
          else
            # Create versioned archive
            tar -czf "${ARCHIVE_BASE}.tar.gz" ${FILES_TO_ARCHIVE}
            # Create latest archive
            tar -czf "${LATEST_ARCHIVE_BASE}.tar.gz" ${FILES_TO_ARCHIVE}
          fi

      - name: Build Linux .deb and .rpm packages
        if: matrix.goos == 'linux' && matrix.edition != 'server'
        uses: ./.github/actions/build-linux-packages
        with:
          goarch: ${{ matrix.goarch }}

      - name: Install Inno Setup (Windows)
        if: matrix.goos == 'windows'
        shell: pwsh
        run: |
          choco install innosetup -y
          # Verify installation
          if (Test-Path "C:\Program Files (x86)\Inno Setup 6\ISCC.exe") {
            Write-Host "✅ Inno Setup installed successfully"
          } else {
            Write-Host "❌ Inno Setup installation failed"
            exit 1
          }

      - name: Create Windows installer (Windows)
        if: matrix.goos == 'windows'
        shell: pwsh
        run: |
          $VERSION = "${{ github.ref_name }}"
          $ARCH = "${{ matrix.goarch }}"

          Write-Host "Building Windows installer for version ${VERSION} (${ARCH})"

          # Run the build script
          .\scripts\build-windows-installer.ps1 -Version $VERSION -Arch $ARCH

          # Verify installer was created
          $INSTALLER_NAME = "mcpproxy-setup-${VERSION}-${ARCH}.exe"
          $INSTALLER_PATH = "dist\${INSTALLER_NAME}"

          if (Test-Path $INSTALLER_PATH) {
            Write-Host "✅ Windows installer created: ${INSTALLER_NAME}"
            $size = (Get-Item $INSTALLER_PATH).Length / 1MB
            Write-Host "   Size: $([math]::Round($size, 2)) MB"
          } else {
            Write-Host "❌ Windows installer not found at ${INSTALLER_PATH}"
            exit 1
          }

      - name: Upload unsigned Windows installer for signing
        if: matrix.goos == 'windows'
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02  # v4.6.2
        id: upload-unsigned-installer
        with:
          name: unsigned-installer-windows-${{ matrix.goarch }}
          # Upload exe directly - GitHub Actions will ZIP it automatically
          path: dist/mcpproxy-setup-${{ github.ref_name }}-${{ matrix.goarch }}.exe

      - name: Create .icns icon (macOS)
        if: matrix.goos == 'darwin'
        run: |
          chmod +x scripts/create-icns.sh
          ./scripts/create-icns.sh

      - name: Create DMG installer (macOS)
        if: matrix.goos == 'darwin'
        env:
          # Ensure DMG creation also uses correct deployment target
          MACOSX_DEPLOYMENT_TARGET: "13.0"
          CGO_CFLAGS: "-mmacosx-version-min=13.0"
          CGO_LDFLAGS: "-mmacosx-version-min=13.0"
        run: |
          VERSION=${GITHUB_REF#refs/tags/}
          chmod +x scripts/create-dmg.sh

          # Determine binary names
          CORE_BINARY="mcpproxy"

          # Use Swift app for DMG when available, fall back to Go tray binary
          if [ -n "$SWIFT_APP_PATH" ] && [ -d "$SWIFT_APP_PATH" ]; then
            echo "Using Swift app bundle for DMG: $SWIFT_APP_PATH"
            ./scripts/create-dmg.sh "$SWIFT_APP_PATH" ${CORE_BINARY} ${VERSION} ${{ matrix.goarch }}
          else
            echo "Using Go tray binary for DMG"
            TRAY_BINARY="mcpproxy-tray"
            ./scripts/create-dmg.sh ${TRAY_BINARY} ${CORE_BINARY} ${VERSION} ${{ matrix.goarch }}
          fi

          # Sign DMG
          DMG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}.dmg"
          echo "Signing DMG: ${DMG_NAME}"

          # Find the Developer ID certificate identity
          CERT_IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"')

          # Verify we found a valid certificate
          if [ -n "${CERT_IDENTITY}" ]; then
            echo "✅ Found Developer ID certificate for DMG: ${CERT_IDENTITY}"
          else
            echo "❌ No Developer ID certificate found for DMG, using team ID as fallback"
            CERT_IDENTITY="${{ secrets.APPLE_TEAM_ID }}"
            echo "⚠️  Using fallback identity for DMG: ${CERT_IDENTITY}"
          fi

          # Sign DMG with proper certificate and timestamp
          codesign --force \
            --sign "${CERT_IDENTITY}" \
            --timestamp \
            "${DMG_NAME}"

          # Verify DMG signing
          echo "=== Verifying DMG signature ==="
          codesign --verify --verbose "${DMG_NAME}"
          echo "DMG verification: $?"

          codesign --display --verbose=4 "${DMG_NAME}"

          echo "✅ DMG created and signed successfully: ${DMG_NAME}"

      - name: Create PKG installer (macOS)
        if: matrix.goos == 'darwin'
        env:
          # Ensure PKG creation also uses correct deployment target
          MACOSX_DEPLOYMENT_TARGET: "13.0"
          CGO_CFLAGS: "-mmacosx-version-min=13.0"
          CGO_LDFLAGS: "-mmacosx-version-min=13.0"
        run: |
          VERSION=${GITHUB_REF#refs/tags/}
          chmod +x scripts/create-pkg.sh
          chmod +x scripts/create-installer-dmg.sh

          # Set up certificate environment for PKG creation (reuse from binary signing)
          echo "=== Setting up certificate environment for PKG creation ==="

          # Debug: List all available certificates for PKG creation
          echo "=== Available certificates for PKG creation ==="
          echo "Codesigning certificates:"
          security find-identity -v -p codesigning || echo "No codesigning certificates found"
          echo "Basic certificates:"
          security find-identity -v -p basic || echo "No basic certificates found"
          echo "All certificates:"
          security find-identity -v || echo "No certificates found"

          # Prefer identities exported during certificate import, fallback to keychain lookup
          APP_CERT_IDENTITY="${APP_CERT_IDENTITY:-}"
          PKG_CERT_IDENTITY="${PKG_CERT_IDENTITY:-}"

          if [ -z "${APP_CERT_IDENTITY}" ]; then
            APP_CERT_IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"')
          fi

          if [ -z "${PKG_CERT_IDENTITY}" ]; then
            PKG_CERT_IDENTITY=$(security find-identity -v -p basic | grep "Developer ID Installer" | head -1 | grep -o '"[^"]*"' | tr -d '"')
          fi

          if [ -z "${APP_CERT_IDENTITY}" ]; then
            echo "❌ Developer ID Application certificate not available in the build keychain"
            exit 1
          fi

          if [ -z "${PKG_CERT_IDENTITY}" ]; then
            echo "❌ Developer ID Installer certificate not available in the isolated keychain"
            echo "   Embed the 'Developer ID Installer' identity in APPLE_DEVELOPER_ID_CERT"
            exit 1
          fi

          echo "✅ Using Developer ID Application certificate: ${APP_CERT_IDENTITY}"
          echo "✅ Using Developer ID Installer certificate: ${PKG_CERT_IDENTITY}"

          export APP_CERT_IDENTITY
          export PKG_CERT_IDENTITY

          # Determine binary names
          CORE_BINARY="mcpproxy"

          # Use Swift app for PKG when available, fall back to Go tray binary
          echo "Creating signed PKG installer with certificate: ${PKG_CERT_IDENTITY}"
          if [ -n "$SWIFT_APP_PATH" ] && [ -d "$SWIFT_APP_PATH" ]; then
            echo "Using Swift app bundle for PKG: $SWIFT_APP_PATH"
            ./scripts/create-pkg.sh "$SWIFT_APP_PATH" ${CORE_BINARY} ${VERSION} ${{ matrix.goarch }}
          else
            echo "Using Go tray binary for PKG"
            TRAY_BINARY="mcpproxy-tray"
            ./scripts/create-pkg.sh ${TRAY_BINARY} ${CORE_BINARY} ${VERSION} ${{ matrix.goarch }}
          fi

          # Create installer DMG containing the PKG
          PKG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}.pkg"
          ./scripts/create-installer-dmg.sh ${PKG_NAME} ${VERSION} ${{ matrix.goarch }}

          echo "✅ PKG installer and installer DMG created successfully"

      - name: Submit for notarization (macOS)
        if: matrix.goos == 'darwin'
        run: |
          set -euo pipefail

          VERSION=${GITHUB_REF#refs/tags/}
          PKG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}.pkg"
          INSTALLER_DMG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}-installer.dmg"

          notarize_and_staple() {
            local FILE_NAME="$1"
            local FILE_LABEL="$2"

            if [ ! -f "${FILE_NAME}" ]; then
              echo "❌ ${FILE_LABEL} (${FILE_NAME}) not found"
              return 1
            fi

            echo "Submitting ${FILE_LABEL} for notarization: ${FILE_NAME}"

            local SUBMISSION_OUTPUT
            if ! SUBMISSION_OUTPUT=$(xcrun notarytool submit "${FILE_NAME}" \
              --apple-id "${{ secrets.APPLE_ID_USERNAME }}" \
              --password "${{ secrets.APPLE_ID_APP_PASSWORD }}" \
              --team-id "${{ secrets.APPLE_TEAM_ID }}" \
              --wait \
              --output-format json 2>&1); then
              echo "❌ ${FILE_LABEL} notarization failed"
              echo "Output: ${SUBMISSION_OUTPUT}"
              return 1
            fi

            local SUBMISSION_ID
            SUBMISSION_ID=$(echo "${SUBMISSION_OUTPUT}" | jq -r '.id // empty')
            local STATUS
            STATUS=$(echo "${SUBMISSION_OUTPUT}" | jq -r '.status // empty')

            if [ -z "${SUBMISSION_ID}" ] || [ "${SUBMISSION_ID}" = "null" ] || [ "${STATUS}" != "Accepted" ]; then
              echo "❌ ${FILE_LABEL} notarization did not succeed"
              echo "Response: ${SUBMISSION_OUTPUT}"
              return 1
            fi

            echo "✅ ${FILE_LABEL} notarization accepted (ID: ${SUBMISSION_ID})"
            echo "${SUBMISSION_ID}" > "${FILE_NAME}.submission_id"

            echo "Stapling notarization ticket to ${FILE_LABEL}"
            xcrun stapler staple "${FILE_NAME}"
            xcrun stapler validate "${FILE_NAME}"
          }

          notarize_and_staple "${PKG_NAME}" "PKG installer"
          notarize_and_staple "${INSTALLER_DMG_NAME}" "Installer DMG"

          echo "✅ Notarization and stapling complete"

      - name: Cleanup isolated keychain (macOS)
        if: matrix.goos == 'darwin' && always()
        run: |
          # Clean up the isolated keychain we created for this worker
          if [ -f .keychain_name ]; then
            TEMP_KEYCHAIN=$(cat .keychain_name)
            echo "Cleaning up keychain: ${TEMP_KEYCHAIN}"

            # Remove from search list and delete
            security delete-keychain "$TEMP_KEYCHAIN" 2>/dev/null || echo "Keychain already cleaned up"
            rm -f .keychain_name
            echo "✅ Keychain cleanup completed"
          else
            echo "No keychain to clean up"
          fi

      - name: Upload versioned archive artifact
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02  # v4.6.2
        with:
          name: versioned-${{ matrix.edition || 'personal' }}-${{ matrix.goos }}-${{ matrix.goarch }}
          path: mcpproxy-*-${{ matrix.goos }}-${{ matrix.goarch }}.${{ matrix.archive_format }}

      - name: Upload latest archive artifact
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02  # v4.6.2
        with:
          name: latest-${{ matrix.edition || 'personal' }}-${{ matrix.goos }}-${{ matrix.goarch }}
          path: mcpproxy-latest-${{ matrix.goos }}-${{ matrix.goarch }}.${{ matrix.archive_format }}

      - name: Upload macOS installer DMG
        if: matrix.goos == 'darwin'
        run: |
          set -euo pipefail

          VERSION=${GITHUB_REF#refs/tags/}
          INSTALLER_DMG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}-installer.dmg"

          echo "Looking for files:"
          echo "  Installer DMG: ${INSTALLER_DMG_NAME}"
          if [ ! -f "${INSTALLER_DMG_NAME}" ]; then
            echo "❌ Installer DMG not found: ${INSTALLER_DMG_NAME}"
            exit 1
          fi

          mkdir -p installers-artifact
          cp "${INSTALLER_DMG_NAME}" installers-artifact/

          SUBMISSION_ID_FILE="${INSTALLER_DMG_NAME}.submission_id"
          if [ -f "${SUBMISSION_ID_FILE}" ]; then
            echo "✅ Found submission ID file: ${SUBMISSION_ID_FILE}"
            cp "${SUBMISSION_ID_FILE}" installers-artifact/
          else
            echo "⚠️  No submission ID file found: ${SUBMISSION_ID_FILE}"
          fi

          echo "Files to upload:"
          ls -la installers-artifact/

      - name: Upload macOS installers artifact
        if: matrix.goos == 'darwin'
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02  # v4.6.2
        with:
          name: installers-${{ matrix.goos }}-${{ matrix.goarch }}
          path: installers-artifact/*

  # Sign Windows installers with SignPath
  sign-windows:
    needs: build
    runs-on: ubuntu-latest
    environment: production
    if: startsWith(github.ref, 'refs/tags/v')
    strategy:
      matrix:
        arch: [amd64, arm64]

    steps:
      - name: Download unsigned installer
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093  # v4.3.0
        with:
          name: unsigned-installer-windows-${{ matrix.arch }}
          path: unsigned

      - name: Re-upload for SignPath
        id: reupload
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02  # v4.6.2
        with:
          name: signpath-input-windows-${{ matrix.arch }}
          # Upload exe - GitHub Actions will ZIP it for SignPath
          path: unsigned/*.exe

      - name: Submit to SignPath for signing
        uses: signpath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd  # v2.2
        with:
          api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
          organization-id: '84efd51b-c11c-4a85-82e6-7c3b1157d7ca'
          project-slug: 'mcpproxy-go'
          signing-policy-slug: 'release-signing'
          artifact-configuration-slug: 'initial'
          github-artifact-id: '${{ steps.reupload.outputs.artifact-id }}'
          wait-for-completion: true
          wait-for-completion-timeout-in-seconds: 3600
          output-artifact-directory: signed

      - name: Prepare signed installer
        run: |
          VERSION=${GITHUB_REF#refs/tags/}
          ARCH="${{ matrix.arch }}"
          TARGET="mcpproxy-setup-${VERSION}-${ARCH}.exe"

          echo "Preparing signed installer..."
          cd signed
          ls -la

          # SignPath returns signed exe - verify it exists
          if [ -f "$TARGET" ]; then
            echo "✅ Signed installer already named correctly: $TARGET"
          else
            # Find and rename if needed
            SIGNED_EXE=$(find . -name "*.exe" -type f | head -1)
            if [ -n "$SIGNED_EXE" ]; then
              mv "$SIGNED_EXE" "$TARGET"
              echo "✅ Renamed to: $TARGET"
            else
              echo "❌ No signed .exe found in SignPath output"
              exit 1
            fi
          fi

      - name: Upload signed Windows installer
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02  # v4.6.2
        with:
          name: installer-windows-${{ matrix.arch }}
          path: signed/mcpproxy-setup-${{ github.ref_name }}-${{ matrix.arch }}.exe

  build-docker:
    runs-on: ubuntu-latest
    needs: [build]
    # Disabled until server MVP is ready — uncomment to enable
    if: false && startsWith(github.ref, 'refs/tags/v')
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1
      - uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca  # v3.9.0
      - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9  # v3.7.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25  # v5.4.0
        with:
          context: .
          push: true
          build-args: |
            VERSION=${{ github.ref_name }}
            COMMIT=${{ github.sha }}
            BUILD_DATE=${{ github.event.head_commit.timestamp }}
          tags: |
            ghcr.io/smart-mcp-proxy/mcpproxy-server:${{ github.ref_name }}
            ghcr.io/smart-mcp-proxy/mcpproxy-server:latest

  release:
    needs: [build, sign-windows, generate-notes]  # add build-docker when server MVP is ready
    runs-on: ubuntu-latest
    environment: production
    outputs:
      # base64-encoded checksums.txt — consumed by the SLSA provenance job (WP-C3)
      hashes: ${{ steps.checksums.outputs.hashes }}

    steps:
      - name: Checkout
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1
        with:
          fetch-depth: 0

      - name: Download all artifacts
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093  # v4.3.0
        with:
          path: dist

      - name: Reorganize files
        run: |
          VERSION=${GITHUB_REF#refs/tags/}
          # Create a flat structure to avoid duplicates
          mkdir -p release-files

          # Copy archives (tar.gz and zip files)
          find dist -name "*.tar.gz" -o -name "*.zip" | while read file; do
            filename=$(basename "$file")
            cp "$file" "release-files/$filename"
          done

          # Copy Linux .deb and .rpm packages
          find dist -path "*/linux-packages-*" \( -name "*.deb" -o -name "*.rpm" \) | while read pkg_file; do
            filename=$(basename "$pkg_file")
            echo "Found Linux package: $filename"
            cp "$pkg_file" "release-files/$filename"
          done

          # Copy Windows installer files (.exe)
          find dist -path "*/installer-windows-*" -name "*.exe" | while read installer_file; do
            filename=$(basename "$installer_file")
            echo "Found Windows installer: $filename"
            cp "$installer_file" "release-files/$filename"
          done

          # Handle installer files (DMG and PKG) and notarization submissions
          mkdir -p pending-notarizations

          # Process installer artifacts (only notarized installer DMGs)
          find dist -path "*/installers-*" -name "*.dmg" | while read installer_file; do
            filename=$(basename "$installer_file")
            submission_id_file="${installer_file}.submission_id"

            if [ -f "$submission_id_file" ]; then
              # File has pending notarization
              SUBMISSION_ID=$(cat "$submission_id_file")

              # Validate submission ID before creating pending file
              if [ -n "$SUBMISSION_ID" ] && [ "$SUBMISSION_ID" != "null" ] && [ ${#SUBMISSION_ID} -gt 10 ]; then
                echo "Found valid pending notarization for $filename (ID: $SUBMISSION_ID)"
                cp "$installer_file" "release-files/$filename"

                # Create pending notarization record
                cat > "pending-notarizations/${filename}.pending" << EOF
          {
            "submission_id": "$SUBMISSION_ID",
            "file_name": "$filename",
            "version": "$VERSION",
            "submitted_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
          }
          EOF
              else
                echo "❌ Invalid submission ID for $filename: '$SUBMISSION_ID'"
                echo "Copying installer file without notarization tracking"
                cp "$installer_file" "release-files/$filename"
              fi
            else
              # No notarization submission (shouldn't happen, but handle it)
              echo "No submission ID for $filename, copying as-is"
              cp "$installer_file" "release-files/$filename"
            fi
          done

      # Set version variable early so SBOM step (WP-C2) and the release body
      # can both reference $CLEAN_VERSION. Moved before "Generate checksums"
      # so the SBOM filename is deterministic and the SBOM file itself is
      # present in release-files/ when checksums run (WP-C2 ordering requirement).
      - name: Set version variable
        run: |
          VERSION=${GITHUB_REF#refs/tags/v}
          echo "CLEAN_VERSION=${VERSION}" >> $GITHUB_ENV

      # WP-C2: Generate SPDX-JSON SBOM from source tree and drop it into
      # release-files/ so it is included in the checksums + uploaded with all
      # other release assets by the "Create release" step below.
      # upload-artifact/upload-release-assets are both disabled because this
      # workflow is tag-push triggered (not a `release` event), so we handle
      # distribution ourselves via the release-files/ directory.
      - name: Generate SBOM (SPDX JSON)
        uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
        with:
          path: .
          format: spdx-json
          output-file: release-files/mcpproxy-${{ env.CLEAN_VERSION }}.spdx.json
          upload-artifact: false
          upload-release-assets: false

      # WP-C4: Generate cumulative CHANGELOG.md with git-cliff and place it in
      # release-files/ so it is checksummed + uploaded alongside all other assets.
      #
      # Rationale for NOT committing back to main:
      #   Pushing a generated file back to main requires a PAT (GITHUB_TOKEN lacks
      #   push access on protected branches) and creates a race condition when two
      #   releases are cut close together.  Distributing CHANGELOG.md as a release
      #   asset is simpler, reproducible, and sufficient for open-source consumers.
      #
      # fetch-depth: 0 is already set on this job's Checkout step (line ~1065) so
      # git-cliff sees the full tag history without an extra checkout here.
      - name: Generate CHANGELOG (git-cliff)
        uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4.8.0
        with:
          config: cliff.toml
          args: --verbose
        env:
          OUTPUT: release-files/CHANGELOG.md
          GITHUB_REPO: ${{ github.repository }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Generate checksums
        id: checksums
        run: |
          cd release-files
          # checksum every artifact except the checksums file and its signature
          # bundle, so re-runs / pre-existing files can't make checksums.txt
          # reference itself. xargs -r guards against an empty artifact list.
          find . -maxdepth 1 -type f ! -name 'checksums.txt' ! -name 'checksums.txt.cosign.bundle' -printf '%P\n' \
            | sort | xargs -r sha256sum > checksums.txt
          echo "Generated checksums:"
          cat checksums.txt
          # base64-encode for SLSA provenance generator (WP-C3)
          echo "hashes=$(base64 -w0 checksums.txt)" >> "$GITHUB_OUTPUT"

      - name: List files for upload
        run: |
          echo "Files to upload:"
          ls -la release-files/
          echo "Pending notarizations:"
          ls -la pending-notarizations/ || echo "No pending notarizations"

      - name: Create release with binaries
        uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65  # v2.6.2
        with:
          files: release-files/*
          body: |
            ${{ needs.generate-notes.outputs.notes }}

            ---

            ## Download Installers

            | Platform | Download | Notes |
            |----------|----------|-------|
            | **macOS (Apple Silicon)** | [**Download DMG**](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-darwin-arm64-installer.dmg) | Signed & Notarized - Recommended for M1/M2/M3/M4 |
            | **macOS (Intel)** | [**Download DMG**](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-darwin-amd64-installer.dmg) | Signed & Notarized |
            | **Windows (64-bit)** | [**Download Setup**](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-setup-${{ github.ref_name }}-amd64.exe) | Setup wizard |
            | **Windows (ARM64)** | [**Download Setup**](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-setup-${{ github.ref_name }}-arm64.exe) | For ARM Windows devices |
            | **Linux Debian/Ubuntu (AMD64)** | [**Download .deb**](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy_${{ env.CLEAN_VERSION }}_amd64.deb) | `sudo apt install ./mcpproxy_*.deb` |
            | **Linux Debian/Ubuntu (ARM64)** | [**Download .deb**](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy_${{ env.CLEAN_VERSION }}_arm64.deb) | For ARM64 (Raspberry Pi etc.) |
            | **Linux Fedora/RHEL (AMD64)** | [**Download .rpm**](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-1.x86_64.rpm) | `sudo dnf install ./mcpproxy-*.rpm` |
            | **Linux Fedora/RHEL (ARM64)** | [**Download .rpm**](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-1.aarch64.rpm) | For ARM64 |
            | **Linux (AMD64) — tarball** | [**Download tar.gz**](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-linux-amd64.tar.gz) | Binary only |
            | **Linux (ARM64) — tarball** | [**Download tar.gz**](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-linux-arm64.tar.gz) | Binary only |

            **Homebrew (macOS/Linux):**
            ```bash
            brew install smart-mcp-proxy/mcpproxy/mcpproxy
            ```

            <details>
            <summary>Other download options (auto-update URLs, archives)</summary>

            **Auto-update URLs** (always points to latest):
            - [Linux AMD64](https://github.com/${{ github.repository }}/releases/latest/download/mcpproxy-latest-linux-amd64.tar.gz) | [Linux ARM64](https://github.com/${{ github.repository }}/releases/latest/download/mcpproxy-latest-linux-arm64.tar.gz)
            - [Windows AMD64](https://github.com/${{ github.repository }}/releases/latest/download/mcpproxy-latest-windows-amd64.zip) | [Windows ARM64](https://github.com/${{ github.repository }}/releases/latest/download/mcpproxy-latest-windows-arm64.zip)
            - [macOS AMD64](https://github.com/${{ github.repository }}/releases/latest/download/mcpproxy-latest-darwin-amd64.tar.gz) | [macOS ARM64](https://github.com/${{ github.repository }}/releases/latest/download/mcpproxy-latest-darwin-arm64.tar.gz)

            **Binary archives (this version):**
            - [macOS AMD64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-darwin-amd64.tar.gz) | [macOS ARM64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-darwin-arm64.tar.gz)
            - [Windows AMD64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-windows-amd64.zip) | [Windows ARM64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-windows-arm64.zip)

            </details>

            ---

            <details>
            <summary>Installation Instructions</summary>

            ### Windows
            1. Download the installer for your architecture
            2. Run `mcpproxy-setup-*.exe`
            3. Follow the installation wizard (requires Administrator privileges)
            4. Launch "MCPProxy" from Start Menu

            ### macOS
            1. Download the signed DMG for your Mac
            2. Double-click the DMG to mount it
            3. Double-click the PKG installer inside
            4. Follow the installation wizard
            5. Launch mcpproxy.app from Applications folder

            ### Linux / Manual Installation
            1. Download the appropriate archive
            2. Extract: `tar -xzf mcpproxy-*.tar.gz`
            3. Make executable: `chmod +x mcpproxy`
            4. Run: `./mcpproxy serve`

            </details>

            <details>
            <summary>Platform Support & Usage</summary>

            ### Platform Support
            - **macOS**: Full system tray support with menu and icons
            - **Windows**: Full system tray support with menu and icons
            - **Linux**: Headless mode only (CLI)

            ### Usage

            **GUI (Recommended):**
            - Launch mcpproxy.app from Applications (auto-starts core server)
            - Manages server via system tray menu

            **CLI:**
            ```bash
            mcpproxy serve                           # Start server
            mcpproxy serve --listen 127.0.0.1:8081  # Custom port
            export MCPPROXY_API_KEY=your-secret-key  # Set API key
            ```

            </details>
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Install cosign
        uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1

      - name: Sign checksums (keyless)
        # Cosign keyless signing — OIDC identity comes from the GitHub Actions JWT
        # (id-token: write is granted at the workflow level).
        # Produces a single bundle file (signature + certificate + Rekor transparency log entry).
        #
        # To verify after download:
        #   cosign verify-blob \
        #     --bundle checksums.txt.cosign.bundle \
        #     --certificate-identity-regexp "^https://github.com/smart-mcp-proxy/mcpproxy-go/.github/workflows/release.yml@refs/tags/v" \
        #     --certificate-oidc-issuer https://token.actions.githubusercontent.com \
        #     checksums.txt
        run: |
          cosign sign-blob \
            --yes \
            --bundle release-files/checksums.txt.cosign.bundle \
            release-files/checksums.txt

      - name: Upload cosign bundle to release
        run: |
          gh release upload "${{ github.ref_name }}" \
            release-files/checksums.txt.cosign.bundle \
            --clobber
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Upload pending notarizations
        if: hashFiles('pending-notarizations/*.pending') != ''
        run: |
          # Upload pending notarization files as release assets
          for pending_file in pending-notarizations/*.pending; do
            if [ -f "$pending_file" ]; then
              echo "Uploading pending notarization: $(basename "$pending_file")"
              gh release upload "${{ github.ref_name }}" "$pending_file" --clobber
            fi
          done
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  # WP-C3: SLSA build provenance — generates a SLSA v1 attestation for all
  # release artifacts and uploads it to the GitHub release as a .intoto.jsonl file.
  provenance:
    needs: [release]
    permissions:
      actions: read       # detect the GitHub Actions environment (trusted builder)
      id-token: write     # keyless signing of the provenance via OIDC
      contents: write     # upload the attestation to the release
    # SLSA generator MUST be referenced by semver tag, not SHA — it verifies its own
    # ref to establish the trusted builder identity; SHA or branch references break the
    # provenance trust model. This is the documented exception to the repo's SHA-pin rule
    # (see WP-C3 spec, WP-B5 exception).
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
    with:
      base64-subjects: ${{ needs.release.outputs.hashes }}
      upload-assets: true   # attach the .intoto.jsonl attestation to the release

  update-homebrew:
    needs: release
    runs-on: ubuntu-latest
    environment: production
    # Stable channel only: skip pre-release tags (v*-rc1, v*-beta, etc.) so an
    # RC tag never overwrites the stable formula/cask in the tap.
    if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') && github.repository == 'smart-mcp-proxy/mcpproxy-go'

    steps:
      - name: Checkout tap repository
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1
        with:
          repository: smart-mcp-proxy/homebrew-mcpproxy
          token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
          path: tap

      - name: Download platform binaries and calculate SHA256
        run: |
          VERSION=${GITHUB_REF#refs/tags/v}
          echo "Processing version: ${VERSION}"

          # Wait a bit for GitHub release assets to be available
          sleep 15

          # Define platforms
          PLATFORMS=("darwin-arm64" "darwin-amd64" "linux-arm64" "linux-amd64")
          BASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}"

          # Download each platform tarball and calculate SHA256
          for PLATFORM in "${PLATFORMS[@]}"; do
            TARBALL="mcpproxy-${VERSION}-${PLATFORM}.tar.gz"
            URL="${BASE_URL}/${TARBALL}"
            echo "Downloading ${PLATFORM}: ${URL}"

            # Try downloading with retries
            for i in {1..5}; do
              echo "  Attempt ${i}/5..."
              if curl -fsSL "${URL}" -o "${TARBALL}"; then
                echo "  Download successful"
                break
              else
                echo "  Download failed, retrying in 10 seconds..."
                sleep 10
              fi

              if [ $i -eq 5 ]; then
                echo "All download attempts failed for ${PLATFORM}"
                curl -I "${URL}" || true
                exit 1
              fi
            done

            # Calculate SHA256 and export as environment variable
            SHA=$(sha256sum "${TARBALL}" | cut -d' ' -f1)
            VAR_NAME="SHA256_$(echo ${PLATFORM} | tr '-' '_' | tr '[:lower:]' '[:upper:]')"
            echo "${VAR_NAME}=${SHA}" >> $GITHUB_ENV
            echo "  SHA256: ${SHA}"

            # Clean up tarball
            rm -f "${TARBALL}"
          done

          echo "VERSION=${VERSION}" >> $GITHUB_ENV

      - name: Generate Homebrew formula
        run: |
          cd tap
          mkdir -p Formula

          cat > Formula/mcpproxy.rb << 'FORMULA_EOF'
          class Mcpproxy < Formula
            desc "Smart MCP Proxy - Intelligent tool discovery and proxying for MCP servers"
            homepage "https://github.com/smart-mcp-proxy/mcpproxy-go"
            version "${VERSION}"
            license "MIT"

            on_macos do
              if Hardware::CPU.arm?
                url "https://github.com/smart-mcp-proxy/mcpproxy-go/releases/download/v${VERSION}/mcpproxy-${VERSION}-darwin-arm64.tar.gz"
                sha256 "${SHA256_DARWIN_ARM64}"
              else
                url "https://github.com/smart-mcp-proxy/mcpproxy-go/releases/download/v${VERSION}/mcpproxy-${VERSION}-darwin-amd64.tar.gz"
                sha256 "${SHA256_DARWIN_AMD64}"
              end
            end

            on_linux do
              if Hardware::CPU.arm?
                url "https://github.com/smart-mcp-proxy/mcpproxy-go/releases/download/v${VERSION}/mcpproxy-${VERSION}-linux-arm64.tar.gz"
                sha256 "${SHA256_LINUX_ARM64}"
              else
                url "https://github.com/smart-mcp-proxy/mcpproxy-go/releases/download/v${VERSION}/mcpproxy-${VERSION}-linux-amd64.tar.gz"
                sha256 "${SHA256_LINUX_AMD64}"
              end
            end

            def install
              bin.install "mcpproxy"
              bin.install "mcpproxy-tray" if OS.mac? && File.exist?("mcpproxy-tray")
            end

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

          # Remove the 10-space YAML indentation from the formula
          sed -i 's/^          //' Formula/mcpproxy.rb

          # Substitute environment variables
          sed -i "s/\${VERSION}/${VERSION}/g" Formula/mcpproxy.rb
          sed -i "s/\${SHA256_DARWIN_ARM64}/${SHA256_DARWIN_ARM64}/g" Formula/mcpproxy.rb
          sed -i "s/\${SHA256_DARWIN_AMD64}/${SHA256_DARWIN_AMD64}/g" Formula/mcpproxy.rb
          sed -i "s/\${SHA256_LINUX_ARM64}/${SHA256_LINUX_ARM64}/g" Formula/mcpproxy.rb
          sed -i "s/\${SHA256_LINUX_AMD64}/${SHA256_LINUX_AMD64}/g" Formula/mcpproxy.rb

          echo "Formula created successfully:"
          cat Formula/mcpproxy.rb

      - name: Download installer DMGs and calculate SHA256 for cask
        run: |
          VERSION=${GITHUB_REF#refs/tags/v}
          BASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}"

          for ARCH in arm64 amd64; do
            DMG="mcpproxy-${VERSION}-darwin-${ARCH}-installer.dmg"
            URL="${BASE_URL}/${DMG}"
            echo "Downloading ${DMG}..."
            for i in {1..5}; do
              echo "  Attempt ${i}/5..."
              if curl -fsSL "${URL}" -o "${DMG}"; then
                echo "  Download successful"
                break
              else
                echo "  Download failed, retrying in 10 seconds..."
                sleep 10
              fi
              if [ $i -eq 5 ]; then
                echo "All download attempts failed for ${DMG}"
                curl -I "${URL}" || true
                exit 1
              fi
            done
            SHA=$(sha256sum "${DMG}" | cut -d' ' -f1)
            VAR_NAME="SHA256_CASK_$(echo ${ARCH} | tr '[:lower:]' '[:upper:]')"
            echo "${VAR_NAME}=${SHA}" >> $GITHUB_ENV
            echo "  SHA256: ${SHA}"
            rm -f "${DMG}"
          done

      - name: Update Homebrew cask
        run: |
          cd tap
          mkdir -p Casks
          CASK_FILE="Casks/mcpproxy.rb"

          if [ ! -f "${CASK_FILE}" ]; then
            echo "Cask file not found at ${CASK_FILE}, skipping cask update"
            exit 0
          fi

          # Bump version and both sha256 values in place
          sed -i "s/version \"[^\"]*\"/version \"${VERSION}\"/" "${CASK_FILE}"
          # arm64 sha256 is the first occurrence, amd64 is the second
          sed -i "0,/sha256 \"[^\"]*\"/{s/sha256 \"[^\"]*\"/sha256 \"${SHA256_CASK_ARM64}\"/}" "${CASK_FILE}"
          sed -i "0,/sha256 \"${SHA256_CASK_ARM64}\"/{b}; s/sha256 \"[^\"]*\"/sha256 \"${SHA256_CASK_AMD64}\"/" "${CASK_FILE}"

          echo "Cask updated:"
          cat "${CASK_FILE}"

      - name: Commit and push changes
        run: |
          cd tap
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add Formula/mcpproxy.rb Casks/mcpproxy.rb

          # Check if there are changes to commit
          if git diff --staged --quiet; then
            echo "No changes to commit"
          else
            git commit -m "Update mcpproxy to ${VERSION} (pre-built binaries)"
            git push
            echo "Changes committed and pushed successfully"
          fi

  # Publish signed apt/yum repositories to Cloudflare R2 (apt.mcpproxy.app, rpm.mcpproxy.app)
  publish-linux-repos:
    needs: [build, release]
    # Stable channel only: skip pre-release tags (v*-rc1, v*-beta, etc.)
    if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') && github.repository == 'smart-mcp-proxy/mcpproxy-go'
    runs-on: ubuntu-latest
    environment: production

    env:
      APT_BUCKET: mcpproxy-apt
      RPM_BUCKET: mcpproxy-rpm
      APT_BASE_URL: https://apt.mcpproxy.app
      RPM_BASE_URL: https://rpm.mcpproxy.app
      GPG_KEY_ID: ${{ vars.PACKAGES_GPG_KEY_ID }}
      RETAIN_N: "10"
      AWS_ENDPOINT_URL: https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
      AWS_DEFAULT_REGION: auto
      AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
      PACKAGES_GPG_PRIVATE_KEY: ${{ secrets.PACKAGES_GPG_PRIVATE_KEY }}
      PACKAGES_GPG_PASSPHRASE: ${{ secrets.PACKAGES_GPG_PASSPHRASE }}

    steps:
      - name: Checkout
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1

      - name: Install repo tooling
        run: |
          sudo apt-get update -qq
          sudo apt-get install -y -qq apt-utils createrepo-c gnupg rpm
          aws --version

      - name: Download package artifacts
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093  # v4.3.0
        with:
          path: release-artifacts
          pattern: linux-packages-*
          merge-multiple: true

      - name: List downloaded artifacts
        run: |
          echo "Contents of release-artifacts:"
          find release-artifacts -type f | sort

      - name: Publish apt + rpm repositories to R2
        run: contrib/linux-repos/publish.sh release-artifacts

      - name: Smoke-test install (Debian)
        run: contrib/linux-repos/smoke-test-debian.sh "${GITHUB_REF_NAME#v}"

      - name: Smoke-test install (Fedora)
        run: contrib/linux-repos/smoke-test-fedora.sh "${GITHUB_REF_NAME#v}"

  # Deploy documentation to Cloudflare Pages
  deploy-docs:
    needs: release
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v') && github.repository == 'smart-mcp-proxy/mcpproxy-go'
    # Non-blocking: docs failure doesn't block release
    continue-on-error: true

    steps:
      - name: Checkout
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1

      - name: Setup Node.js
        uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020  # v4.4.0
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: website/package-lock.json

      - name: Set version for docs
        run: |
          VERSION="${{ github.ref_name }}"
          # Extract minor version: v0.11.2 → 0.11
          MINOR_VERSION=$(echo "$VERSION" | sed 's/^v//' | cut -d. -f1,2)
          echo "MCPPROXY_VERSION=$MINOR_VERSION" >> $GITHUB_ENV
          echo "Documentation version: $MINOR_VERSION"

      - name: Install dependencies
        working-directory: website
        run: npm ci

      - name: Build docs with version
        working-directory: website
        run: |
          # Inject version into config
          sed -i "s/__VERSION__/$MCPPROXY_VERSION/g" docusaurus.config.js
          npm run build

      - name: Deploy to Cloudflare Pages
        uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f  # v3.9.0
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy website/build --project-name=mcpproxy-docs

  # Trigger marketing site to update download links
  trigger-marketing-update:
    needs: release
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v') && github.repository == 'smart-mcp-proxy/mcpproxy-go'
    # Non-blocking: marketing update failure doesn't block release
    continue-on-error: true

    steps:
      - name: Trigger marketing site update
        uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0  # v3.0.0
        with:
          token: ${{ secrets.MARKETING_SITE_DISPATCH_TOKEN }}
          repository: smart-mcp-proxy/mcpproxy.app-website
          event-type: update-version
          client-payload: '{"version": "${{ github.ref_name }}"}'

  mcp-registry:
    name: Publish to MCP Registry
    needs: release
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    continue-on-error: true
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1

      - name: Extract version from tag
        id: version
        run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

      - name: Install mcp-publisher
        run: |
          curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" | tar xz mcp-publisher

      - name: Authenticate to MCP Registry
        run: ./mcp-publisher login github-oidc

      - name: Update version in server.json
        run: |
          jq --arg v "${{ steps.version.outputs.VERSION }}" '.version = $v' server.json > server.tmp && mv server.tmp server.json

      - name: Publish to MCP Registry
        run: ./mcp-publisher publish
