mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
🛤️ ci: Limit GitNexus Deploys To Main And Dev Only (#13799)
This commit is contained in:
parent
4cb35945dc
commit
c820dfb9a0
2 changed files with 50 additions and 87 deletions
115
.github/workflows/gitnexus-deploy.yml
vendored
115
.github/workflows/gitnexus-deploy.yml
vendored
|
|
@ -2,14 +2,10 @@
|
|||
#
|
||||
# Architecture:
|
||||
# GitHub Actions (deploy)
|
||||
# 1. Resolves latest successful index runs for main, dev, and every
|
||||
# open PR that already has an index artifact (contributor-gated
|
||||
# upstream by the index workflow's author_association check)
|
||||
# 1. Resolves latest successful index runs for main and dev
|
||||
# 2. Downloads each matching .gitnexus/ artifact
|
||||
# 3. Rsyncs them into /opt/gitnexus/indexes/<name>/ on the droplet
|
||||
# 4. Removes any stale folders on the droplet for PRs that closed
|
||||
# (even though gitnexus-cleanup-pr.yml also handles that path,
|
||||
# this is a safety net in case the close event was missed)
|
||||
# 4. Removes any stale folders on the droplet that are not main/dev
|
||||
# 5. Pulls latest image, force-recreates gitnexus, reloads Caddy,
|
||||
# and polls docker health until the container reports healthy
|
||||
# The caddy container is untouched — no TLS churn.
|
||||
|
|
@ -58,14 +54,14 @@ on:
|
|||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'Optional PR number to post completion comment on (set by bot-triggered dispatches from gitnexus-index.yml)'
|
||||
description: 'Optional PR number for status comments from bot-triggered dispatches'
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: write # post completion comments on served PR indexes
|
||||
pull-requests: write # post status comments on PR command dispatches
|
||||
|
||||
# Global serialization. Earlier versions used per-ref concurrency with
|
||||
# cancel-in-progress so rapid pushes to the same ref coalesced but deploys
|
||||
|
|
@ -93,7 +89,12 @@ jobs:
|
|||
build-image:
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
(
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
(github.event.workflow_run.head_branch == 'main' ||
|
||||
github.event.workflow_run.head_branch == 'dev')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
|
|
@ -158,7 +159,7 @@ jobs:
|
|||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: write # post deploy-complete comments on served PR indexes
|
||||
pull-requests: write # post deploy-complete comments on PR command dispatches
|
||||
steps:
|
||||
- name: Checkout deploy config
|
||||
uses: actions/checkout@v4
|
||||
|
|
@ -217,62 +218,7 @@ jobs:
|
|||
core.info(`${branch}: run ${fresh.workflow_run.id} -> ${name}`);
|
||||
}
|
||||
|
||||
// --- open PRs with at least one successful index run ---
|
||||
// github.paginate handles the 100-per-page ceiling automatically
|
||||
// so the resolution works on repos with 200+ concurrent open PRs.
|
||||
const openPrs = await github.paginate(github.rest.pulls.list, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
});
|
||||
core.info(`Found ${openPrs.length} open PRs`);
|
||||
|
||||
// Parallelize artifact lookups in fixed-size batches so the
|
||||
// resolve step runs in seconds instead of minutes on big repos,
|
||||
// without burning the GitHub API rate limit all at once.
|
||||
const BATCH_SIZE = 10;
|
||||
const prMatches = [];
|
||||
for (let i = 0; i < openPrs.length; i += BATCH_SIZE) {
|
||||
const batch = openPrs.slice(i, i + BATCH_SIZE);
|
||||
const results = await Promise.all(
|
||||
batch.map(async (pr) => {
|
||||
const artifactName = `gitnexus-index-pr-${pr.number}`;
|
||||
const fresh = await latestArtifact(artifactName);
|
||||
return fresh ? { pr, artifactName, fresh } : null;
|
||||
}),
|
||||
);
|
||||
for (const hit of results) {
|
||||
if (hit) prMatches.push(hit);
|
||||
}
|
||||
}
|
||||
|
||||
// Cap to the N most recent PR indexes by artifact creation time.
|
||||
// On a 10GB droplet each index is ~130MB; 3 PRs + main + dev ≈
|
||||
// 650MB of index data, leaving headroom for the ~700MB Docker image
|
||||
// and OS. Older PR indexes are evicted by the prune step.
|
||||
const MAX_PR_INDEXES = 3;
|
||||
prMatches.sort(
|
||||
(a, b) => new Date(b.fresh.created_at) - new Date(a.fresh.created_at),
|
||||
);
|
||||
const keptPrs = prMatches.slice(0, MAX_PR_INDEXES);
|
||||
const evictedPrs = prMatches.slice(MAX_PR_INDEXES);
|
||||
|
||||
for (const { pr, artifactName, fresh } of keptPrs) {
|
||||
serve.push({
|
||||
name: `LibreChat-pr-${pr.number}`,
|
||||
artifactName,
|
||||
runId: fresh.workflow_run.id,
|
||||
});
|
||||
core.info(`PR #${pr.number}: run ${fresh.workflow_run.id} -> LibreChat-pr-${pr.number}`);
|
||||
}
|
||||
if (evictedPrs.length) {
|
||||
core.info(
|
||||
`Evicted ${evictedPrs.length} older PR indexes (cap=${MAX_PR_INDEXES}): ` +
|
||||
evictedPrs.map((e) => `#${e.pr.number}`).join(', '),
|
||||
);
|
||||
}
|
||||
core.info(`Serving ${keptPrs.length} PR indexes out of ${prMatches.length} with artifacts (${openPrs.length} open PRs total)`);
|
||||
core.info('PR index deploys are paused; serving main and dev only.');
|
||||
|
||||
if (!serve.length) {
|
||||
core.setFailed('No indexes to serve');
|
||||
|
|
@ -387,7 +333,7 @@ jobs:
|
|||
# ── Step 1: prune FIRST ────────────────────────────────
|
||||
# Remove any folders on the droplet that aren't in the active set.
|
||||
# This frees disk BEFORE rsyncing new data, which matters on a
|
||||
# 10GB disk where each index is ~130MB.
|
||||
# 10GB disk where each current index is ~400MB.
|
||||
echo "Pruning stale indexes (keeping: $ACTIVE_NAMES)"
|
||||
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \
|
||||
ACTIVE_NAMES="$ACTIVE_NAMES" bash <<'REMOTE'
|
||||
|
|
@ -415,8 +361,8 @@ jobs:
|
|||
# it into place. If rsync fails, the old index survives intact
|
||||
# and the partial temp dir is cleaned up — no production data
|
||||
# is lost. The brief period where both old + new exist costs
|
||||
# ~130MB of extra disk, but the prune step already freed
|
||||
# space from evicted PR indexes so this fits on a 10GB disk.
|
||||
# ~400MB of extra disk, but the prune step already freed
|
||||
# space from evicted indexes so this fits on a 10GB disk.
|
||||
for dir in staging/*/; do
|
||||
[ -d "$dir" ] || continue
|
||||
name=$(basename "$dir")
|
||||
|
|
@ -567,15 +513,41 @@ jobs:
|
|||
DEPLOY_STATUS: ${{ job.status }}
|
||||
with:
|
||||
script: |
|
||||
const deployUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
const matrix = JSON.parse(process.env.MATRIX || '[]');
|
||||
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);
|
||||
const dispatchPrRaw = process.env.DISPATCH_PR_NUMBER;
|
||||
if (!/^\d+$/.test(dispatchPrRaw)) {
|
||||
core.setFailed(`Invalid PR number: ${dispatchPrRaw}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const dispatchPrNum = Number(dispatchPrRaw);
|
||||
const servedPr = matrix.some((m) => m.name === `LibreChat-pr-${dispatchPrNum}`);
|
||||
|
||||
if (!servedPr) {
|
||||
const body = [
|
||||
'### GitNexus: PR deploy skipped',
|
||||
'',
|
||||
'PR-specific deploys are paused; only `LibreChat` and `LibreChat-dev` are currently served.',
|
||||
`[Deploy run](${deployUrl})`,
|
||||
].join('\n');
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: dispatchPrNum,
|
||||
body,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
prNum = dispatchPrNum;
|
||||
}
|
||||
// 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-'),
|
||||
|
|
@ -590,7 +562,6 @@ jobs:
|
|||
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'}`,
|
||||
|
|
|
|||
22
.github/workflows/gitnexus-index.yml
vendored
22
.github/workflows/gitnexus-index.yml
vendored
|
|
@ -267,35 +267,27 @@ jobs:
|
|||
pull-requests: write # post completion comments for /gitnexus command runs
|
||||
steps:
|
||||
# GitHub suppresses workflow_run events for workflow runs triggered
|
||||
# by GITHUB_TOKEN (to prevent recursive chaining). Command-triggered
|
||||
# index runs opt into a deploy by setting deploy_after=true.
|
||||
- name: Trigger deploy workflow after command-triggered runs
|
||||
if: inputs.deploy_after && needs.index.result == 'success'
|
||||
# by GITHUB_TOKEN (to prevent recursive chaining). Dispatches without
|
||||
# a PR number can still opt into a deploy by setting deploy_after=true.
|
||||
- name: Trigger deploy workflow after non-PR dispatches
|
||||
if: inputs.deploy_after && inputs.pr_number == '' && needs.index.result == 'success'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ inputs.pr_number }}
|
||||
with:
|
||||
script: |
|
||||
core.info('deploy_after=true; 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: process.env.PR_NUMBER || '',
|
||||
pr_number: '',
|
||||
},
|
||||
});
|
||||
|
||||
# Reply on the PR when the /gitnexus command path runs so the
|
||||
# requester knows the index step finished. This fires when
|
||||
# inputs.pr_number is set and reports the index job result. A
|
||||
# separate comment posts from the deploy workflow when the live
|
||||
# server has the fresh index.
|
||||
# inputs.pr_number is set and reports the index job result.
|
||||
- name: Comment on PR — index complete
|
||||
if: inputs.pr_number != ''
|
||||
uses: actions/github-script@v7
|
||||
|
|
@ -321,7 +313,7 @@ jobs:
|
|||
`[Index run](${runUrl})`,
|
||||
'',
|
||||
indexSucceeded
|
||||
? '⏳ Waiting for deploy to serve the fresh index…'
|
||||
? 'PR-specific deploys are paused; only `LibreChat` and `LibreChat-dev` are currently served.'
|
||||
: '_Index run failed — the previous index (if any) continues to be served._',
|
||||
].join('\n');
|
||||
await github.rest.issues.createComment({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue