-
Notifications
You must be signed in to change notification settings - Fork 9
135 lines (122 loc) · 7.12 KB
/
Copy pathdeploy-appliance.yml
File metadata and controls
135 lines (122 loc) · 7.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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.
#
# Runs ONLY on release tags (release-plz cuts vX.Y.Z): every main push deploying to
# production meant a ~10-minute host reindex per commit and production tracking
# unreleased code. Manual runs stay available via workflow_dispatch.
on:
push:
tags: ["v*"]
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.