-
-
Notifications
You must be signed in to change notification settings - Fork 89
213 lines (196 loc) · 10.6 KB
/
Copy pathselfhost.yml
File metadata and controls
213 lines (196 loc) · 10.6 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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# Self-host stack CI (#980/#982). Provides integration coverage the main CI can't:
# 1. Postgres integration test — needs a real PG service container
# 2. Self-host bundle build validation (build-selfhost.ts)
# 3. Docker image build + container smoke test (/health, /ready, /metrics)
# Unit tests and typecheck are NOT duplicated here — the main CI validate job covers them.
#
# Push-to-main only since 2026-07-24 (was also per-PR): at this repo's PR volume the ~5-minute
# build-boot fired ~100x/week (its path list includes migrations/**, which nearly every backend PR
# touches) and was a top runner-queue consumer. A boot-breaking merge is caught by this run minutes
# after landing, and self-host users only ever consume tagged releases (release-selfhost.yml), never
# main. Revisit if a hosted deploy ever tracks main directly.
#
# #10138 CORRECTION. This header used to claim ci.yml validated "the selfhost pg integration suites"
# pre-merge. It does not, and cannot: those suites require a real Postgres, ci.yml has no service
# container, and without PG_TEST_URL they SKIP. The only place they run is this workflow -- post-merge.
#
# That gap shipped an outage. Migration 0209 used SQLite `AUTOINCREMENT`, which Postgres cannot parse,
# so every Orb upgrading past 0208 crash-looped on boot. The "applies every migration" test here caught
# it correctly on the very first push -- and then this workflow stayed red across five consecutive runs
# while PRs kept merging, because a post-merge failure blocks nothing and pages no one.
#
# The pre-merge counterpart is test/unit/migration-dialect-portability.test.ts: it applies translateDdl
# to every migration and fails if a SQLite-only construct survives. No Postgres, no service container,
# ~11ms inside the unit run that already happens -- deliberately NOT a second PG job, because CI
# runtime is the constraint that moved this workflow post-merge in the first place. It proves parseable,
# not correct; this workflow remains the only real-Postgres coverage, and its red is worth acting on.
name: self-host
on:
push:
branches: [main]
paths:
- "src/selfhost/**"
- "src/server.ts"
- "scripts/build-selfhost.ts"
- "scripts/validate-selfhost-sourcemap.ts"
- "Dockerfile"
- "docker-compose.yml"
- "migrations/**"
- "test/unit/selfhost-*"
- "test/integration/selfhost-pg*"
- ".github/workflows/selfhost.yml"
# Least privilege — the smoke test only reads the repo; no writes, no packages.
permissions:
contents: read
concurrency:
# Push-only workflow: sha-scoped so distinct main commits never cancel each other's validation.
group: selfhost-${{ github.sha }}
# NB: keep this a literal boolean, not an expression — see ci.yml for why (startup_failure).
cancel-in-progress: true
jobs:
build-boot:
name: build + boot smoke test
runs-on: ubuntu-latest
timeout-minutes: 20
env:
# Throwaway credential for the job-scoped ephemeral Postgres SERVICE CONTAINER below — deliberately
# not a repository secret: the container exists only for this job's lifetime, is reachable only from
# the runner's localhost, and holds nothing but synthetic test rows. Single-sourced here so the
# service env and the test step can never drift.
PG_TEST_PASSWORD: devpw
services:
postgres:
image: postgres:18-alpine
env:
POSTGRES_PASSWORD: ${{ env.PG_TEST_PASSWORD }}
POSTGRES_DB: loopover
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres" --health-interval 5s --health-timeout 5s --health-retries 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version-file: .nvmrc
cache: "npm"
- name: Install deps
run: npm ci --ignore-scripts
# Same #ci-engine-build-order fix as ci.yml (see its "Build engine package" step): the Postgres
# integration test's import graph now reaches @loopover/engine transitively (#5117 moved
# local-write action specs there; src/mcp/local-write-tools.ts re-exports them). That package's
# dist/ is gitignored and only exists after this build step, so the test fails to resolve the
# package's exports without it -- this workflow never needed the engine package built before, so it
# never had this step; it does now.
# Built before the engine for the same reason the Dockerfile does: src/'s import graph reaches
# @loopover/contract, whose package exports resolve to dist/, so anything type-checking or
# bundling src/ needs it emitted first. Zod-only leaf, no workspace dependencies of its own.
- name: Build contract package
run: npm run build --workspace @loopover/contract
- name: Build engine package
run: npm run build --workspace @loopover/engine
- name: Postgres integration test (real PG)
run: PG_TEST_URL=postgres://postgres:${{ env.PG_TEST_PASSWORD }}@localhost:5432/loopover npx vitest run test/integration/selfhost-pg.test.ts test/integration/selfhost-pg-calibration.test.ts
- name: Build the self-host bundle
run: node --experimental-strip-types scripts/build-selfhost.ts
- name: Validate self-host source map
run: node scripts/validate-selfhost-sourcemap.ts
- name: Validate docker-compose.yml
run: |
docker compose config --quiet
docker compose --profile workflows --profile storage config --quiet
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- name: Build the Docker image
run: |
docker buildx build \
--cache-from type=gha \
--cache-to type=gha,mode=max \
--load \
-t loopover:selfhost-ci .
- name: Smoke-test bundled AI CLIs
run: |
docker run --rm --entrypoint sh loopover:selfhost-ci -c \
'command -v claude && claude --version && command -v codex && codex --version'
- name: Free disk before visual-review image build
run: |
docker image prune -f
docker builder prune -f --filter until=24h || true
- name: Build release target with visual review deps
run: |
docker buildx build \
--target runtime-prebuilt \
--build-context selfhost_dist=./dist \
--build-arg INSTALL_VISUAL_REVIEW=true \
--load \
-t loopover:selfhost-prebuilt-visual-ci .
docker run --rm --entrypoint node loopover:selfhost-prebuilt-visual-ci \
-e "import('puppeteer-core').then(() => console.log('puppeteer-core ok'))"
docker rmi loopover:selfhost-prebuilt-visual-ci || true
docker image prune -f
- name: Boot the container + smoke-test /health, /ready, /metrics, migrations
run: |
docker network create gt-smoke
docker run -d --name gt-redis --network gt-smoke redis:7-alpine
trap 'docker rm -f gt gt-redis >/dev/null 2>&1 || true; docker network rm gt-smoke >/dev/null 2>&1 || true' EXIT
ok=0
for _ in $(seq 1 30); do
if docker exec gt-redis redis-cli ping | grep -q PONG; then ok=1; break; fi
sleep 1
done
if [ "$ok" != "1" ]; then echo "::error::redis did not become ready"; docker logs gt-redis; exit 1; fi
docker run -d --name gt --network gt-smoke -p 8787:8787 \
-e REDIS_URL=redis://gt-redis:6379 \
-e SELFHOST_SETUP_TOKEN=selfhost-ci-setup-token \
-e PUBLIC_API_ORIGIN=https://selfhost-ci.example \
loopover:selfhost-ci
ok=0
for _ in $(seq 1 30); do
if curl -sf http://127.0.0.1:8787/health >/dev/null; then ok=1; break; fi
sleep 2
done
if [ "$ok" != "1" ]; then echo "::error::container did not become healthy"; docker logs gt; exit 1; fi
curl -sf http://127.0.0.1:8787/health | grep -q '"status":"ok"'
curl -sf http://127.0.0.1:8787/ready | grep -q '"ok":true'
curl -sf http://127.0.0.1:8787/metrics | grep -q 'loopover_uptime_seconds'
docker logs gt 2>&1 | grep -q 'selfhost_migrations_applied'
echo "self-host smoke test passed"
# #10146: a post-merge workflow going red blocks nothing and pages no one. This one caught migration 0209's
# SQLite-only AUTOINCREMENT correctly on the very first push (#10138) and then stayed red across five
# consecutive runs while PRs kept merging, because the only signal was a red check on a branch nobody was
# watching. #9951 had already solved this exact class for the publish workflows; the escalation is now a
# shared script rather than a second copy of it.
#
# Deliberately `failure()` and not `always()`: a success must never file anything. Deliberately consecutive
# -- one red run is a flake and alerting on it is how an alert gets muted.
escalate-persistent-failure:
name: escalate persistent failure
needs: build-boot
if: ${{ failure() }}
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
# The one thing this job does that build-boot cannot: file the tracking issue.
issues: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
# Needed only for `node --experimental-strip-types` below (the escalation is a .ts script), and pinned to
# the same SHA as the other 32 setup-node uses in this repo -- including build-boot above -- so this job
# adds no dependency that was not already here.
#
# A scanner flagged this as a filesystem-read risk, citing `__tests__/authutil.test.ts` in setup-node's
# own repository. That is a test fixture, not runtime behaviour, and the feature it covers -- writing an
# auth token into .npmrc -- only engages when `registry-url` is supplied. It is supplied in the five
# publish-*.yml workflows and deliberately not here, so that path is inert in this job. Reviewed and
# dismissed rather than silenced; re-check if this step ever gains `registry-url`.
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version-file: .nvmrc
- name: Escalate if this workflow has failed on consecutive runs
env:
GH_TOKEN: ${{ github.token }}
run: node --experimental-strip-types scripts/escalate-workflow-outage.ts --workflow selfhost.yml