From 546f006e42bb496cfb8b474b03d620de9f42df65 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 11 Apr 2026 18:15:56 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=AC=20feat:=20Serialize=20GitNexus=20D?= =?UTF-8?q?eploys=20and=20Post=20Completion=20Comments=20on=20PR=20Command?= =?UTF-8?q?s=20(#12623)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related changes that tighten the GitNexus CI/CD loop. Serialized deploys - Previous concurrency group was keyed by head ref with cancel-in-progress, which let deploys targeting different refs (e.g. main push + PR command) run in parallel. That's a data race: the prune-stale-indexes step computes active_names up front, so deploy A rsyncing /opt/gitnexus/indexes/LibreChat-pr-12580 can collide with deploy B pruning the same folder based on a pre-rsync view of the active set. - Collapse to a single global group gitnexus-deploy with cancel-in-progress: false. All deploys queue behind one another. A rsync/docker-compose restart is never killed mid-operation. The 20-minute job timeout bounds queue depth. PR completion feedback - Add a "index complete" comment step in gitnexus-index.yml that fires only when inputs.pr_number is set (i.e. the run came via the /gitnexus command). Posts success or failure with a link to the run and whether embeddings were generated. - Add a "deploy complete" comment step in gitnexus-deploy.yml that handles both trigger paths: workflow_run from a native PR auto-index (PR number recovered from the matrix entry whose runId matches the trigger run), and workflow_dispatch from the index workflow's bot- fallback path (PR number passed through as a new inputs.pr_number). - Plumb inputs.pr_number through the bot-fallback dispatch in gitnexus-index.yml so the deploy workflow knows where to comment for command-triggered runs. - Only comments on the PR that asked for the index, never broadcasts. Workflow rename - Drop the "DigitalOcean" suffix from the deploy workflow's display name and filename. The platform is still DO (.do/gitnexus/ still holds the compose + caddy config) but the workflow itself is platform-agnostic in form and the suffix was visual noise. - File renamed gitnexus-deploy-do.yml -> gitnexus-deploy.yml. - Concurrency group and all cross-references updated in lock-step. - permissions at deploy job level now includes pull-requests: write so the completion comment can post. --- ...exus-deploy-do.yml => gitnexus-deploy.yml} | 97 +++++++++++++++++-- .github/workflows/gitnexus-index.yml | 45 ++++++++- 2 files changed, 129 insertions(+), 13 deletions(-) rename .github/workflows/{gitnexus-deploy-do.yml => gitnexus-deploy.yml} (81%) diff --git a/.github/workflows/gitnexus-deploy-do.yml b/.github/workflows/gitnexus-deploy.yml similarity index 81% rename from .github/workflows/gitnexus-deploy-do.yml rename to .github/workflows/gitnexus-deploy.yml index 6a086fe522..986d5b5503 100644 --- a/.github/workflows/gitnexus-deploy-do.yml +++ b/.github/workflows/gitnexus-deploy.yml @@ -1,4 +1,4 @@ -# Deploys GitNexus indexes to a DigitalOcean droplet via SSH + rsync. +# Deploys GitNexus indexes to a droplet via SSH + rsync. # # Architecture: # GitHub Actions (deploy) @@ -49,26 +49,39 @@ # GITNEXUS_DO_KNOWN_HOST β€” output of `ssh-keyscan -H ` pinning the # droplet's host keys (prevents MITM/TOFU risk) -name: GitNexus Deploy (DigitalOcean) +name: GitNexus Deploy on: workflow_run: workflows: ['GitNexus Index'] types: [completed] workflow_dispatch: + inputs: + pr_number: + description: 'Optional PR number to post completion comment on (set by bot-triggered dispatches from gitnexus-index.yml)' + type: string + default: '' permissions: actions: read contents: read - pull-requests: read + pull-requests: write # post completion comments on served PR indexes -# Per-ref concurrency: rapid pushes to the same branch/PR coalesce, but -# deploys targeting different refs can still run in parallel. Safe because -# each deploy only rsync's its own folder; final step always reflects the -# latest state for all refs discovered at resolve time. +# Global serialization. Earlier versions used per-ref concurrency with +# cancel-in-progress so rapid pushes to the same ref coalesced but deploys +# targeting different refs ran in parallel. That had a data race: the +# prune-stale-indexes step computes its active_names up front, so if +# deploy A is rsyncing /opt/gitnexus/indexes/LibreChat-pr-12580 while +# deploy B (started slightly later with a different ref) prunes, B can +# rm -rf a folder A is still uploading into. +# +# All deploys now queue behind a single group. cancel-in-progress is +# false so a running rsync/docker-compose restart never gets killed +# mid-operation (which would leave the droplet in a partial state). +# The 20-minute job timeout bounds total queue depth. concurrency: - group: gitnexus-deploy-do-${{ github.event.workflow_run.head_branch || github.ref }} - cancel-in-progress: true + group: gitnexus-deploy + cancel-in-progress: false env: GITNEXUS_VERSION: '1.5.3' @@ -145,7 +158,7 @@ jobs: permissions: actions: read contents: read - pull-requests: read + pull-requests: write # post deploy-complete comments on served PR indexes steps: - name: Checkout deploy config uses: actions/checkout@v4 @@ -450,6 +463,70 @@ jobs: docker compose logs --tail 30 gitnexus || true REMOTE + # When the deploy was triggered by a PR command path, post a + # terminal status comment on that one PR only. Two sub-cases: + # + # 1. workflow_run trigger: the PR's native auto-index run fired + # workflow_run, so github.event.workflow_run.id is the trigger. + # Find the matching PR via the matrix entry whose runId matches. + # + # 2. workflow_dispatch trigger with inputs.pr_number set: the + # index workflow's bot-fallback path dispatched us directly + # because workflow_run is suppressed for GITHUB_TOKEN triggers. + # Use inputs.pr_number as the comment target. + # + # Broadcast-commenting on every active PR would be noise β€” only the + # PR that asked for a fresh index gets a reply. + - name: Comment on PR β€” deploy complete + if: always() + uses: actions/github-script@v7 + env: + MATRIX: ${{ steps.resolve.outputs.matrix }} + TRIGGER_RUN_ID: ${{ github.event.workflow_run.id }} + DISPATCH_PR_NUMBER: ${{ github.event.inputs.pr_number }} + DEPLOY_STATUS: ${{ job.status }} + with: + script: | + let prNum = null; + + // Case 1: dispatched directly with pr_number (bot-fallback path) + if (process.env.DISPATCH_PR_NUMBER && process.env.DISPATCH_PR_NUMBER !== '') { + prNum = parseInt(process.env.DISPATCH_PR_NUMBER, 10); + } + // Case 2: workflow_run trigger from a PR index run + else if (context.eventName === 'workflow_run') { + const matrix = JSON.parse(process.env.MATRIX || '[]'); + const triggerRunId = Number(process.env.TRIGGER_RUN_ID); + const match = matrix.find( + (m) => m.runId === triggerRunId && m.name.startsWith('LibreChat-pr-'), + ); + if (match) { + prNum = parseInt(match.name.replace('LibreChat-pr-', ''), 10); + } + } + + if (!prNum) { + core.info('No PR to comment on (trigger was not a PR-scoped index); skipping.'); + return; + } + + const deployUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const ok = process.env.DEPLOY_STATUS === 'success'; + const body = [ + `### GitNexus: ${ok ? 'πŸš€ deployed' : '❌ deploy failed'}`, + '', + ok + ? `The \`LibreChat-pr-${prNum}\` index is now live on the MCP server.` + : `The deploy failed β€” the previous index (if any) continues to be served.`, + `[Deploy run](${deployUrl})`, + ].join('\n'); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNum, + body, + }); + - name: Cleanup SSH key if: always() run: rm -f ~/.ssh/deploy_key diff --git a/.github/workflows/gitnexus-index.yml b/.github/workflows/gitnexus-index.yml index 4a649b8483..3b0c052c81 100644 --- a/.github/workflows/gitnexus-index.yml +++ b/.github/workflows/gitnexus-index.yml @@ -31,7 +31,8 @@ on: permissions: contents: read - actions: write # needed to dispatch gitnexus-deploy-do.yml on bot-triggered runs + 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 @@ -168,10 +169,48 @@ jobs: uses: actions/github-script@v7 with: script: | - core.info('Triggering actor is github-actions[bot]; workflow_run would not fire. Dispatching gitnexus-deploy-do.yml manually.'); + 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-do.yml', + 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, });