name: CI

on:
  push:
    branches: [main]
  pull_request:

# Least privilege: none of the jobs write to the repo.
permissions:
  contents: read

# Cancel superseded runs on the same ref to save Actions minutes.
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  focused-test-guidance:
    name: Focused test guidance (report-only)
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    continue-on-error: true
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          fetch-depth: 0
          persist-credentials: false
      - name: Report changed test paths
        env:
          BASE_SHA: ${{ github.event.pull_request.base.sha }}
          HEAD_SHA: ${{ github.event.pull_request.head.sha }}
        run: |
          report_file="$RUNNER_TEMP/focused-test-guidance.md"
          publish_report() {
            cat "$report_file"
            if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
              cat "$report_file" >> "$GITHUB_STEP_SUMMARY" || true
            fi
            return 0
          }

          report_unavailable() {
            {
              printf '%s\n\n' '## Focused test guidance unavailable (report-only)'
              printf '%s\n\n' "$1"
              printf '%s\n' 'Existing blocking CI remains the source of truth.'
            } > "$report_file"
            publish_report
            exit 0
          }

          if [ -z "$BASE_SHA" ] || [ -z "$HEAD_SHA" ]; then
            report_unavailable "Pull request base/head metadata is missing."
          fi

          if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
            report_unavailable "The pull request base commit is unavailable locally."
          fi

          if ! git cat-file -e "${HEAD_SHA}^{commit}" 2>/dev/null; then
            report_unavailable "The pull request head commit is unavailable locally."
          fi

          if ! python3 .github/scripts/focused_test_guidance.py \
            --base-sha "$BASE_SHA" \
            --head-sha "$HEAD_SHA" > "$report_file"; then
            report_unavailable "The focused test guidance helper could not produce a report."
          fi

          publish_report

  python-syntax:
    name: Python syntax (compileall)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          persist-credentials: false
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: "3.11"
      # Byte-compile sources — catches syntax errors without installing deps.
      - run: python -m compileall -q app.py core routes src services scripts tests

  node-syntax:
    name: JS syntax (node --check)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          persist-credentials: false
      - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e  # v6.4.0
        with:
          node-version: "20"
      # Syntax-check our own JS (skip vendored libs in static/lib).
      - name: node --check
        run: |
          shopt -s globstar nullglob
          for f in static/app.js static/js/**/*.js; do
            node --check "$f"
          done

  python-tests:
    name: Python tests (pytest)
    runs-on: ubuntu-latest
    # Informational for now: the suite has known flaky / environment-dependent
    # failures (test isolation + embedding-model assertions). Tracked under the
    # ROADMAP "fresh install smoke tests" item; make this required once green.
    continue-on-error: true
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          fetch-depth: 0
          persist-credentials: false

      # Detect whether this PR only touches documentation files.
      # If so, skip the expensive pytest run while still reporting a passing check.
      - name: Check for docs-only changes
        id: docs-check
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            BASE="${{ github.event.pull_request.base.sha }}"
            HEAD="${{ github.event.pull_request.head.sha }}"
          else
            BASE="${{ github.event.before }}"
            HEAD="${{ github.sha }}"
          fi
          # List all changed files; if every file matches docs/markdown patterns, skip pytest.
          changed=$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || git diff --name-only HEAD~1 HEAD)
          non_docs=$(echo "$changed" | grep -Ev '^(docs/|.*\.md$|\.github/[^/]+\.md$)' || true)
          if [ -z "$non_docs" ]; then
            echo "docs_only=true" >> "$GITHUB_OUTPUT"
            echo "Docs-only change detected — skipping pytest."
          else
            echo "docs_only=false" >> "$GITHUB_OUTPUT"
          fi

      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        if: steps.docs-check.outputs.docs_only != 'true'
        with:
          python-version: "3.11"
          cache: pip
      - run: pip install -r requirements.txt
        if: steps.docs-check.outputs.docs_only != 'true'
      - run: mkdir -p data  # sqlite DB lives at ./data/app.db
        if: steps.docs-check.outputs.docs_only != 'true'
      - run: python -m pytest -q
        if: steps.docs-check.outputs.docs_only != 'true'
