-
-
Notifications
You must be signed in to change notification settings - Fork 89
161 lines (145 loc) · 7.35 KB
/
Copy pathselfhost.yml
File metadata and controls
161 lines (145 loc) · 7.35 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
# 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. Pre-merge, ci.yml already validates everything except
# the docker boot itself (db:migrations:check, schema drift, the selfhost pg integration suites); 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 -- so the pre-merge boot smoke bought
# almost nothing at real cost. Revisit if a hosted deploy ever tracks main directly.
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"