name: GitNexus Index

on:
  push:
    branches: [main, dev]
    paths-ignore: ['**.md', 'docs/**', 'LICENSE', '.github/**']
  pull_request:
    branches: [main, dev]
    paths-ignore: ['**.md', 'docs/**', 'LICENSE', '.github/**']
  workflow_dispatch:
    inputs:
      embeddings:
        description: 'Enable embedding generation (slow, increases index size)'
        type: boolean
        default: false
      force:
        description: 'Force full re-index'
        type: boolean
        default: false
      # When invoked from the /gitnexus index PR command, the command
      # workflow fills these so the index is built from the PR's head
      # ref and uploaded under the PR-numbered artifact name.
      pr_number:
        description: 'PR number to index (set by /gitnexus command)'
        type: string
        default: ''
      pr_ref:
        description: 'PR head SHA or ref to check out (set by /gitnexus command)'
        type: string
        default: ''

permissions:
  contents: read
  actions: write # dispatch gitnexus-deploy.yml on bot-triggered runs
  pull-requests: write # post completion comments for /gitnexus command runs

concurrency:
  # When triggered by the /gitnexus command, group by PR number so rapid
  # re-runs coalesce. Otherwise group by git ref as before.
  group: gitnexus-${{ inputs.pr_number != '' && format('pr-{0}', inputs.pr_number) || github.ref }}
  cancel-in-progress: true

env:
  GITNEXUS_VERSION: '1.5.3'

