refactor(index): unify the call-side path stripper onto the scanner (… #9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: deploy-appliance | |
| # Deploys the Lens web appliance (editors/appliance, #1106) to the production host over | |
| # SSH. The host's IP/hostname lives ONLY in the `appliance-prod` environment's secrets — | |
| # never in this file — and the public URL is a CNAME to the Cloudflare named tunnel, so | |
| # no DNS record ever points at the box. | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - editors/appliance/** | |
| - editors/vscode/** | |
| - crates/** | |
| - Cargo.toml | |
| - Cargo.lock | |
| workflow_dispatch: | |
| env: | |
| DEPLOY_DIR: /opt/rag-rat-appliance | |
| permissions: | |
| contents: read | |
| jobs: | |
| deploy: | |
| runs-on: ubuntu-latest | |
| # Holds DEPLOY_HOST, DEPLOY_HOST_KEY, DEPLOY_SSH_KEY (and can require reviewers). | |
| environment: appliance-prod | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - name: Build the rag-rat binary (dist profile) | |
| run: cargo build --profile dist -p rag-rat | |
| - name: Package the Lens extension (identical to the published artifact) | |
| working-directory: editors/vscode | |
| run: | | |
| npm ci | |
| npm run package | |
| npx --yes @vscode/vsce package --no-dependencies --out rag-rat-lens.vsix | |
| - name: Stage artifacts into the build context | |
| run: editors/appliance/stage-artifacts.sh | |
| - uses: webfactory/[email protected] | |
| with: | |
| ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }} | |
| - name: Pin the host key | |
| run: | | |
| mkdir -p ~/.ssh | |
| printf '%s\n' '${{ secrets.DEPLOY_HOST_KEY }}' > ~/.ssh/known_hosts | |
| chmod 600 ~/.ssh/known_hosts | |
| - name: Sync the appliance to the host | |
| run: | | |
| ssh ragrat@${{ secrets.DEPLOY_HOST }} "mkdir -p '$DEPLOY_DIR'" | |
| rsync -az --delete --exclude artifacts --exclude '.env*' editors/appliance/ \ | |
| ragrat@${{ secrets.DEPLOY_HOST }}:$DEPLOY_DIR/ | |
| rsync -az editors/appliance/artifacts/ \ | |
| ragrat@${{ secrets.DEPLOY_HOST }}:$DEPLOY_DIR/artifacts/ | |
| # The served index is commit-scoped: the clone must sit at the deployed SHA and the | |
| # DB must be reindexed against it, or commit-scoped views (callers/callees) serve | |
| # nothing while repo-scoped memories still work. `git checkout -f` keeps the clone | |
| # pristine across deploys; the host-side database path override is then re-applied | |
| # idempotently (config has no env override). Incremental `index` self-heals what | |
| # the new binary owes (schema/graph migrations run on open). | |
| - name: Pin the clone to the deployed SHA and reindex the DB | |
| timeout-minutes: 45 | |
| run: | | |
| ssh ragrat@${{ secrets.DEPLOY_HOST }} 'bash -se' <<'DEPLOY' | |
| set -e | |
| DEPLOY_DIR=/opt/rag-rat-appliance | |
| install -m 755 "$DEPLOY_DIR/artifacts/rag-rat" "$HOME/.local/bin/rag-rat" | |
| # Serialize the checkout against the live serve watcher: its event-driven | |
| # maintenance pass writes DIRECTLY to the live generation (no staging), and | |
| # `rag-rat index` only takes the repo write lock after the tree has been | |
| # replaced — so a pass woken by checkout churn could index the transient | |
| # half-switched tree into the live scope. Hold the repo's own write flock | |
| # across the checkout instead; anything the watcher writes afterwards is | |
| # superseded by the staged rebuild's atomic flip. | |
| # | |
| # The lock file is derived, not globbed: stale per-repo lock files from an | |
| # identity transition must not let us hold the WRONG one. rag-rat's lock | |
| # discriminator is the first 12 alphanumerics of the repo id, and a portable | |
| # repo id is the first-parent root commit hash. | |
| ROOT=$(git -C /srv/rag-rat/repo rev-list --max-parents=0 --first-parent HEAD | head -1) | |
| DISC=$(printf %s "$ROOT" | tr -cd '[:alnum:]' | cut -c1-12) | |
| flock -x "/srv/rag-rat/db/rag-rat-write-$DISC.lock" -c \ | |
| "git -C /srv/rag-rat/repo fetch -q origin && git -C /srv/rag-rat/repo checkout -qf ${{ github.sha }}" | |
| grep -q '^database = ' /srv/rag-rat/repo/rag-rat.toml || \ | |
| sed -i 's|^root = "\."|root = "."\ndatabase = "/srv/rag-rat/db/rag-rat.sqlite"|' \ | |
| /srv/rag-rat/repo/rag-rat.toml | |
| # Index from the HOST checkout: its rag-rat.toml resolves root + database to | |
| # the host paths. Running from $DEPLOY_DIR would load the appliance config | |
| # with the container-only /srv/workspace and /srv/db instead. | |
| # | |
| # HOT deploy, no stopped-writer window: the full rebuild is generation-staged | |
| # (readers see the complete old generation until the atomic flip, never a | |
| # half-built mix) and the repo write lock serializes the old serve's watcher | |
| # against this new binary, so indexing runs while the appliance keeps serving. | |
| # Caveat: on a SCHEMA-VERSIONED deploy the old serve may fail its schema | |
| # check and exit early — bounded (the recreate below follows within minutes), | |
| # and strictly better than a certain 10-minute outage on every deploy. | |
| cd /srv/rag-rat/repo | |
| "$HOME/.local/bin/rag-rat" index | |
| # The clone lane is a separate derived artifact: a plain index leaves the | |
| # precomputed clone graph stamped against the old source revision, and the | |
| # Clone Classes view serves stale data until it is rebuilt. | |
| "$HOME/.local/bin/rag-rat" clones --precompute | |
| DEPLOY | |
| - name: Rebuild and restart | |
| run: | | |
| ssh ragrat@${{ secrets.DEPLOY_HOST }} \ | |
| "cd '$DEPLOY_DIR' && docker compose build --pull \ | |
| && docker compose --profile tunnel run --rm init-runtime \ | |
| && docker compose --profile tunnel up -d --no-deps --force-recreate serve \ | |
| && docker compose rm -sf code-server \ | |
| && (docker volume rm rag-rat-appliance_lens_ext || true) \ | |
| && docker compose --profile tunnel up -d --no-deps --force-recreate code-server edge tunnel" | |
| # lens_ext is a named volume: it initializes from the image ONCE and then shadows | |
| # every newer extension bundle (a stale vsix kept serving long after rebuilds — | |
| # the demoFile onboarding was invisible until the volume was dropped). The rm | |
| # must follow `rm -sf code-server`: the running container holds the volume, so | |
| # removing first silently fails and the recreate reuses the stale bundle. The | |
| # parens keep `|| true` scoped to the volume removal alone: a build or compose | |
| # failure must fail the deploy, not quietly serve the previous image. | |
| # | |
| # Per-service recreate order keeps the public gap to seconds: init-runtime first, | |
| # SYNCHRONOUSLY (`run --rm` returns only after the chown completes — `up -d` | |
| # would let serve start while the one-shot init is still running, and the | |
| # recreate's `--no-deps` bypasses the depends_on guard), then the backend, then | |
| # the workbench and its forwarders. |