name: Pulse Release Pipeline

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Version number (e.g., 4.30.0)'
        required: true
        type: string
      release_notes:
        description: 'Release notes (markdown) - generated by Claude'
        required: true
        type: string

concurrency:
  group: release-${{ github.event.inputs.version || github.ref || github.run_id }}
  cancel-in-progress: false

jobs:
  extract_version:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    outputs:
      version: ${{ steps.extract.outputs.version }}
      tag: ${{ steps.extract.outputs.tag }}
    steps:
      - name: Extract version
        id: extract
        run: |
          # Handle both tag push and workflow_dispatch
          if [ "${{ github.event_name }}" = "push" ]; then
            TAG="${GITHUB_REF#refs/tags/}"
            VERSION="${TAG#v}"
          else
            VERSION=$(jq -r '.inputs.version // ""' "$GITHUB_EVENT_PATH" 2>/dev/null || echo "")
            if [ -z "$VERSION" ]; then
              echo "::error::workflow_dispatch must include a version input"
              exit 1
            fi
            TAG="v${VERSION}"
          fi
          echo "tag=${TAG}" >> $GITHUB_OUTPUT
          echo "version=${VERSION}" >> $GITHUB_OUTPUT
          echo "Version: ${VERSION}, Tag: ${TAG}"

  version_guard:
    needs: extract_version
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Ensure VERSION file matches requested version
        run: |
          FILE_VERSION=$(cat VERSION | tr -d '\n')
          REQUESTED_VERSION="${{ needs.extract_version.outputs.version }}"
          if [ "$FILE_VERSION" != "$REQUESTED_VERSION" ]; then
            echo "::error::VERSION file ($FILE_VERSION) does not match requested version ($REQUESTED_VERSION)."
            echo "The VERSION file must be updated and committed before running release."
            exit 1
          fi
          echo "✓ VERSION file matches requested version ($REQUESTED_VERSION)"

  preflight_tests:
    needs:
      - extract_version
      - version_guard
    runs-on: ubuntu-latest
    timeout-minutes: 90
    permissions:
      contents: read
      packages: write
    env:
      FRONTEND_DIST: frontend-modern/dist
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: 'frontend-modern/package-lock.json'

      - name: Install frontend dependencies
        run: npm --prefix frontend-modern ci

      - name: Restore frontend build cache
        uses: actions/cache@v4
        with:
          path: frontend-modern/dist
          key: frontend-build-${{ hashFiles('frontend-modern/package-lock.json', 'frontend-modern/src/**/*', 'frontend-modern/index.html', 'frontend-modern/postcss.config.cjs', 'frontend-modern/tailwind.config.cjs') }}

      - name: Build frontend bundle for Go embed
        run: |
          if [ -d "$FRONTEND_DIST" ] && [ -f "$FRONTEND_DIST/index.html" ]; then
            echo "Using cached frontend build";
          else
            npm --prefix frontend-modern run build;
          fi
          rm -rf internal/api/frontend-modern
          mkdir -p internal/api/frontend-modern
          cp -r frontend-modern/dist internal/api/frontend-modern/

      - name: Lint frontend
        run: npm --prefix frontend-modern run lint

      - name: Install docker-compose
        run: |
          sudo apt-get update
          sudo apt-get install -y docker-compose

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.24'
          cache: true

      - name: Run backend tests
        run: make test

      - name: Cache Playwright browsers
        uses: actions/cache@v4
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ runner.os }}-${{ hashFiles('tests/integration/package-lock.json') }}

      - name: Prepare integration test dependencies
        working-directory: tests/integration
        run: |
          npm ci
          npx playwright install --with-deps chromium

      - name: Build Pulse for integration tests
        run: make build

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

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

      - name: Build and push staging Docker images
        uses: docker/build-push-action@v6
        with:
          context: .
          target: runtime
          platforms: linux/amd64,linux/arm64
          push: true
          provenance: false
          cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:buildcache
          cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:buildcache,mode=max
          tags: |
            ghcr.io/${{ github.repository_owner }}/pulse:staging-${{ needs.extract_version.outputs.tag }}

      - name: Build and push staging Docker agent image
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./Dockerfile
          target: agent_runtime
          platforms: linux/amd64,linux/arm64
          push: true
          provenance: false
          cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:buildcache
          cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:buildcache,mode=max
          tags: |
            ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:staging-${{ needs.extract_version.outputs.tag }}

      - name: Build Docker images for integration tests
        run: |
          docker build -t pulse-mock-github:test tests/integration/mock-github-server
          docker build -t pulse:test -f Dockerfile --target runtime --cache-from ghcr.io/${{ github.repository_owner }}/pulse:buildcache --build-arg BUILDKIT_INLINE_CACHE=1 .

      - name: Run update integration smoke tests
        working-directory: tests/integration
        env:
          MOCK_CHECKSUM_ERROR: "false"
          MOCK_NETWORK_ERROR: "false"
          MOCK_RATE_LIMIT: "false"
          MOCK_STALE_RELEASE: "false"
        run: |
          docker-compose -f docker-compose.test.yml up -d

          # Wait for services to be healthy
          echo "Waiting for mock-github to be healthy..."
          timeout 60 sh -c 'until docker inspect --format="{{json .State.Health.Status}}" pulse-mock-github | grep -q "healthy"; do sleep 2; done' || {
            echo "Mock GitHub failed to become healthy"
            docker logs pulse-mock-github
            exit 1
          }

          echo "Waiting for pulse-test-server to be healthy..."
          timeout 60 sh -c 'until docker inspect --format="{{json .State.Health.Status}}" pulse-test-server | grep -q "healthy"; do sleep 2; done' || {
            echo "Pulse server failed to become healthy"
            docker logs pulse-test-server
            exit 1
          }

          echo "All services healthy, verifying port mapping..."
          # Test that the host can actually reach the container through port mapping
          for i in 1 2 3 4 5; do
            if curl -f -s http://localhost:7655/api/health > /dev/null 2>&1; then
              echo "Port mapping verified: Pulse server is reachable from host"
              break
            elif [ $i -eq 5 ]; then
              echo "ERROR: Port mapping failed - cannot reach Pulse server from host"
              echo "Container healthcheck passed, but host cannot connect via localhost:7655"
              echo "Pulse server logs:"
              docker logs pulse-test-server || true
              echo "Mock GitHub logs:"
              docker logs pulse-mock-github || true
              exit 1
            else
              echo "Attempt $i: Server not yet reachable from host, waiting..."
              sleep 2
            fi
          done

          echo "Running API-level update integration test..."
          UPDATE_API_BASE_URL=http://localhost:7655 go test ../../tests/integration/api -run TestUpdateFlowIntegration -count=1

          echo "Skipping legacy Playwright update scenarios (removed until they can be rebuilt)"
          docker-compose -f docker-compose.test.yml down -v

      - name: Cleanup integration environment
        if: always()
        working-directory: tests/integration
        run: docker-compose -f docker-compose.test.yml down -v || true

  create_release:
    needs:
      - extract_version
      - preflight_tests
    runs-on: ubuntu-latest
    timeout-minutes: 30
    permissions:
      contents: write
    outputs:
      release_id: ${{ steps.create_release.outputs.release_id }}
      release_url: ${{ steps.create_release.outputs.release_url }}
      target_commitish: ${{ github.sha }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Fetch all history for changelog generation

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.24'
          cache: true

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: 'frontend-modern/package-lock.json'

      - name: Install dependencies
        run: |
          # Install zip for Windows binaries
          sudo apt-get update
          sudo apt-get install -y zip

      - name: Set up Helm
        uses: azure/setup-helm@v4
        with:
          version: 'v3.15.2'

      - name: Build release artifacts
        run: |
          echo "Building release ${{ needs.extract_version.outputs.tag }}..."
          ./scripts/build-release.sh ${{ needs.extract_version.outputs.version }}

      - name: Post-build health check
        run: |
          echo "Verifying server binary responds to --version and API health..."
          if [ -x ./pulse ]; then
            ./pulse --version
          elif [ -x ./cmd/pulse/pulse ]; then
            ./cmd/pulse/pulse --version
          else
            echo "::warning::Pulse binary not found after build-release; skipping version check"
          fi

      - name: Prepare release notes
        id: generate_notes
        run: |
          VERSION="${{ needs.extract_version.outputs.version }}"
          RELEASE_NOTES_INPUT=$(jq -r '.inputs.release_notes // ""' "$GITHUB_EVENT_PATH" 2>/dev/null || echo "")

          # Save release notes to file
          NOTES_FILE=$(mktemp)

          if [ -n "$RELEASE_NOTES_INPUT" ]; then
            echo "Using Claude-generated release notes from workflow input"
            printf "%s\n" "$RELEASE_NOTES_INPUT" > "$NOTES_FILE"
          else
            echo "Tag-triggered release - using placeholder notes"
            echo "Release $VERSION" > "$NOTES_FILE"
            echo "" >> "$NOTES_FILE"
            echo "See commit history for changes." >> "$NOTES_FILE"
          fi

          # Add installation instructions
          {
            echo ""
            echo "## Installation"
            echo ""
            echo "**Docker (recommended):**"
            echo '```bash'
            echo "docker pull rcourtman/pulse:${VERSION}"
            echo '```'
            echo ""
            echo "**Docker Compose:**"
            echo "Update your \`docker-compose.yml\` to use \`rcourtman/pulse:${VERSION}\`"
            echo ""
            echo "See the [Installation Guide](https://github.com/rcourtman/Pulse#installation) for complete setup instructions."
          } >> "$NOTES_FILE"

          echo "notes_file=${NOTES_FILE}" >> $GITHUB_OUTPUT

          echo "Release notes content:"
          cat "$NOTES_FILE"

      - name: Create tag
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          TAG="${{ needs.extract_version.outputs.tag }}"
          echo "Creating tag ${TAG}..."
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git tag -a "${TAG}" -m "Release ${TAG}"
          git push origin "${TAG}"

      - name: Create draft release
        id: create_release
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          TAG="${{ needs.extract_version.outputs.tag }}"
          NOTES_FILE="${{ steps.generate_notes.outputs.notes_file }}"

          echo "Creating draft release for ${TAG}..."

          # Tag must exist first - draft releases can't create tags (GitHub API limitation)
          # See: https://github.com/cli/cli/issues/11589
          # Create as draft first so we can upload assets before publishing
          RELEASE_JSON=$(gh api "repos/${{ github.repository }}/releases" \
            -X POST \
            -F tag_name="${TAG}" \
            -F name="Pulse ${TAG}" \
            -F body="$(cat $NOTES_FILE)" \
            -F draft=true)

          rm -f "$NOTES_FILE"

          RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id')
          RELEASE_URL=$(echo "$RELEASE_JSON" | jq -r '.html_url')

          if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
            echo "::error::Failed to extract release ID from API response"
            exit 1
          fi

          echo "release_url=${RELEASE_URL}" >> $GITHUB_OUTPUT
          echo "release_id=${RELEASE_ID}" >> $GITHUB_OUTPUT

          echo "✓ Draft release created: ${TAG} (ID: ${RELEASE_ID})"
      - name: Upload checksums.txt
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          TAG="${{ needs.extract_version.outputs.tag }}"

          echo "Uploading checksums.txt..."
          gh release upload "${TAG}" release/checksums.txt

          # Upload individual .sha256 files for backward compatibility
          echo "Uploading .sha256 checksum files..."
          gh release upload "${TAG}" release/*.sha256

      - name: Upload release assets
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          TAG="${{ needs.extract_version.outputs.tag }}"

          echo "Uploading release assets..."

          # Upload tarballs
          gh release upload "${TAG}" release/*.tar.gz

          # Upload Windows zip files
          gh release upload "${TAG}" release/*.zip

          # Upload Helm chart if it exists
          if ls release/*.tgz 1> /dev/null 2>&1; then
            echo "Uploading Helm chart..."
            gh release upload "${TAG}" release/*.tgz
          fi

          # Upload install scripts as standalone assets
          # Users can now use: curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install.sh | bash
          # This ensures scripts are version-locked to the release, not pulled from main branch
          gh release upload "${TAG}" release/install.sh
          gh release upload "${TAG}" release/install-sensor-proxy.sh
          gh release upload "${TAG}" release/install-docker.sh
          gh release upload "${TAG}" release/pulse-auto-update.sh

      - name: Publish release
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          TAG="${{ needs.extract_version.outputs.tag }}"
          RELEASE_ID="${{ steps.create_release.outputs.release_id }}"

          echo "Publishing release ${TAG}..."

          gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" \
            -X PATCH \
            -F draft=false \
            -F make_latest=true

          echo "✓ Release published as latest: ${TAG}"

      - name: Trigger demo server update
        continue-on-error: true  # Non-fatal if dispatch fails
        env:
          GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
        run: |
          TAG="${{ needs.extract_version.outputs.tag }}"
          echo "Triggering demo server update for ${TAG}..."

          # Publishing via API doesn't fire the release webhook, so we dispatch manually
          # Requires WORKFLOW_PAT secret with 'repo' and 'workflow' scopes
          gh workflow run update-demo-server.yml -f tag="${TAG}"

          echo "✓ Demo server update workflow dispatched"

      - name: Output release information
        run: |
          echo "✅ Release published successfully!"
          echo ""
          echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
          echo "📦 Release: ${{ needs.extract_version.outputs.tag }}"
          echo "🔗 URL: ${{ steps.create_release.outputs.release_url }}"
          echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
          echo ""
          echo "Docker images, Helm chart, and demo server will be updated automatically."
          echo ""

  validate_release_assets:
    needs:
      - extract_version
      - create_release
    uses: ./.github/workflows/validate-release-assets.yml
    secrets: inherit
    with:
      tag: ${{ needs.extract_version.outputs.tag }}
      version: ${{ needs.extract_version.outputs.version }}
      release_id: ${{ needs.create_release.outputs.release_id }}
      draft: false
      target_commitish: ${{ needs.create_release.outputs.target_commitish }}