jobs:
  index:
    # Push + dispatch run unconditionally. Native pull_request events
    # are restricted to PRs authored by danny-avila only — this keeps
    # automatic CI spend low on a repo with 200+ open PRs.
    #
    # Other contributors' PRs can still be indexed on demand:
    #   - /gitnexus index    (PR comment command, contributor-gated)
    #   - workflow_dispatch   (manual dispatch from Actions UI)
    # Both bypass this filter because they arrive as workflow_dispatch,
    # not pull_request.
    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.user.login == 'danny-avila'
    runs-on: ubuntu-latest
    timeout-minutes: 25
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          # When the /gitnexus command dispatches us with a pr_ref, it's
          # a refs/pull/<N>/head ref that GitHub mirrors into the base
          # repo for every PR, so checkout works for fork PRs too. When
          # pr_ref is empty (native push/pull_request), fall back to the
          # default ref actions/checkout would use.
          ref: ${{ inputs.pr_ref || '' }}
          fetch-depth: 1

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 24

      - name: Cache npm store
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: gitnexus-npm-${{ runner.os }}-${{ env.GITNEXUS_VERSION }}
          restore-keys: gitnexus-npm-${{ runner.os }}-

      - name: Run GitNexus Analyze
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          FLAGS="--skip-agents-md --verbose"

          # Decide whether to generate embeddings. Rules:
          #   push (main/dev)      -> always embed
          #   pull_request         -> embed ONLY when the PR changes files
          #                           under paths that also trigger backend
          #                           or frontend unit tests (api/, client/,
          #                           packages/). Docs/config-only PRs skip
          #                           embeddings to save ~3-5 min of CI.
          #   workflow_dispatch    -> respect the explicit `embeddings` input
          #                           (default false). This also covers the
          #                           /gitnexus index [embeddings] command.
          ENABLE_EMBEDDINGS=false
          case "${{ github.event_name }}" in
            workflow_dispatch)
              [ "${{ inputs.embeddings }}" = "true" ] && ENABLE_EMBEDDINGS=true
              ;;
            push)
              ENABLE_EMBEDDINGS=true
              ;;
            pull_request)
              PR_NUM="${{ github.event.pull_request.number }}"
              CHANGED=$(gh api "repos/${{ github.repository }}/pulls/$PR_NUM/files" \
                --paginate --jq '.[].filename' 2>/dev/null || echo "")
              if printf '%s\n' "$CHANGED" | grep -qE '^(api/|client/|packages/)'; then
                echo "PR #$PR_NUM touches unit-test paths (api|client|packages) — enabling embeddings"
                ENABLE_EMBEDDINGS=true
              else
                echo "PR #$PR_NUM does not touch unit-test paths — graph-only index"
              fi
              ;;
          esac

          if [ "$ENABLE_EMBEDDINGS" = "true" ]; then
            FLAGS="$FLAGS --embeddings"
          fi
          if [ "${{ inputs.force }}" = "true" ]; then
            FLAGS="$FLAGS --force"
          fi
          npx --yes gitnexus@${{ env.GITNEXUS_VERSION }} analyze . $FLAGS

      - name: Verify index
        run: |
          if [ ! -d ".gitnexus" ] || [ ! -f ".gitnexus/meta.json" ]; then
            echo "::error::GitNexus index was not created"
            exit 1
          fi
          echo "::group::Index metadata"
          cat .gitnexus/meta.json
          echo ""
          echo "::endgroup::"

      - name: Upload GitNexus index
        uses: actions/upload-artifact@v4
        with:
          # Artifact naming order of precedence:
          #   1. /gitnexus command dispatch: inputs.pr_number -> pr-<N>
          #   2. Native pull_request event: github.event.pull_request.number
          #   3. Push or manual dispatch without pr_number: github.ref_name
          name: >-
            gitnexus-index-${{
              inputs.pr_number != ''
              && format('pr-{0}', inputs.pr_number)
              || (github.event_name == 'pull_request'
                  && format('pr-{0}', github.event.pull_request.number)
                  || github.ref_name)
            }}
          path: .gitnexus/
          include-hidden-files: true
          retention-days: 30

      # GitHub suppresses workflow_run events for workflow runs whose
      # triggering actor is GITHUB_TOKEN (to prevent recursive chaining).
      # That means when this workflow is dispatched by gitnexus-pr-command
      # via `gh api workflow_dispatch`, the deploy workflow's workflow_run
      # trigger never fires. Manually dispatch the deploy here in that
      # specific case — user-triggered runs continue to rely on the
      # existing workflow_run trigger, so we don't double-deploy.
      - name: Trigger deploy workflow for bot-triggered runs
        if: github.triggering_actor == 'github-actions[bot]'
        uses: actions/github-script@v7
        with:
          script: |
            core.info('Triggering actor is github-actions[bot]; workflow_run would not fire. Dispatching gitnexus-deploy.yml manually.');
            // Pass pr_number through so the deploy workflow knows which
            // PR to post its completion comment on (for /gitnexus
            // command runs this will be set; for other bot dispatches
            // it's empty and the deploy step falls back to matrix match).
            await github.rest.actions.createWorkflowDispatch({
              owner: context.repo.owner,
              repo: context.repo.repo,
              workflow_id: 'gitnexus-deploy.yml',
              ref: 'main',
              inputs: {
                pr_number: '${{ inputs.pr_number }}',
              },
            });

      # Reply on the PR when the /gitnexus command path runs so the
      # requester knows the index step finished. This only fires when
      # inputs.pr_number is set (command-triggered) AND the rest of the
      # job succeeded. A separate comment posts from the deploy workflow
      # when the live server has the fresh index.
      - name: Comment on PR — index complete
        if: always() && inputs.pr_number != ''
        uses: actions/github-script@v7
        with:
          script: |
            const outcome = '${{ job.status }}' === 'success' ? '✅ indexed' : '❌ index failed';
            const prNum = parseInt('${{ inputs.pr_number }}', 10);
            const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
            const embeddingsFlag = '${{ inputs.embeddings }}' === 'true' ? 'with embeddings' : 'graph-only';
            const body = [
              `### GitNexus: ${outcome}`,
              ``,
              `PR #${prNum} was indexed ${embeddingsFlag}.`,
              `[Index run](${runUrl})`,
              '',
              '${{ job.status }}' === 'success'
                ? '⏳ Waiting for deploy to serve the fresh index…'
                : '_Index run failed — the previous index (if any) continues to be served._',
            ].join('\n');
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: prNum,
              body,
            });
