📊 experimental: Add GitNexus CI/CD and deployment configuration (#12577)

* feat: Add GitNexus CI/CD and deployment configuration

- Introduced a Dockerfile for building the GitNexus application with necessary dependencies and configurations.
- Added a Caddyfile to set up a reverse proxy with bearer token authentication for secure access to GitNexus.
- Created an entrypoint script to validate the API token and start both GitNexus and Caddy services.
- Configured Fly.io deployment settings in fly.toml, including health checks and service parameters.
- Established GitHub Actions workflows for deploying the GitNexus index and managing deployments to Fly.io.

* fix: use npx instead of bunx for native addon compatibility

bunx skips node-gyp lifecycle scripts, so @ladybugdb/core's native
.node binary never gets compiled/downloaded. npx handles this correctly.
This commit is contained in:
Danny Avila 2026-04-08 16:17:19 -04:00 committed by GitHub
parent 96312aa4fd
commit 01a1bc1689
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 283 additions and 0 deletions

21
.fly/gitnexus/Caddyfile Normal file
View file

@ -0,0 +1,21 @@
:8080 {
# Health check — unauthenticated so Fly.io can probe it
@health path /health
handle @health {
reverse_proxy localhost:4747 {
rewrite /api/info
}
}
# All other routes require bearer token
@authed {
header Authorization "Bearer {env.API_TOKEN}"
}
handle @authed {
reverse_proxy localhost:4747
}
# Reject unauthenticated requests
respond "Unauthorized" 401
}

29
.fly/gitnexus/Dockerfile Normal file
View file

@ -0,0 +1,29 @@
FROM node:24-slim
ARG GITNEXUS_VERSION=1.5.3
# Build tools for native addons (LadybugDB, tree-sitter), cleaned up after install
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ caddy \
&& npm install -g gitnexus@${GITNEXUS_VERSION} \
&& apt-get purge -y --auto-remove python3 make g++ \
&& rm -rf /var/lib/apt/lists/* /root/.npm
WORKDIR /repo
# Copy the pre-built GitNexus index (from CI artifact)
COPY .gitnexus/ .gitnexus/
# Register the index so `gitnexus serve` can discover it
RUN gitnexus index /repo --allow-non-git
# Caddy reverse proxy: bearer token auth in front of gitnexus serve
# Token is set via FLY_API_SECRET (flyctl secrets set API_TOKEN=...)
COPY Caddyfile /etc/caddy/Caddyfile
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 8080
ENTRYPOINT ["/entrypoint.sh"]

View file

@ -0,0 +1,14 @@
#!/bin/sh
set -e
if [ -z "$API_TOKEN" ]; then
echo "ERROR: API_TOKEN secret is not set."
echo "Run: flyctl secrets set API_TOKEN=<your-token>"
exit 1
fi
# Start gitnexus serve in background
gitnexus serve --host 127.0.0.1 --port 4747 &
# Start caddy auth proxy in foreground
exec caddy run --config /etc/caddy/Caddyfile

24
.fly/gitnexus/fly.toml Normal file
View file

@ -0,0 +1,24 @@
app = 'librechat-gitnexus'
primary_region = 'iad'
[build]
dockerfile = 'Dockerfile'
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = 'stop'
auto_start_machines = true
min_machines_running = 1
[[vm]]
size = 'shared-cpu-1x'
memory = '512mb'
[checks]
[checks.health]
type = 'http'
port = 8080
path = '/health'
interval = '30s'
timeout = '5s'

104
.github/workflows/gitnexus-deploy.yml vendored Normal file
View file

@ -0,0 +1,104 @@
# Deploys the GitNexus index to Fly.io as a persistent MCP + REST server.
#
# Endpoints available after deploy:
# /api/mcp — MCP-over-HTTP (StreamableHTTP transport)
# /api/query — Search execution flows
# /api/search — Hybrid BM25 + semantic search
# /api/repos — List indexed repositories
# /api/info — Server version and status
#
# First-time setup:
# 1. flyctl apps create librechat-gitnexus
# 2. flyctl tokens create deploy -x 999999h
# 3. flyctl secrets set API_TOKEN=$(openssl rand -hex 32)
# 4. Add FLY_API_TOKEN as a GitHub repo secret
#
# All requests (except /health) require: Authorization: Bearer <API_TOKEN>
name: GitNexus Deploy
on:
workflow_run:
workflows: ['GitNexus Index']
branches: [main]
types: [completed]
workflow_dispatch:
permissions:
actions: read
concurrency:
group: gitnexus-deploy
cancel-in-progress: true
env:
GITNEXUS_VERSION: '1.5.3'
jobs:
deploy:
if: |
github.event_name == 'workflow_dispatch' ||
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout deploy config
uses: actions/checkout@v4
with:
sparse-checkout: .fly/gitnexus
fetch-depth: 1
- name: Resolve index run
id: resolve
uses: actions/github-script@v7
with:
script: |
const runId = context.payload.workflow_run?.id;
if (runId) {
core.setOutput('run_id', String(runId));
return;
}
// workflow_dispatch: find latest successful index run on main
const { data } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'gitnexus-index.yml',
branch: 'main',
status: 'success',
per_page: 1,
});
if (!data.workflow_runs.length) {
core.setFailed('No successful GitNexus Index runs found on main');
return;
}
core.setOutput('run_id', String(data.workflow_runs[0].id));
- name: Download GitNexus index
uses: actions/download-artifact@v4
with:
name: gitnexus-index-main
path: deploy/.gitnexus
run-id: ${{ steps.resolve.outputs.run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare deploy context
run: |
cp .fly/gitnexus/Dockerfile deploy/Dockerfile
cp .fly/gitnexus/Caddyfile deploy/Caddyfile
cp .fly/gitnexus/entrypoint.sh deploy/entrypoint.sh
echo "Deploy context:"
ls -la deploy/
ls -la deploy/.gitnexus/
- name: Setup Fly
uses: superfly/flyctl-actions/setup-flyctl@master
- name: Deploy to Fly.io
working-directory: deploy
run: |
flyctl deploy \
--config ../.fly/gitnexus/fly.toml \
--build-arg GITNEXUS_VERSION=${{ env.GITNEXUS_VERSION }} \
--remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

91
.github/workflows/gitnexus-index.yml vendored Normal file
View file

@ -0,0 +1,91 @@
name: GitNexus Index
on:
push:
branches: [main, dev]
paths-ignore: ['**.md', 'docs/**', 'LICENSE', '.github/**']
pull_request:
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
permissions:
contents: read
concurrency:
group: gitnexus-${{ github.ref }}
cancel-in-progress: true
env:
GITNEXUS_VERSION: '1.5.3'
jobs:
index:
# Allow push + dispatch unconditionally; filter PRs to contributors only
if: |
github.event_name != 'pull_request' ||
github.event.pull_request.author_association == 'OWNER' ||
github.event.pull_request.author_association == 'MEMBER' ||
github.event.pull_request.author_association == 'COLLABORATOR'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
- name: Cache npm store
uses: actions/cache@v4
with:
path: ~/.npm
key: gitnexus-npm-${{ runner.os }}-${{ env.GITNEXUS_VERSION }}
restore-keys: gitnexus-npm-${{ runner.os }}-
- name: Run GitNexus Analyze
run: |
FLAGS="--skip-agents-md --verbose"
if [ "${{ inputs.embeddings }}" = "true" ]; then
FLAGS="$FLAGS --embeddings"
fi
if [ "${{ inputs.force }}" = "true" ]; then
FLAGS="$FLAGS --force"
fi
npx --yes gitnexus@${{ env.GITNEXUS_VERSION }} analyze . $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@v4
with:
name: >-
gitnexus-index-${{
github.event_name == 'pull_request'
&& format('pr-{0}', github.event.pull_request.number)
|| github.ref_name
}}
path: .gitnexus/
retention-days: 30