💬 feat: Serialize GitNexus Deploys and Post Completion Comments on PR Commands (#12623)

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.
This commit is contained in:
Danny Avila 2026-04-11 18:15:56 -04:00 committed by GitHub
parent 8cb5c62fa1
commit 546f006e42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 129 additions and 13 deletions

View file

@ -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 <host>` 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

View file

@ -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,
});