mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 04:07:05 +00:00
🔭 ci: Codegraph Test-Selection Probe (Observe-Only) (#14936)
* ci: codegraph test-selection probe (observe-only) Asks the codegraph service which test files and matrix jobs the PR needs and writes the decision to the job summary. Gates nothing — every path exits 0, forks without secrets no-op. Companion to the shadow-mode evaluation: the decision CI would act on, made visible next to the runs it would have replaced. * chore: remove GitNexus CI and deployment configs Superseded by the codegraph service: the index workflow spent ~45min per invocation building an artifact the PR flow never served, while the replacement indexes incrementally in ~1.4s per commit server-side. Removes the four workflows (index, deploy, cleanup-pr, pr-command) and the .do/gitnexus deployment bundle. No remaining references. * ci: render playwright spec tiers in the codegraph probe summary
This commit is contained in:
parent
f9876eaaf0
commit
c14b8c54eb
10 changed files with 72 additions and 1390 deletions
|
|
@ -1,25 +0,0 @@
|
|||
# Caddy reverse proxy with bearer token auth and automatic HTTPS.
|
||||
# The domain is supplied via environment variable GITNEXUS_DOMAIN,
|
||||
# and the auth token via API_TOKEN. Both are set in docker-compose.yml.
|
||||
|
||||
{$GITNEXUS_DOMAIN} {
|
||||
# Health check — unauthenticated so monitoring can probe it
|
||||
@health path /health
|
||||
handle @health {
|
||||
reverse_proxy gitnexus:4747 {
|
||||
rewrite /api/info
|
||||
}
|
||||
}
|
||||
|
||||
# All other routes require bearer token
|
||||
@authed {
|
||||
header Authorization "Bearer {$API_TOKEN}"
|
||||
}
|
||||
|
||||
handle @authed {
|
||||
reverse_proxy gitnexus:4747
|
||||
}
|
||||
|
||||
# Reject unauthenticated requests
|
||||
respond "Unauthorized" 401
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
# Long-lived GitNexus image for DigitalOcean droplet deployment.
|
||||
#
|
||||
# This image does NOT bake in the index data. Indexes are mounted from
|
||||
# the host at /indexes/<repo>/.gitnexus/ and registered at container
|
||||
# startup. A fresh index only requires rsync + container restart — no
|
||||
# image rebuild on every push.
|
||||
|
||||
FROM node:24.16.0-slim
|
||||
|
||||
ARG GITNEXUS_VERSION=1.6.7
|
||||
# Pin the native DB to match the index workflow; gitnexus's ^0.17.0 range
|
||||
# would otherwise let the served image drift from the CI-produced index.
|
||||
ARG LADYBUG_VERSION=0.17.1
|
||||
|
||||
# 1. Build native addons with Bookworm toolchain, then remove build tools.
|
||||
# curl stays for the docker healthcheck; Caddy lives in its own container.
|
||||
# LadybugDB is pinned nested under gitnexus so step 3's require() resolves it.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends python3 make g++ curl \
|
||||
&& npm install -g gitnexus@${GITNEXUS_VERSION} \
|
||||
&& npm install --no-save --prefix /usr/local/lib/node_modules/gitnexus "@ladybugdb/core@${LADYBUG_VERSION}" \
|
||||
&& apt-get purge -y --auto-remove python3 make g++ \
|
||||
&& rm -rf /var/lib/apt/lists/* /root/.npm
|
||||
|
||||
# 2. Upgrade libstdc++ from Trixie — @ladybugdb/core prebuilt binary needs
|
||||
# GLIBCXX_3.4.32 which Bookworm (3.4.31) doesn't ship.
|
||||
RUN echo "deb http://deb.debian.org/debian trixie main" > /etc/apt/sources.list.d/trixie.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y -t trixie libstdc++6 \
|
||||
&& rm /etc/apt/sources.list.d/trixie.list \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 3. Pre-install LadybugDB FTS + vector extensions so ~/.kuzu/extension/
|
||||
# is baked into the image. gitnexus serve loads extensions with a
|
||||
# load-only policy and never installs them at runtime, so the cache
|
||||
# must already exist. (GitNexus loads the vector extension itself
|
||||
# via loadVectorExtension — no adapter patch needed.)
|
||||
COPY install-extensions.js /tmp/install-extensions.js
|
||||
RUN node /tmp/install-extensions.js && rm -rf /tmp/install-extensions.js /tmp/lbug-ext-install
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
EXPOSE 4747
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
# GitNexus stack for the DigitalOcean droplet.
|
||||
#
|
||||
# Two services: the gitnexus server (bound to an internal network only)
|
||||
# and a Caddy reverse proxy that handles TLS + auth.
|
||||
#
|
||||
# Index data lives on the host at /opt/gitnexus/indexes/ and is
|
||||
# bind-mounted read-write into the gitnexus container. The deploy
|
||||
# workflow rsyncs fresh indexes into that directory and restarts
|
||||
# only the gitnexus container — Caddy keeps running undisturbed.
|
||||
#
|
||||
# Break-glass: if gitnexus is stuck unhealthy and you need to restart
|
||||
# just Caddy (e.g. to push an emergency Caddyfile fix), the
|
||||
# `depends_on: condition: service_healthy` would block:
|
||||
# docker compose up -d caddy
|
||||
# Use --no-deps to bypass the dependency check:
|
||||
# docker compose up -d --no-deps caddy
|
||||
|
||||
name: gitnexus
|
||||
|
||||
# Shared logging defaults applied to both services so the droplet's
|
||||
# disk doesn't fill up with unbounded json-file logs.
|
||||
x-logging: &default-logging
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: '50m'
|
||||
max-file: '3'
|
||||
|
||||
services:
|
||||
gitnexus:
|
||||
# Override via GITNEXUS_IMAGE in /opt/gitnexus/.env to use a fork or
|
||||
# a pinned version tag like :v1.5.3 for reproducible rollbacks.
|
||||
image: ${GITNEXUS_IMAGE:-ghcr.io/danny-avila/librechat-gitnexus:latest}
|
||||
container_name: gitnexus
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- gitnexus-net
|
||||
volumes:
|
||||
- /opt/gitnexus/indexes:/indexes
|
||||
# memswap_limit equal to mem_limit disables swap for this container.
|
||||
# Without it, Docker lets the process silently swap onto host disk,
|
||||
# turning sub-second graph queries into multi-second ones. Hard
|
||||
# OOM-kill is preferable — the container restarts via unless-stopped,
|
||||
# the deploy health poll catches it, and the failure is explicit.
|
||||
mem_limit: 1792m
|
||||
memswap_limit: 1792m
|
||||
logging: *default-logging
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-fsS', 'http://127.0.0.1:4747/api/info']
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
container_name: gitnexus-caddy
|
||||
restart: unless-stopped
|
||||
# service_healthy (not just service_started) ensures Caddy doesn't
|
||||
# start routing traffic until gitnexus passes its initial healthcheck
|
||||
# on a cold `compose up`. This only governs initial startup ordering —
|
||||
# during force-recreates of gitnexus, Caddy stays up and may briefly
|
||||
# return 502 while the new gitnexus container binds its port. The
|
||||
# deploy workflow's health poll catches any sustained failure.
|
||||
depends_on:
|
||||
gitnexus:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
networks:
|
||||
- gitnexus-net
|
||||
volumes:
|
||||
- /opt/gitnexus/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
logging: *default-logging
|
||||
environment:
|
||||
GITNEXUS_DOMAIN: ${GITNEXUS_DOMAIN}
|
||||
API_TOKEN: ${API_TOKEN}
|
||||
|
||||
networks:
|
||||
gitnexus-net:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Cap Node heap below the container's cgroup limit (1792m in compose),
|
||||
# leaving room for @ladybugdb/core's C++ heap and OS overhead. Native
|
||||
# allocations happen outside V8's view, so a slim V8 budget is the only
|
||||
# thing between a heavy query and a cgroup OOM-kill. Without this cap,
|
||||
# gitnexus defaults to --max-old-space-size=8192 and reserves memory
|
||||
# the container doesn't have.
|
||||
export NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=1280}"
|
||||
|
||||
# Register every index mounted under /indexes/<name>/.gitnexus/.
|
||||
# This is idempotent — re-registering an existing repo updates the
|
||||
# metadata pointer without touching the index data.
|
||||
#
|
||||
# Registration failure handling:
|
||||
# - main (LibreChat) and dev (LibreChat-dev) are critical. If either
|
||||
# fails to register, exit 1 so docker marks the container unhealthy
|
||||
# and the deploy workflow's readiness check surfaces the error.
|
||||
# - PR indexes (LibreChat-pr-*) are best-effort. A corrupt PR index
|
||||
# shouldn't take the whole server down.
|
||||
if [ -d /indexes ]; then
|
||||
for dir in /indexes/*/; do
|
||||
[ -d "$dir" ] || continue
|
||||
name=$(basename "$dir")
|
||||
[ -d "$dir.gitnexus" ] || continue
|
||||
echo "Registering index: $name"
|
||||
if ! gitnexus index "$dir" --allow-non-git; then
|
||||
case "$name" in
|
||||
LibreChat|LibreChat-dev)
|
||||
echo "ERROR: failed to register critical index $name" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo "WARN: failed to register PR index $name — skipping" >&2
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "WARN: /indexes directory not mounted" >&2
|
||||
fi
|
||||
|
||||
# Bind 0.0.0.0 inside the container so Caddy (in a separate container
|
||||
# on the same docker network) can reach gitnexus at gitnexus:4747.
|
||||
# docker-compose.yml intentionally does NOT expose port 4747 on the
|
||||
# host — only Caddy's 80/443 are published.
|
||||
exec gitnexus serve --host 0.0.0.0 --port 4747
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
/**
|
||||
* Pre-install LadybugDB extensions (FTS + vector) into the Docker image's
|
||||
* extension cache (~/.kuzu/extension/). Without this, gitnexus serve's
|
||||
* lbug-adapter calls LOAD EXTENSION fts at runtime but fails silently
|
||||
* because the extension was never installed, causing all BM25 and
|
||||
* semantic queries via the query() tool to return empty.
|
||||
*
|
||||
* Workaround for upstream GitNexus 1.5.3 bug where the CI-produced
|
||||
* .gitnexus/ artifact doesn't include the extension cache.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// @ladybugdb/core lives under the globally-installed gitnexus package.
|
||||
// This path is stable across gitnexus versions because npm always nests
|
||||
// transitive deps under the installed package's node_modules.
|
||||
const lbugPath = '/usr/local/lib/node_modules/gitnexus/node_modules/@ladybugdb/core';
|
||||
const lbug = require(lbugPath);
|
||||
|
||||
const tmpDir = '/tmp/lbug-ext-install';
|
||||
fs.mkdirSync(tmpDir, { recursive: true });
|
||||
|
||||
// Open a throwaway database just to run INSTALL against. The extension
|
||||
// cache persists in ~/.kuzu/extension/ regardless of which database was
|
||||
// used to install it, so the throwaway db and tmpDir are deleted in the
|
||||
// Dockerfile after this script finishes.
|
||||
const db = new lbug.Database(path.join(tmpDir, 'db'), 0, false, false);
|
||||
const conn = new lbug.Connection(db);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await conn.query('INSTALL fts');
|
||||
console.log('FTS extension installed');
|
||||
} catch (err) {
|
||||
console.error('FTS install failed:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
await conn.query('INSTALL vector');
|
||||
console.log('Vector extension installed');
|
||||
} catch (err) {
|
||||
console.error('Vector install failed:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
72
.github/workflows/codegraph-select.yml
vendored
Normal file
72
.github/workflows/codegraph-select.yml
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Codegraph test selection — OBSERVE-ONLY.
|
||||
#
|
||||
# Asks the codegraph service which test files / matrix jobs this PR actually needs and writes
|
||||
# the answer to the job summary. It gates NOTHING: no workflow reads its outputs yet, it cannot
|
||||
# fail the PR (every path exits 0), and forks without secrets no-op silently. This is the
|
||||
# production probe for the shadow-mode evaluation: the same decision CI would act on, made
|
||||
# visible next to the runs it would have replaced.
|
||||
#
|
||||
# Requires repo secrets: CODEGRAPH_URL (https endpoint), CODEGRAPH_TOKEN (bearer).
|
||||
name: Codegraph Select (observe)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
select:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
steps:
|
||||
- name: Ask codegraph, render, never fail
|
||||
env:
|
||||
URL: ${{ secrets.CODEGRAPH_URL }}
|
||||
TOKEN: ${{ secrets.CODEGRAPH_TOKEN }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR: ${{ github.event.pull_request.number }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
set +e
|
||||
note() { echo "$1" >> "$GITHUB_STEP_SUMMARY"; }
|
||||
note "### Codegraph select — observe-only"
|
||||
if [ -z "$URL" ] || [ -z "$TOKEN" ]; then note "_secrets not configured; skipped_"; exit 0; fi
|
||||
|
||||
gh api "repos/$REPO/pulls/$PR/files" --paginate \
|
||||
--jq '.[] | {path: .filename, status, patch}' | jq -s . > files.json
|
||||
if [ ! -s files.json ]; then note "_could not fetch changed files; skipped_"; exit 0; fi
|
||||
|
||||
jq -c --arg b "$BASE_SHA" --arg h "$HEAD_SHA" \
|
||||
'{files: ., lockBaseSha: $b, lockHeadSha: $h}' files.json > body.json
|
||||
RESP=$(curl -sS -m 45 -H "Authorization: Bearer $TOKEN" \
|
||||
-H 'content-type: application/json' --data-binary @body.json "$URL/v1/select")
|
||||
if [ -z "$RESP" ] || ! echo "$RESP" | jq -e .selected >/dev/null 2>&1; then
|
||||
note "_codegraph unavailable (${RESP:0:120}); skipped — full CI runs as always_"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "$RESP" | jq -r '
|
||||
"graph `\(.gate.head[0:12] // "?")` · \(.engine) · \(.mode) · \(.ms)ms · reached \(.reached)",
|
||||
"",
|
||||
"| workspace | decision |",
|
||||
"|---|---|",
|
||||
(.selected | to_entries[] |
|
||||
"| \(.key) | " + (if .value.mode == "FULL" then "FULL — \(.value.why)"
|
||||
elif .value.mode == "NONE" then "no tests"
|
||||
else "\(.value.files | length) test files" end) + " |"),
|
||||
"",
|
||||
"matrix: " + ([.matrix | to_entries[] | .key as $wf | .value | to_entries[] |
|
||||
"\($wf)/\(.key)=" + (if .value then "run" else "SKIP" end)] | join(" ")),
|
||||
(if .shards then "shards: " + (.shards | to_json) else empty end),
|
||||
(if .lock_workspaces then "lockfile → " + (.lock_workspaces | to_json) else empty end),
|
||||
(if .e2e and (.e2e.error | not) then
|
||||
"e2e tiers: must \(.e2e.must_run | length) · floor \(.e2e.floor | length) · skippable \(.e2e.skippable | length)" +
|
||||
(if (.e2e.must_run | length) > 0 then " — must: " + (.e2e.must_run[:4] | join(", ")) else "" end)
|
||||
else empty end)
|
||||
' >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
91
.github/workflows/gitnexus-cleanup-pr.yml
vendored
91
.github/workflows/gitnexus-cleanup-pr.yml
vendored
|
|
@ -1,91 +0,0 @@
|
|||
# Removes a PR's GitNexus index from the droplet when the PR is closed
|
||||
# (merged or not). The deploy workflow also prunes stale folders as a
|
||||
# safety net, but this gives us immediate cleanup without waiting for
|
||||
# the next deploy trigger.
|
||||
|
||||
name: GitNexus Cleanup PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
concurrency:
|
||||
group: gitnexus-cleanup-pr-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
# Skip fork PRs entirely. GitHub withholds repository secrets from
|
||||
# pull_request events originating on forks, so an SSH deploy job run
|
||||
# from a fork close would fail noisily. The deploy workflow's stale-
|
||||
# folder pruning step catches any fork-contributor indexes that
|
||||
# actually made it onto the droplet.
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
# Skip the SSH round-trip entirely when no index artifact was ever
|
||||
# built for this PR (docs-only PRs, paths-ignored PRs, PRs closed
|
||||
# before indexing finished, etc). Eliminates ~95% of no-op SSH
|
||||
# sessions on a busy repo.
|
||||
- name: Check for index artifact
|
||||
id: check
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const { data } = await github.rest.actions.listArtifactsForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: `gitnexus-index-pr-${context.payload.pull_request.number}`,
|
||||
per_page: 1,
|
||||
});
|
||||
const hasArtifact = data.total_count > 0;
|
||||
core.info(`Artifact exists: ${hasArtifact}`);
|
||||
core.setOutput('has_artifact', hasArtifact ? 'true' : 'false');
|
||||
|
||||
- name: Setup SSH
|
||||
if: steps.check.outputs.has_artifact == 'true'
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.GITNEXUS_DO_SSH_KEY }}
|
||||
KNOWN_HOST: ${{ secrets.GITNEXUS_DO_KNOWN_HOST }}
|
||||
run: |
|
||||
set -e
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
if [ -z "$KNOWN_HOST" ]; then
|
||||
echo "::error::GITNEXUS_DO_KNOWN_HOST secret is empty"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$KNOWN_HOST" > ~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
|
||||
- name: Remove PR index from droplet
|
||||
if: steps.check.outputs.has_artifact == 'true'
|
||||
env:
|
||||
SSH_USER: ${{ secrets.GITNEXUS_DO_USER }}
|
||||
SSH_HOST: ${{ secrets.GITNEXUS_DO_HOST }}
|
||||
PR_NUM: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" PR_NUM="$PR_NUM" bash <<'REMOTE'
|
||||
set -e
|
||||
TARGET="/opt/gitnexus/indexes/LibreChat-pr-$PR_NUM"
|
||||
if [ -d "$TARGET" ]; then
|
||||
echo "Removing $TARGET"
|
||||
rm -rf "$TARGET"
|
||||
cd /opt/gitnexus
|
||||
docker compose up -d --force-recreate gitnexus
|
||||
echo "GitNexus restarted without PR #$PR_NUM"
|
||||
else
|
||||
echo "No index to clean up for PR #$PR_NUM (artifact existed but droplet folder did not)"
|
||||
fi
|
||||
REMOTE
|
||||
|
||||
- name: Cleanup SSH key
|
||||
if: always()
|
||||
run: rm -f ~/.ssh/deploy_key
|
||||
583
.github/workflows/gitnexus-deploy.yml
vendored
583
.github/workflows/gitnexus-deploy.yml
vendored
|
|
@ -1,583 +0,0 @@
|
|||
# Deploys GitNexus indexes to a droplet via SSH + rsync.
|
||||
#
|
||||
# Architecture:
|
||||
# GitHub Actions (deploy)
|
||||
# 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 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.
|
||||
#
|
||||
# First-time droplet bootstrap (run once, manually):
|
||||
# 1. Create 2GB+ Ubuntu 24.04 droplet, add SSH key
|
||||
# 2. Point DNS A record for your subdomain at the droplet IP
|
||||
# 3. SSH in and run:
|
||||
# curl -fsSL https://get.docker.com | sh
|
||||
# systemctl enable --now docker
|
||||
# mkdir -p /opt/gitnexus/indexes
|
||||
# useradd -m -s /bin/bash deploy
|
||||
# usermod -aG docker deploy
|
||||
# mkdir -p /home/deploy/.ssh
|
||||
# # Add deploy pubkey to /home/deploy/.ssh/authorized_keys
|
||||
# chown -R deploy:deploy /home/deploy/.ssh /opt/gitnexus
|
||||
# chmod 700 /home/deploy/.ssh
|
||||
# ufw allow 22,80,443/tcp
|
||||
# ufw --force enable
|
||||
# 4. Copy .do/gitnexus/docker-compose.yml and Caddyfile into /opt/gitnexus/
|
||||
# 5. Create /opt/gitnexus/.env with: GITNEXUS_DOMAIN=... and API_TOKEN=...
|
||||
# 6. cd /opt/gitnexus && docker compose up -d
|
||||
#
|
||||
# Then capture the droplet's SSH host key from your workstation and
|
||||
# save it as the GITNEXUS_DO_KNOWN_HOST secret (below) so CI can pin it:
|
||||
# ssh-keyscan -H gitnexus.yourdomain.com
|
||||
#
|
||||
# GHCR image: the workflow runs `docker login ghcr.io` on the droplet
|
||||
# on every deploy using GITHUB_TOKEN, so the package can stay private.
|
||||
# If you'd rather not have CI manage droplet auth, make the package
|
||||
# public under repo Settings -> Packages.
|
||||
#
|
||||
# Required GitHub secrets:
|
||||
# GITNEXUS_DO_HOST — droplet IP or hostname
|
||||
# GITNEXUS_DO_USER — SSH user (e.g. "deploy")
|
||||
# GITNEXUS_DO_SSH_KEY — private key matching the authorized pubkey
|
||||
# GITNEXUS_DO_KNOWN_HOST — output of `ssh-keyscan -H <host>` pinning the
|
||||
# droplet's host keys (prevents MITM/TOFU risk)
|
||||
|
||||
name: GitNexus Deploy
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['GitNexus Index']
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'Optional PR number for status comments from bot-triggered dispatches'
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
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
|
||||
# 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
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
GITNEXUS_VERSION: '1.6.7'
|
||||
IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/librechat-gitnexus
|
||||
|
||||
jobs:
|
||||
# Rebuilds the long-lived image only when Dockerfile/entrypoint/extensions
|
||||
# change. Skipped on every other run, so index-only deploys are fast.
|
||||
build-image:
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
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:
|
||||
contents: read
|
||||
packages: write # push image to GHCR
|
||||
outputs:
|
||||
image_tag: ${{ steps.tag.outputs.value }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Detect image changes
|
||||
id: changes
|
||||
run: |
|
||||
# Default to rebuild when we can't cleanly diff (first commit,
|
||||
# workflow_run from a PR branch where HEAD isn't the trigger, etc).
|
||||
# Rebuild on miss > skip when we should have rebuilt.
|
||||
if git rev-parse --verify HEAD~1 >/dev/null 2>&1 && \
|
||||
git diff --quiet HEAD~1 HEAD -- .do/gitnexus/Dockerfile .do/gitnexus/entrypoint.sh .do/gitnexus/install-extensions.js; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Compute image tag
|
||||
id: tag
|
||||
run: echo "value=v${{ env.GITNEXUS_VERSION }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.changes.outputs.changed == 'true' || github.event_name == 'workflow_dispatch'
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to GHCR
|
||||
if: steps.changes.outputs.changed == 'true' || github.event_name == 'workflow_dispatch'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push image
|
||||
if: steps.changes.outputs.changed == 'true' || github.event_name == 'workflow_dispatch'
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .do/gitnexus
|
||||
file: .do/gitnexus/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}:latest
|
||||
${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.value }}
|
||||
build-args: |
|
||||
GITNEXUS_VERSION=${{ env.GITNEXUS_VERSION }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
deploy:
|
||||
needs: build-image
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: write # post deploy-complete comments on PR command dispatches
|
||||
steps:
|
||||
- name: Checkout deploy config
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
sparse-checkout: .do/gitnexus
|
||||
fetch-depth: 1
|
||||
|
||||
# Resolve every index to serve. All resolutions go through
|
||||
# listArtifactsForRepo keyed by the expected artifact name, so a
|
||||
# run's branch or event type doesn't matter — we always pick the
|
||||
# freshest artifact that actually exists.
|
||||
#
|
||||
# Why this matters: a /gitnexus index command dispatches
|
||||
# gitnexus-index.yml with ref=main and an input pr_number, which
|
||||
# produces a run whose head_branch is "main" but whose artifact
|
||||
# is gitnexus-index-pr-<N>. listWorkflowRuns(branch='main') would
|
||||
# happily return that run, and we'd then try to download a
|
||||
# nonexistent gitnexus-index-main artifact from it. Querying by
|
||||
# artifact name directly avoids the whole mess.
|
||||
- name: Resolve indexes to serve
|
||||
id: resolve
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const serve = []; // [{ name, artifactName, runId }]
|
||||
|
||||
// Helper — pick the newest non-expired artifact matching a name.
|
||||
const latestArtifact = async (artifactName) => {
|
||||
const { data } = await github.rest.actions.listArtifactsForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: artifactName,
|
||||
per_page: 10,
|
||||
});
|
||||
return data.artifacts
|
||||
.filter((a) => !a.expired)
|
||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))[0];
|
||||
};
|
||||
|
||||
// --- main and dev branches ---
|
||||
for (const [branch, name] of [
|
||||
['main', 'LibreChat'],
|
||||
['dev', 'LibreChat-dev'],
|
||||
]) {
|
||||
const artifactName = `gitnexus-index-${branch}`;
|
||||
const fresh = await latestArtifact(artifactName);
|
||||
if (!fresh) {
|
||||
core.warning(`No artifact found for ${branch} (expected ${artifactName})`);
|
||||
continue;
|
||||
}
|
||||
serve.push({
|
||||
name,
|
||||
artifactName,
|
||||
runId: fresh.workflow_run.id,
|
||||
});
|
||||
core.info(`${branch}: run ${fresh.workflow_run.id} -> ${name}`);
|
||||
}
|
||||
|
||||
core.info('PR index deploys are paused; serving main and dev only.');
|
||||
|
||||
if (!serve.length) {
|
||||
core.setFailed('No indexes to serve');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('matrix', JSON.stringify(serve));
|
||||
core.setOutput('active_names', serve.map((s) => s.name).join(','));
|
||||
|
||||
- name: Download each index artifact
|
||||
env:
|
||||
MATRIX: ${{ steps.resolve.outputs.matrix }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
mkdir -p staging
|
||||
# main/dev artifact download failures are fatal — a missing
|
||||
# main/dev index is a real deploy failure. PR artifact failures
|
||||
# are soft — a PR artifact deleted mid-deploy shouldn't abort
|
||||
# the whole deploy and take main/dev down with it.
|
||||
echo "$MATRIX" | jq -c '.[]' | while read -r entry; do
|
||||
name=$(echo "$entry" | jq -r '.name')
|
||||
artifact=$(echo "$entry" | jq -r '.artifactName')
|
||||
runId=$(echo "$entry" | jq -r '.runId')
|
||||
target="staging/${name}/.gitnexus"
|
||||
echo "Downloading $artifact from run $runId -> $target"
|
||||
mkdir -p "$target"
|
||||
if ! gh run download "$runId" \
|
||||
--repo "${{ github.repository }}" \
|
||||
--name "$artifact" \
|
||||
--dir "$target"; then
|
||||
case "$name" in
|
||||
LibreChat|LibreChat-dev)
|
||||
echo "::error::Failed to download critical artifact $artifact"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
# The name stays in active_names so the prune step
|
||||
# won't remove the droplet's existing copy. The old
|
||||
# index keeps being served instead of being wiped to
|
||||
# nothing — stale beats empty — but observability
|
||||
# requires an explicit notice since this path is
|
||||
# invisible in the happy-path deploy log.
|
||||
echo "::warning::Failed to download PR artifact $artifact — skipping fresh sync; previous index (if any) will continue being served from the droplet"
|
||||
rm -rf "staging/${name}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo "Staged for rsync:"
|
||||
du -sh staging/*/.gitnexus/ 2>/dev/null || echo "(none)"
|
||||
|
||||
- name: Setup SSH
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.GITNEXUS_DO_SSH_KEY }}
|
||||
KNOWN_HOST: ${{ secrets.GITNEXUS_DO_KNOWN_HOST }}
|
||||
run: |
|
||||
set -e
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
# Pin the droplet's SSH host key from a repository secret instead
|
||||
# of trusting whatever ssh-keyscan returns at deploy time. The
|
||||
# secret is populated from `ssh-keyscan -H <host>` at bootstrap.
|
||||
if [ -z "$KNOWN_HOST" ]; then
|
||||
echo "::error::GITNEXUS_DO_KNOWN_HOST secret is empty. Run ssh-keyscan -H <host> and paste the output as this secret."
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$KNOWN_HOST" > ~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
|
||||
- name: Authenticate droplet with GHCR
|
||||
# GHCR packages pushed by GITHUB_TOKEN start private. The droplet
|
||||
# pulls the image on every deploy, so we re-authenticate it here
|
||||
# using the same short-lived token. If the package is public, this
|
||||
# step is redundant but harmless.
|
||||
#
|
||||
# The token MUST travel through SSH stdin (not as a command arg)
|
||||
# so it's never visible in the droplet's process table via
|
||||
# /proc/<pid>/cmdline. `printf '%s'` is preferred over `echo`
|
||||
# so the exact byte sequence sent is explicit — docker login
|
||||
# tolerates a trailing newline but `printf` makes the intent
|
||||
# obvious and portable across shells.
|
||||
env:
|
||||
SSH_USER: ${{ secrets.GITNEXUS_DO_USER }}
|
||||
SSH_HOST: ${{ secrets.GITNEXUS_DO_HOST }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_ACTOR: ${{ github.actor }}
|
||||
run: |
|
||||
printf '%s' "$GH_TOKEN" | ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \
|
||||
"docker login ghcr.io -u '$GH_ACTOR' --password-stdin"
|
||||
|
||||
- name: Upload config files
|
||||
env:
|
||||
SSH_USER: ${{ secrets.GITNEXUS_DO_USER }}
|
||||
SSH_HOST: ${{ secrets.GITNEXUS_DO_HOST }}
|
||||
run: |
|
||||
rsync -az -e "ssh -i ~/.ssh/deploy_key" \
|
||||
.do/gitnexus/docker-compose.yml \
|
||||
.do/gitnexus/Caddyfile \
|
||||
"$SSH_USER@$SSH_HOST:/opt/gitnexus/"
|
||||
|
||||
- name: Prune stale indexes then sync fresh ones
|
||||
env:
|
||||
SSH_USER: ${{ secrets.GITNEXUS_DO_USER }}
|
||||
SSH_HOST: ${{ secrets.GITNEXUS_DO_HOST }}
|
||||
ACTIVE_NAMES: ${{ steps.resolve.outputs.active_names }}
|
||||
run: |
|
||||
set -e
|
||||
# ── 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 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'
|
||||
set -e
|
||||
cd /opt/gitnexus/indexes || exit 0
|
||||
shopt -s nullglob
|
||||
IFS=',' read -ra ACTIVE <<< "$ACTIVE_NAMES"
|
||||
for dir in */; do
|
||||
dir="${dir%/}"
|
||||
keep=false
|
||||
for a in "${ACTIVE[@]}"; do
|
||||
if [ "$dir" = "$a" ]; then keep=true; break; fi
|
||||
done
|
||||
if [ "$keep" = false ]; then
|
||||
echo "Removing stale index: $dir"
|
||||
rm -rf "$dir"
|
||||
fi
|
||||
done
|
||||
echo "Disk after prune:"
|
||||
df -h / | tail -1
|
||||
REMOTE
|
||||
|
||||
# ── Step 2: rsync-then-swap ─────────────────────────────
|
||||
# Upload each index to a temp directory, then atomically swap
|
||||
# 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
|
||||
# ~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")
|
||||
echo "Syncing $name (rsync-then-swap)"
|
||||
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \
|
||||
"mkdir -p /opt/gitnexus/indexes/${name}.new"
|
||||
if rsync -az -e "ssh -i ~/.ssh/deploy_key" \
|
||||
"$dir" \
|
||||
"$SSH_USER@$SSH_HOST:/opt/gitnexus/indexes/${name}.new/"; then
|
||||
# Swap: remove old, rename new into place
|
||||
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \
|
||||
"rm -rf /opt/gitnexus/indexes/$name && mv /opt/gitnexus/indexes/${name}.new /opt/gitnexus/indexes/$name"
|
||||
echo " $name swapped successfully"
|
||||
else
|
||||
# Clean up the partial temp dir
|
||||
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \
|
||||
"rm -rf /opt/gitnexus/indexes/${name}.new"
|
||||
# main/dev are critical — abort the deploy so the failure
|
||||
# is visible and the container isn't restarted with stale
|
||||
# or missing data. PR indexes are best-effort.
|
||||
case "$name" in
|
||||
LibreChat|LibreChat-dev)
|
||||
echo "::error::rsync failed for critical index $name — aborting deploy"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo "::warning::rsync failed for PR index $name — keeping previous index"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Pull image, restart gitnexus, reload Caddy, wait for healthy
|
||||
env:
|
||||
SSH_USER: ${{ secrets.GITNEXUS_DO_USER }}
|
||||
SSH_HOST: ${{ secrets.GITNEXUS_DO_HOST }}
|
||||
run: |
|
||||
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" bash <<'REMOTE'
|
||||
set -e
|
||||
cd /opt/gitnexus
|
||||
|
||||
# ── Disk cleanup ──────────────────────────────────────
|
||||
# Docker accumulates old image layers, dangling images, and
|
||||
# build cache across deploys. This droplet is only ~8.7GB
|
||||
# usable with a 700MB+ gitnexus image, so disk pressure is
|
||||
# constant. Prune everything not used by currently-running
|
||||
# containers BEFORE pulling the new image so the extract has
|
||||
# room; the post-recreate prune below reclaims the old image.
|
||||
echo "Disk before cleanup:"
|
||||
df -h / | tail -1
|
||||
# Omit --volumes: Caddy's caddy-data and caddy-config volumes
|
||||
# hold TLS certificates and ACME state. If Caddy happens to be
|
||||
# stopped when this runs (the workflow handles that case later),
|
||||
# --volumes would wipe them, forcing Let's Encrypt re-issuance
|
||||
# and risking rate-limit lockout (5 certs/domain/week).
|
||||
docker system prune -af 2>/dev/null || true
|
||||
echo "Disk after cleanup:"
|
||||
df -h / | tail -1
|
||||
|
||||
# Fail fast if disk is critically low even after prune. The
|
||||
# gitnexus image is ~700MB and shares most layers with the
|
||||
# running one, so an incremental pull needs well under 1GB.
|
||||
# 1536MB leaves headroom on this small droplet without the
|
||||
# over-conservative 2GB guard aborting on a healthy box.
|
||||
AVAIL_MB=$(df --output=avail -m / | tail -1 | tr -d ' ')
|
||||
if [ "$AVAIL_MB" -lt 1536 ]; then
|
||||
echo "::error::Disk critically low (${AVAIL_MB}MB free). Aborting deploy."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker compose pull gitnexus
|
||||
docker compose up -d --force-recreate gitnexus
|
||||
|
||||
# The previous gitnexus image is now dangling (the running
|
||||
# container was recreated onto the freshly pulled image). The
|
||||
# pre-pull prune above couldn't touch it because it was still
|
||||
# in use at that point. Reclaim it now so the old generation
|
||||
# doesn't accumulate — critical on this 10GB droplet.
|
||||
docker image prune -f 2>/dev/null || true
|
||||
|
||||
# Reload Caddy in-place so a changed Caddyfile takes effect
|
||||
# without losing TLS certs or restarting connections. If caddy
|
||||
# isn't running yet (first-time bootstrap), bring it up.
|
||||
if docker compose ps --status running caddy 2>/dev/null | grep -q caddy; then
|
||||
echo "Reloading Caddy config"
|
||||
docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile || {
|
||||
echo "Caddy reload failed — forcing restart"
|
||||
docker compose up -d --force-recreate caddy
|
||||
}
|
||||
else
|
||||
echo "Caddy not running — starting"
|
||||
docker compose up -d caddy
|
||||
fi
|
||||
|
||||
# Poll gitnexus health until ready or timeout. Docker's own
|
||||
# unhealthy detection takes up to 150s (start_period 60s +
|
||||
# retries 3 * interval 30s), so the poll ceiling must clear
|
||||
# that to avoid false negatives when gitnexus legitimately
|
||||
# takes ~2.5 min to warm up.
|
||||
# Max wait = 36 sleeps * 5s = 180s (final iteration exits
|
||||
# before its sleep on failure, so 37 iterations is the
|
||||
# correct upper bound for a true 180s ceiling).
|
||||
echo "Waiting for gitnexus to report healthy..."
|
||||
for i in $(seq 1 37); do
|
||||
STATUS=$(docker inspect --format='{{.State.Health.Status}}' gitnexus 2>/dev/null || echo unknown)
|
||||
echo "[$i/37] gitnexus health: $STATUS"
|
||||
if [ "$STATUS" = "healthy" ]; then
|
||||
echo "gitnexus is healthy"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 37 ]; then
|
||||
echo "ERROR: gitnexus failed to become healthy after 180s"
|
||||
docker compose ps
|
||||
docker compose logs --tail 80 gitnexus
|
||||
exit 1
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
docker compose ps
|
||||
echo "--- Caddy logs (last 20 lines) ---"
|
||||
docker compose logs --tail 20 caddy || true
|
||||
echo "--- GitNexus logs (last 30 lines) ---"
|
||||
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@v8
|
||||
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: |
|
||||
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 !== '') {
|
||||
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 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 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
|
||||
323
.github/workflows/gitnexus-index.yml
vendored
323
.github/workflows/gitnexus-index.yml
vendored
|
|
@ -1,323 +0,0 @@
|
|||
name: GitNexus Index
|
||||
|
||||
on:
|
||||
# PR branches are NOT auto-indexed — an embeddings run is too slow to
|
||||
# spend on every PR push. Only main/dev are indexed automatically;
|
||||
# individual PRs are indexed on demand via the /gitnexus command or a
|
||||
# manual workflow_dispatch.
|
||||
push:
|
||||
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: 'Optional PR head ref to check out; defaults to refs/pull/<pr_number>/head when pr_number is set'
|
||||
type: string
|
||||
default: ''
|
||||
deploy_after:
|
||||
description: 'Dispatch GitNexus Deploy after a successful index run'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
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.6.7'
|
||||
|
||||
jobs:
|
||||
index:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read # read changed files to decide whether embeddings are needed
|
||||
# Push + dispatch run unconditionally. The pull_request trigger is
|
||||
# disabled (see `on:` above), so this never runs automatically on a
|
||||
# PR. PRs are indexed on demand instead:
|
||||
# - /gitnexus index (PR comment command, contributor-gated)
|
||||
# - workflow_dispatch (manual dispatch from Actions UI)
|
||||
# Both arrive as workflow_dispatch. The pull_request guard is kept as
|
||||
# a safety net should the trigger ever be re-added.
|
||||
if: |
|
||||
github.event_name != 'pull_request' ||
|
||||
github.event.pull_request.user.login == 'danny-avila'
|
||||
runs-on: ubuntu-latest
|
||||
# Embedding generation dominates the budget: ~45 min worst case on
|
||||
# standard runners since the 1.6.x graph (~23k nodes) doubled vs 1.5.x.
|
||||
timeout-minutes: 60
|
||||
# Best-effort index: a tool-internal crash must not block PRs. Fail soft on
|
||||
# PR events; push/dispatch runs still fail loudly so regressions stay visible.
|
||||
continue-on-error: ${{ github.event_name == 'pull_request' }}
|
||||
steps:
|
||||
- name: Validate dispatch inputs
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
PR_NUMBER: ${{ inputs.pr_number }}
|
||||
PR_REF: ${{ inputs.pr_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
if [[ ! "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
|
||||
echo "::error::pr_number must be numeric"
|
||||
exit 1
|
||||
fi
|
||||
EXPECTED_REF="refs/pull/${PR_NUMBER}/head"
|
||||
if [ -n "$PR_REF" ] && [ "$PR_REF" != "$EXPECTED_REF" ]; then
|
||||
echo "::error::pr_ref must match ${EXPECTED_REF}"
|
||||
exit 1
|
||||
fi
|
||||
elif [ -n "$PR_REF" ]; then
|
||||
echo "::error::pr_ref requires pr_number"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Resolve GitNexus flags
|
||||
id: flags
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
ENABLE_EMBEDDINGS_INPUT: ${{ inputs.embeddings }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUM: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# 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 "$EVENT_NAME" in
|
||||
workflow_dispatch)
|
||||
[ "$ENABLE_EMBEDDINGS_INPUT" = "true" ] && ENABLE_EMBEDDINGS=true
|
||||
;;
|
||||
push)
|
||||
ENABLE_EMBEDDINGS=true
|
||||
;;
|
||||
pull_request)
|
||||
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
|
||||
echo "enable_embeddings=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "enable_embeddings=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Install GitNexus CLI
|
||||
working-directory: ${{ runner.temp }}
|
||||
env:
|
||||
NPM_CONFIG_AUDIT: false
|
||||
NPM_CONFIG_CACHE: ${{ runner.temp }}/gitnexus-npm-cache
|
||||
NPM_CONFIG_FUND: false
|
||||
NPM_CONFIG_GLOBALCONFIG: ${{ runner.temp }}/gitnexus-cli/global-npmrc
|
||||
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
|
||||
NPM_CONFIG_USERCONFIG: ${{ runner.temp }}/gitnexus-cli/.npmrc
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/gitnexus-cli" "$RUNNER_TEMP/gitnexus-npm-cache"
|
||||
: > "$RUNNER_TEMP/gitnexus-cli/global-npmrc"
|
||||
printf '%s\n' \
|
||||
'registry=https://registry.npmjs.org/' \
|
||||
'audit=false' \
|
||||
'fund=false' \
|
||||
> "$RUNNER_TEMP/gitnexus-cli/.npmrc"
|
||||
# Keep GitNexus' native DB dependency deterministic in fresh CI installs.
|
||||
npm install \
|
||||
--prefix "$RUNNER_TEMP/gitnexus-cli" \
|
||||
--no-save \
|
||||
--no-package-lock \
|
||||
"gitnexus@${{ env.GITNEXUS_VERSION }}" \
|
||||
"@ladybugdb/core@0.17.1"
|
||||
test -x "$RUNNER_TEMP/gitnexus-cli/node_modules/.bin/gitnexus"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
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 || (inputs.pr_number != '' && format('refs/pull/{0}/head', inputs.pr_number) || '') }}
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
# HuggingFace throttles anonymous model downloads from shared GHA
|
||||
# runner IPs (429s or stalled transfers). Cache the embedding model
|
||||
# across runs so warm runs never touch HF at all.
|
||||
- name: Cache HuggingFace embedding model
|
||||
if: steps.flags.outputs.enable_embeddings == 'true'
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ runner.temp }}/hf-cache
|
||||
key: hf-model-snowflake-arctic-embed-xs-v1
|
||||
|
||||
- name: Run GitNexus Analyze
|
||||
working-directory: ${{ runner.temp }}
|
||||
env:
|
||||
ENABLE_EMBEDDINGS: ${{ steps.flags.outputs.enable_embeddings }}
|
||||
FORCE: ${{ inputs.force }}
|
||||
GITNEXUS_BIN: ${{ runner.temp }}/gitnexus-cli/node_modules/.bin/gitnexus
|
||||
# Fail soft in ~2 min on stalled downloads instead of eating the
|
||||
# 25-min job budget; HF_TOKEN lifts the anonymous rate limit on
|
||||
# cold-cache runs (empty when the secret is unset — safe no-op).
|
||||
HF_DOWNLOAD_TIMEOUT_MS: '60000'
|
||||
HF_HOME: ${{ runner.temp }}/hf-cache
|
||||
HF_MAX_ATTEMPTS: '2'
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
NPM_CONFIG_AUDIT: false
|
||||
NPM_CONFIG_CACHE: ${{ runner.temp }}/gitnexus-npm-cache
|
||||
NPM_CONFIG_FUND: false
|
||||
NPM_CONFIG_GLOBALCONFIG: ${{ runner.temp }}/gitnexus-cli/global-npmrc
|
||||
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
|
||||
NPM_CONFIG_USERCONFIG: ${{ runner.temp }}/gitnexus-cli/.npmrc
|
||||
run: |
|
||||
set -euo pipefail
|
||||
FLAGS=(--skip-agents-md --verbose)
|
||||
|
||||
if [ "$ENABLE_EMBEDDINGS" = "true" ]; then
|
||||
FLAGS+=(--embeddings)
|
||||
fi
|
||||
if [ "$FORCE" = "true" ]; then
|
||||
FLAGS+=(--force)
|
||||
fi
|
||||
"$GITNEXUS_BIN" analyze "$GITHUB_WORKSPACE" "${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@v6
|
||||
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
|
||||
|
||||
post-index:
|
||||
needs: index
|
||||
if: |
|
||||
always() &&
|
||||
(inputs.pr_number != '' ||
|
||||
inputs.deploy_after)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write # dispatch gitnexus-deploy.yml when deploy_after is set
|
||||
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). 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@v8
|
||||
with:
|
||||
script: |
|
||||
core.info('deploy_after=true; dispatching gitnexus-deploy.yml manually.');
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'gitnexus-deploy.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
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.
|
||||
- name: Comment on PR — index complete
|
||||
if: inputs.pr_number != ''
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
EMBEDDINGS_INPUT: ${{ inputs.embeddings }}
|
||||
INDEX_RESULT: ${{ needs.index.result }}
|
||||
PR_NUMBER: ${{ inputs.pr_number }}
|
||||
with:
|
||||
script: |
|
||||
const indexSucceeded = process.env.INDEX_RESULT === 'success';
|
||||
const outcome = indexSucceeded ? '✅ indexed' : '❌ index failed';
|
||||
const prNum = parseInt(process.env.PR_NUMBER || '', 10);
|
||||
if (!Number.isSafeInteger(prNum)) {
|
||||
core.setFailed(`Invalid PR number: ${process.env.PR_NUMBER}`);
|
||||
return;
|
||||
}
|
||||
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
const embeddingsFlag = process.env.EMBEDDINGS_INPUT === 'true' ? 'with embeddings' : 'graph-only';
|
||||
const body = [
|
||||
`### GitNexus: ${outcome}`,
|
||||
``,
|
||||
`PR #${prNum} was indexed ${embeddingsFlag}.`,
|
||||
`[Index run](${runUrl})`,
|
||||
'',
|
||||
indexSucceeded
|
||||
? '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({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNum,
|
||||
body,
|
||||
});
|
||||
141
.github/workflows/gitnexus-pr-command.yml
vendored
141
.github/workflows/gitnexus-pr-command.yml
vendored
|
|
@ -1,141 +0,0 @@
|
|||
# Responds to `/gitnexus index` comments on pull requests.
|
||||
#
|
||||
# Gated to the same author_association roles (OWNER, MEMBER, COLLABORATOR)
|
||||
# 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/<N>/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 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
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
actions: write # needed to dispatch gitnexus-index.yml
|
||||
|
||||
concurrency:
|
||||
group: gitnexus-pr-command-${{ github.event.issue.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
dispatch:
|
||||
# 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') &&
|
||||
(github.event.comment.author_association == 'OWNER' ||
|
||||
github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'COLLABORATOR')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Parse command and resolve PR head ref
|
||||
id: parse
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
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 [fast]`);
|
||||
return;
|
||||
}
|
||||
const [, subcommand, modifier] = match;
|
||||
if (subcommand !== 'index') {
|
||||
core.setFailed(`Unknown subcommand: ${subcommand}. Only 'index' is supported.`);
|
||||
return;
|
||||
}
|
||||
// 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';
|
||||
}
|
||||
|
||||
// Use refs/pull/<N>/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;
|
||||
core.setOutput('pr_number', String(prNum));
|
||||
core.setOutput('pr_ref', `refs/pull/${prNum}/head`);
|
||||
core.setOutput('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@v8
|
||||
env:
|
||||
EMBEDDINGS: ${{ steps.parse.outputs.embeddings }}
|
||||
PR_NUMBER: ${{ steps.parse.outputs.pr_number }}
|
||||
PR_REF: ${{ steps.parse.outputs.pr_ref }}
|
||||
with:
|
||||
script: |
|
||||
const prNumber = process.env.PR_NUMBER || '';
|
||||
const prRef = process.env.PR_REF || '';
|
||||
const embeddings = process.env.EMBEDDINGS || 'false';
|
||||
if (!/^[0-9]+$/.test(prNumber)) {
|
||||
core.setFailed(`Invalid PR number: ${prNumber}`);
|
||||
return;
|
||||
}
|
||||
if (prRef !== `refs/pull/${prNumber}/head`) {
|
||||
core.setFailed(`Invalid PR ref: ${prRef}`);
|
||||
return;
|
||||
}
|
||||
if (!['true', 'false'].includes(embeddings)) {
|
||||
core.setFailed(`Invalid embeddings value: ${embeddings}`);
|
||||
return;
|
||||
}
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'gitnexus-index.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
pr_number: prNumber,
|
||||
pr_ref: prRef,
|
||||
embeddings,
|
||||
force: 'false',
|
||||
deploy_after: 'true',
|
||||
},
|
||||
});
|
||||
|
||||
- name: React to the comment
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: context.payload.comment.id,
|
||||
content: 'rocket',
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue