diff --git a/.github/workflows/gitnexus-index.yml b/.github/workflows/gitnexus-index.yml index 914b0a35f9..048351c1f7 100644 --- a/.github/workflows/gitnexus-index.yml +++ b/.github/workflows/gitnexus-index.yml @@ -45,8 +45,10 @@ jobs: index: # Allow push + dispatch unconditionally; filter native pull_request # events to contributors only. The /gitnexus command workflow does - # its own contributor check before it dispatches this workflow, so - # workflow_dispatch is always trusted here. + # its own contributor-commenter check before it dispatches this + # workflow, so workflow_dispatch is always trusted here — including + # the case where the commenter wants to index a non-contributor or + # fork PR (the command uses refs/pull//head so checkout resolves). if: | github.event_name != 'pull_request' || github.event.pull_request.author_association == 'OWNER' || @@ -58,8 +60,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - # When the /gitnexus command dispatches us with a pr_ref, check - # out that exact SHA. Otherwise fall back to the default ref. + # When the /gitnexus command dispatches us with a pr_ref, it's + # a refs/pull//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 @@ -76,11 +81,43 @@ jobs: restore-keys: gitnexus-npm-${{ runner.os }}- - name: Run GitNexus Analyze + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | FLAGS="--skip-agents-md --verbose" - # Auto-enable embeddings for main branch pushes; opt-in via dispatch input elsewhere - if [ "${{ inputs.embeddings }}" = "true" ] || \ - ([ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]); then + + # 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 diff --git a/.github/workflows/gitnexus-pr-command.yml b/.github/workflows/gitnexus-pr-command.yml index 4ce58bb2b8..b299beb3b1 100644 --- a/.github/workflows/gitnexus-pr-command.yml +++ b/.github/workflows/gitnexus-pr-command.yml @@ -1,18 +1,29 @@ # Responds to `/gitnexus index` comments on pull requests. # # Gated to the same author_association roles (OWNER, MEMBER, COLLABORATOR) -# as the automatic PR index trigger. When a matching comment lands on a -# PR, this workflow dispatches `gitnexus-index.yml` with the PR number -# and head SHA so the existing indexing pipeline does the actual work. +# as the automatic PR index trigger, but applied to the COMMENTER, not +# the PR author. This intentionally lets a contributor index a PR from +# a non-contributor / first-time fork author — the contributor takes +# responsibility for the trust boundary by typing the command. +# +# When a matching comment lands on a PR, this workflow dispatches +# `gitnexus-index.yml` with the PR number and the `refs/pull//head` +# ref so indexing works for fork PRs too (GitHub mirrors every PR's +# head ref into the base repo regardless of which fork it originated +# from, so actions/checkout can always resolve it). # # Use cases: # - Re-index a PR after a rebase without pushing a new commit # - Index a docs-only PR that was skipped by paths-ignore +# - Index a non-contributor (fork) PR that the auto-trigger skipped # - Re-run a failed index # # Supported commands: -# /gitnexus index — index with defaults (no embeddings) -# /gitnexus index embeddings — index with --embeddings enabled +# /gitnexus index — index the PR with embeddings (default) +# /gitnexus index embeddings — explicit form of the above; same effect +# /gitnexus index fast — graph-only index (skip embeddings), for +# a quick re-index without waiting ~5 min +# of embedding generation name: GitNexus PR Command @@ -31,7 +42,9 @@ concurrency: jobs: dispatch: - # Only run for PR comments that start with /gitnexus from trusted users + # Only run for PR comments that start with /gitnexus from trusted + # commenters. Intentionally checks the COMMENTER's association so a + # contributor can index a non-contributor's PR on demand. if: | github.event.issue.pull_request != null && startsWith(github.event.comment.body, '/gitnexus') && @@ -41,7 +54,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - name: Parse command and resolve PR head + - name: Parse command and resolve PR head ref id: parse uses: actions/github-script@v7 with: @@ -49,7 +62,7 @@ jobs: const body = context.payload.comment.body.trim(); const match = body.match(/^\/gitnexus\s+(\w+)(?:\s+(\w+))?/); if (!match) { - core.setFailed(`Unrecognized command: ${body}. Try: /gitnexus index [embeddings]`); + core.setFailed(`Unrecognized command: ${body}. Try: /gitnexus index [fast]`); return; } const [, subcommand, modifier] = match; @@ -57,19 +70,27 @@ jobs: core.setFailed(`Unknown subcommand: ${subcommand}. Only 'index' is supported.`); return; } - const embeddings = modifier === 'embeddings' ? 'true' : 'false'; + // Default to embeddings on — a contributor typing the command + // has already decided they want a full re-index. The `fast` + // modifier is the explicit opt-out for graph-only runs. + // `embeddings` is accepted as a no-op alias for backwards + // compat with the previous command form. + let embeddings = 'true'; + if (modifier === 'fast' || modifier === 'graph-only' || modifier === 'no-embeddings') { + embeddings = 'false'; + } - // Resolve the PR's head SHA — listWorkflowDispatchRun needs a real ref. + // Use refs/pull//head instead of the raw head SHA. GitHub + // mirrors every PR's head into the base repo as this ref, so + // actions/checkout can always resolve it — even for PRs from + // forks whose raw SHAs don't exist in the base repo. const prNum = context.payload.issue.number; - const { data: pr } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: prNum, - }); core.setOutput('pr_number', String(prNum)); - core.setOutput('pr_ref', pr.head.sha); + core.setOutput('pr_ref', `refs/pull/${prNum}/head`); core.setOutput('embeddings', embeddings); - core.info(`Dispatching index for PR #${prNum} at ${pr.head.sha} (embeddings=${embeddings})`); + core.info( + `Dispatching index for PR #${prNum} at refs/pull/${prNum}/head (embeddings=${embeddings}, modifier=${modifier || '(none)'})`, + ); - name: Dispatch gitnexus-index workflow uses: actions/github-script@v7