name: CI

on:
  push:
    branches: [main, staging]
  pull_request:
    branches: [main, staging]

# Cancel in-progress CI runs when a new commit arrives on the same ref.
# This prevents multiple stale runs from queuing behind each other and
# monopolising the self-hosted macOS arm64 runner.
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # Detect which paths changed so downstream jobs can skip when only
  # docs/markdown files were modified. Uses plain `git diff` — no macOS
  # dependency, so this runs on ubuntu-latest to free the self-hosted
  # macOS arm64 runner for jobs that genuinely need it.
  changes:
    name: Detect changes
    runs-on: ubuntu-latest
    outputs:
      platform: ${{ steps.check.outputs.platform }}
      canvas: ${{ steps.check.outputs.canvas }}
      python: ${{ steps.check.outputs.python }}
      scripts: ${{ steps.check.outputs.scripts }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - id: check
        run: |
          # For PR events: diff against the base branch (not HEAD~1 of the branch,
          # which may be unrelated after force-pushes). When a push updates a PR,
          # both pull_request and push events fire — prefer the PR base so that
          # the diff is always computed against the actual merge base, not the
          # previous SHA on the branch which may be on a different history line.
          BASE="${GITHUB_BASE_REF:-${{ github.event.before }}}"
          # GITHUB_BASE_REF is set by GitHub for PR events (the base branch name).
          # For pull_request events we use the stored base.sha; for push events
          # (or when base.sha is unavailable) fall back to github.event.before.
          if [ "${{ github.event_name }}" = "pull_request" ] && [ -n "${{ github.event.pull_request.base.sha }}" ]; then
            BASE="${{ github.event.pull_request.base.sha }}"
          fi
          # Fallback: if BASE is empty or all zeros (new branch), run everything
          if [ -z "$BASE" ] || echo "$BASE" | grep -qE '^0+$'; then
            echo "platform=true" >> "$GITHUB_OUTPUT"
            echo "canvas=true" >> "$GITHUB_OUTPUT"
            echo "python=true" >> "$GITHUB_OUTPUT"
            echo "scripts=true" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          DIFF=$(git diff --name-only "$BASE" HEAD 2>/dev/null || echo ".github/workflows/ci.yml")
          echo "platform=$(echo "$DIFF" | grep -qE '^workspace-server/|^\.github/workflows/ci\.yml$' && echo true || echo false)" >> "$GITHUB_OUTPUT"
          echo "canvas=$(echo "$DIFF" | grep -qE '^canvas/|^\.github/workflows/ci\.yml$' && echo true || echo false)" >> "$GITHUB_OUTPUT"
          echo "python=$(echo "$DIFF" | grep -qE '^workspace/|^\.github/workflows/ci\.yml$' && echo true || echo false)" >> "$GITHUB_OUTPUT"
          echo "scripts=$(echo "$DIFF" | grep -qE '^tests/e2e/|^scripts/|^\.github/workflows/ci\.yml$' && echo true || echo false)" >> "$GITHUB_OUTPUT"

  platform-build:
    name: Platform (Go)
    needs: changes
    if: needs.changes.outputs.platform == 'true'
    runs-on: [self-hosted, macos, arm64]
    defaults:
      run:
        working-directory: workspace-server
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: 'stable'
      - run: go mod download
      - run: go build ./cmd/server
      # CLI (molecli) moved to standalone repo: github.com/Molecule-AI/molecule-cli
      - run: go vet ./...
      - name: Run golangci-lint
        uses: golangci/golangci-lint-action@v9
        with:
          version: latest
          working-directory: workspace-server
          args: --timeout 3m
        continue-on-error: true  # Warn but don't block until codebase is clean
      - name: Run tests with race detection and coverage
        run: go test -race -coverprofile=coverage.out ./...
      - name: Check coverage baseline
        run: |
          COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
          echo "Total coverage: ${COVERAGE}%"
          THRESHOLD=25
          awk "BEGIN{if ($COVERAGE < $THRESHOLD) exit 1}" || {
            echo "::error::Coverage ${COVERAGE}% is below the ${THRESHOLD}% threshold"
            exit 1
          }

  canvas-build:
    name: Canvas (Next.js)
    needs: changes
    if: needs.changes.outputs.canvas == 'true'
    runs-on: [self-hosted, macos, arm64]
    defaults:
      run:
        working-directory: canvas
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      - run: rm -f package-lock.json && npm install
      - run: npm run build
      - name: Run tests
        run: npx vitest run

  # MCP Server + SDK removed from CI — now in standalone repos:
  # - github.com/Molecule-AI/molecule-mcp-server (npm CI)
  # - github.com/Molecule-AI/molecule-sdk-python (PyPI CI)

  # e2e-api job moved to .github/workflows/e2e-api.yml (issue #458).
  # It now has workflow-level concurrency (cancel-in-progress: false) so
  # new pushes queue the E2E run rather than cancelling it at the run level.

  shellcheck:
    name: Shellcheck (E2E scripts)
    needs: changes
    if: needs.changes.outputs.scripts == 'true'
    runs-on: [self-hosted, macos, arm64]
    steps:
      - uses: actions/checkout@v4
      - name: Run shellcheck on tests/e2e/*.sh
        # `ludeeus/action-shellcheck` is a Docker action (Linux-only). We rely
        # on shellcheck being pre-installed on the self-hosted runner instead.
        run: |
          if ! command -v shellcheck >/dev/null 2>&1; then
            echo "::error::shellcheck is not installed on the runner"
            exit 1
          fi
          find tests/e2e -type f -name '*.sh' -print0 \
            | xargs -0 shellcheck --severity=warning

  canvas-deploy-reminder:
    name: Canvas Deploy Reminder
    runs-on: [self-hosted, macos, arm64]
    needs: [changes, canvas-build]
    # Only fires on direct pushes to main (i.e. after staging→main promotion).
    if: needs.changes.outputs.canvas == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/main'
    permissions:
      # Required to post commit comments via the GitHub API.
      contents: write
    steps:
      - name: Post deploy reminder as commit comment
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          COMMIT_SHA: ${{ github.sha }}
          RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
        run: |
          # Write body to a temp file — avoids backtick escaping in shell.
          cat > /tmp/deploy-reminder.md << 'BODY'
          ## Canvas build passed ✅ — deploy required

          The `publish-canvas-image` workflow is now building a fresh Docker image
          (`ghcr.io/molecule-ai/canvas:latest`) in the background.

          Once it completes (~3–5 min), apply on the host machine with:
          ```bash
          cd <runner-workspace>
          git pull origin main
          docker compose pull canvas && docker compose up -d canvas
          ```

          If you need to rebuild from local source instead (e.g. testing unreleased
          changes or a new `NEXT_PUBLIC_*` URL), use:
          ```bash
          docker compose build canvas && docker compose up -d canvas
          ```
          BODY
          printf '\n> Posted automatically by CI · commit `%s` · [build log](%s)\n' \
            "$COMMIT_SHA" "$RUN_URL" >> /tmp/deploy-reminder.md

          gh api \
            --method POST \
            "repos/${{ github.repository }}/commits/${{ github.sha }}/comments" \
            --field "body=@/tmp/deploy-reminder.md"

  python-lint:
    name: Python Lint & Test
    needs: changes
    if: needs.changes.outputs.python == 'true'
    runs-on: [self-hosted, macos, arm64]
    env:
      WORKSPACE_ID: test
    defaults:
      run:
        working-directory: workspace
    steps:
      - uses: actions/checkout@v4
      # setup-python@v5 cannot write to /Users/runner (GitHub-hosted path) on
      # the self-hosted macOS arm64 runner (user: <runner-user>) and also hits
      # EACCES on /usr/local/bin due to macOS SIP. Skip it — Homebrew installs
      # Python 3.11 at /opt/homebrew/opt/python@3.11 which is already on PATH.
      - name: Verify Python 3.11 (Homebrew)
        run: |
          export PATH="/opt/homebrew/opt/python@3.11/bin:/opt/homebrew/bin:$PATH"
          python3.11 --version
          echo "/opt/homebrew/opt/python@3.11/bin" >> "$GITHUB_PATH"
          echo "/opt/homebrew/bin" >> "$GITHUB_PATH"
      - run: pip3.11 install -r requirements.txt pytest pytest-asyncio pytest-cov
      - run: python3.11 -m pytest --tb=short -q --cov=. --cov-report=term-missing

      # SDK + plugin validation moved to standalone repo:
      # github.com/Molecule-AI/molecule-sdk-python
