diff --git a/.fly/gitnexus/Caddyfile b/.fly/gitnexus/Caddyfile new file mode 100644 index 0000000000..ac69e6e775 --- /dev/null +++ b/.fly/gitnexus/Caddyfile @@ -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 +} diff --git a/.fly/gitnexus/Dockerfile b/.fly/gitnexus/Dockerfile new file mode 100644 index 0000000000..cf5cd29756 --- /dev/null +++ b/.fly/gitnexus/Dockerfile @@ -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"] diff --git a/.fly/gitnexus/entrypoint.sh b/.fly/gitnexus/entrypoint.sh new file mode 100644 index 0000000000..8a127848b4 --- /dev/null +++ b/.fly/gitnexus/entrypoint.sh @@ -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=" + 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 diff --git a/.fly/gitnexus/fly.toml b/.fly/gitnexus/fly.toml new file mode 100644 index 0000000000..086a64a640 --- /dev/null +++ b/.fly/gitnexus/fly.toml @@ -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' diff --git a/.github/workflows/gitnexus-deploy.yml b/.github/workflows/gitnexus-deploy.yml new file mode 100644 index 0000000000..7ee6e23fc7 --- /dev/null +++ b/.github/workflows/gitnexus-deploy.yml @@ -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 + +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 }} diff --git a/.github/workflows/gitnexus-index.yml b/.github/workflows/gitnexus-index.yml new file mode 100644 index 0000000000..4fb425b4c2 --- /dev/null +++ b/.github/workflows/gitnexus-index.yml @@ -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