-
Notifications
You must be signed in to change notification settings - Fork 0
396 lines (356 loc) · 14.6 KB
/
Copy pathci.yml
File metadata and controls
396 lines (356 loc) · 14.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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
name: CI Pipeline
on:
workflow_dispatch: {}
push:
branches:
- main
pull_request:
branches:
- main
env:
NODE_VERSION: '24'
jobs:
# Quality checks - runs on all branches and PRs
# Self-host deploy: .github/workflows/deploy-selfhost.yml on push to main
quality:
name: Code Quality Checks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
# SSOT: lint + umlauts + typecheck + test + build are bundled in the
# `verify` npm script (package.json). CI calls it verbatim so the gating
# chain can't drift from what runs locally. Do not re-inline these checks
# here — the umlaut gate (Swiss ä/ö/ü convention) lives inside `verify`
# too. Unit tests are ALSO run by the standalone `test` job below, which
# the post-main verdict needs by name; that duplication is deliberate and
# parallel (no wall-clock cost), not an oversight.
- name: Verify (lint + umlauts + typecheck + test + build)
run: npm run verify
env:
# Build-time placeholders so strict env validation doesn't fail during CI compile.
AUTH_SECRET: ci-build-placeholder-secret-32chars
DB_HOST: localhost
DB_NAME: revampit_ci
DB_USER: ci
DB_PASSWORD: ci
# Auth smoke gate: verifies prod login when E2E secrets are present.
# Prefers dual-persona admin credentials; falls back to legacy AUTH_TEST_*.
auth-smoke:
name: Auth Smoke Test
runs-on: ubuntu-latest
# Deliberately NO `needs:`. This job tests the ALREADY-DEPLOYED production
# site (PLAYWRIGHT_BASE_URL defaults to https://revampit.orangecat.ch); it
# never builds this branch and reads nothing `quality` produced. Waiting on
# `quality` (429s) was ordering, not dependency — and after #303 unblocked
# the e2e job, this pair became the new critical path: quality 429s →
# inventory-smoke 181s = 610s of a 634s run.
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browser
run: npx playwright install --with-deps chromium
- name: Run auth smoke (if credentials configured)
env:
AUTH_TEST_EMAIL: ${{ secrets.AUTH_TEST_EMAIL }}
AUTH_TEST_PASSWORD: ${{ secrets.AUTH_TEST_PASSWORD }}
AUTH_TEST_ADMIN_EMAIL: ${{ secrets.AUTH_TEST_ADMIN_EMAIL }}
AUTH_TEST_ADMIN_PASSWORD: ${{ secrets.AUTH_TEST_ADMIN_PASSWORD }}
run: |
EMAIL="${AUTH_TEST_EMAIL:-${AUTH_TEST_ADMIN_EMAIL:-}}"
PASSWORD="${AUTH_TEST_PASSWORD:-${AUTH_TEST_ADMIN_PASSWORD:-}}"
if [ -z "$EMAIL" ] || [ -z "$PASSWORD" ]; then
echo "No AUTH_TEST_* or AUTH_TEST_ADMIN_* secrets — skipping auth smoke."
exit 0
fi
export AUTH_TEST_EMAIL="$EMAIL"
export AUTH_TEST_PASSWORD="$PASSWORD"
PLAYWRIGHT_BASE_URL="${PLAYWRIGHT_BASE_URL:-https://revampit.orangecat.ch}" \
npx playwright test tests/e2e/auth-smoke.spec.ts --project=chromium --reporter=line
# Prod health gate: full dual-persona route inventory (186 routes) when passwords set.
# Primary gate is post-deploy in deploy-selfhost.yml; this catches prod regressions on PRs too.
inventory-smoke:
name: Dual-Persona Inventory Smoke
runs-on: ubuntu-latest
# Deliberately NO `needs:` — same reason as auth-smoke above. This walks 186
# routes on the LIVE site via scripts/e2e-inventory-prod.sh; nothing about
# that verdict depends on whether this branch lints. Removing the wait moves
# 181s off the critical path without changing what is checked.
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browser
run: npx playwright install --with-deps chromium
- name: Run inventory smoke (if dual-persona secrets configured)
env:
AUTH_TEST_USER_PASSWORD: ${{ secrets.AUTH_TEST_USER_PASSWORD }}
AUTH_TEST_ADMIN_PASSWORD: ${{ secrets.AUTH_TEST_ADMIN_PASSWORD }}
AUTH_TEST_USER_EMAIL: ${{ secrets.AUTH_TEST_USER_EMAIL }}
AUTH_TEST_ADMIN_EMAIL: ${{ secrets.AUTH_TEST_ADMIN_EMAIL }}
run: bash scripts/e2e-inventory-prod.sh
# Migration drift check — applies every SQL migration in scripts/db/migrations/
# to a throwaway Postgres container. Uses pgvector image so 005-hirn-ai-rag.sql
# can CREATE EXTENSION vector. Runs on PRs and main pushes.
migrations:
name: Migration Drift Check
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main'
services:
postgres:
image: pgvector/pgvector:pg17
env:
POSTGRES_PASSWORD: ci
POSTGRES_DB: revampit_drift
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Apply migrations in order
env:
PGHOST: localhost
PGPORT: 5432
PGUSER: postgres
PGPASSWORD: ci
PGDATABASE: revampit_drift
run: bash scripts/db/apply-migrations-ci.sh
e2e-local:
name: Local E2E Journeys
runs-on: ubuntu-latest
# Deliberately NO `needs:`. This job is self-contained — it brings its own
# postgres service, applies its own migrations, seeds its own data, builds
# the app and starts its own server. It consumed no output from `quality`
# or `migrations`; both were gates expressed as dependencies, and they cost
# the full 373s of `quality` on the critical path of the second-longest job.
#
# Measured before: quality 373s → e2e 336s = 709s wall-clock.
# `needs:` is for "I use what that job produced", not "I prefer to run
# after it". Every job here remains a required check regardless of order.
#
# Trade-off, stated: a PR that fails lint now also spends the e2e minutes
# rather than being cut off early.
services:
postgres:
image: pgvector/pgvector:pg17
env:
POSTGRES_PASSWORD: ci
POSTGRES_DB: revampit_e2e
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
env:
AUTH_SECRET: ci-e2e-placeholder-secret-32chars
NEXTAUTH_URL: http://localhost:3001
AUTH_URL: http://localhost:3001
PLAYWRIGHT_BASE_URL: http://localhost:3001
DB_HOST: localhost
DB_PORT: 5432
DB_NAME: revampit_e2e
DB_USER: postgres
DB_PASSWORD: ci
DB_SSL: false
PGHOST: localhost
PGPORT: 5432
PGUSER: postgres
PGPASSWORD: ci
PGDATABASE: revampit_e2e
AUTH_TEST_EMAIL: e2e-admin@revampit.test
AUTH_TEST_PASSWORD: E2EAdmin123!
AUTH_TEST_ADMIN_EMAIL: e2e-admin@revampit.test
AUTH_TEST_ADMIN_PASSWORD: E2EAdmin123!
AUTH_TEST_SECOND_ADMIN_EMAIL: e2e-admin2@revampit.test
AUTH_TEST_SECOND_ADMIN_PASSWORD: E2EAdmin123!
AUTH_TEST_USER_EMAIL: e2e-user@revampit.test
AUTH_TEST_USER_PASSWORD: E2EUser123!
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browser
run: npx playwright install --with-deps chromium
- name: Apply migrations
run: bash scripts/db/apply-migrations-ci.sh
- name: Seed E2E data
run: npm run e2e:seed
- name: Build app
run: npm run build
- name: Start app
run: |
npm run start -- -p 3001 > revampit-e2e.log 2>&1 &
for i in {1..90}; do
if curl -fsS http://localhost:3001/api/health >/dev/null; then
exit 0
fi
sleep 2
done
cat revampit-e2e.log
exit 1
- name: Run Playwright E2E journeys
run: npm run test:e2e:journeys -- --project=chromium --reporter=line
# Specs that are not named *journey and therefore fell outside the glob
# above — so nothing ran them, anywhere, ever. Run by name, not by
# pattern, so a file cannot silently drop out of coverage again.
# security 41 assertions: no admin page renders and no admin/
# money API answers 2xx to a signed-out request
# notification-hrefs 21 assertions
# user-admin-flows 16 assertions
- name: Run Playwright E2E guards
run: npm run test:e2e:guards -- --project=chromium --reporter=line
- name: Upload E2E artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: e2e-local-artifacts
path: |
playwright-report/
test-results/
revampit-e2e.log
if-no-files-found: ignore
# Unit tests. This job ran with `continue-on-error: true` from the day it was
# added ("non-blocking while suite matures"), and the suite matured to 7,769
# tests without anyone flipping it back. The cost was not hypothetical: the
# 2026-07-28 primary-* -> success-* token sweep left 25 assertions failing in
# 15 suites, and every PR since reported this job green. A gate whose result
# is discarded is not a gate — it is a 6-minute no-op that buys confidence it
# has not earned.
test:
name: Run Tests
runs-on: ubuntu-latest
timeout-minutes: 20
if: github.event_name == 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test
# Dependency advisories. Blocks a PR that INTRODUCES a critical; high and
# below are reported but do not fail, because a third party publishing an
# advisory must not be able to jam the merge train (auto-merge refuses a red
# base). Advisories that appear with no code change are caught by the
# scheduled security-audit.yml instead — this job only sees what a run touches.
security:
name: Dependency Security Audit
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Audit runtime dependencies
env:
FAIL_ON: critical
run: bash scripts/ci/dependency-audit.sh
# Red-main alarm for the DISPATCHED path.
#
# Every merge here is an auto-merge made with GITHUB_TOKEN, which fires no
# push event — so the sweep re-arms this workflow by dispatch, and a
# dispatched run emits no `workflow_run` event for main-red-alert.yml to
# catch. That workflow is therefore silent on almost every main CI run this
# repo produces (observed 2026-08-06: main went red on a66baa55 and no issue
# was filed). A dispatched run does the handoff itself; the push path keeps
# using main-red-alert.yml. One trigger per path, one shared policy script.
post-main:
name: Main Red Alert (dispatched)
# MUST list every other job in this file. The alarm's verdict has to mean
# the same thing as the merge gate's, and the gate reads the RUN's
# conclusion — which every job contributes to. Enforced by
# src/__tests__/ci/main-red-verdict.test.ts, because this list has already
# drifted once (see the commit that added this comment).
needs: [quality, auth-smoke, inventory-smoke, migrations, e2e-local, test, security]
if: always() && github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
issues: write
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Resolve this run's verdict
id: verdict
env:
R_QUALITY: ${{ needs.quality.result }}
R_AUTH: ${{ needs.auth-smoke.result }}
R_INVENTORY: ${{ needs.inventory-smoke.result }}
R_MIGRATIONS: ${{ needs.migrations.result }}
R_E2E: ${{ needs.e2e-local.result }}
R_TEST: ${{ needs.test.result }}
R_SECURITY: ${{ needs.security.result }}
run: |
set -euo pipefail
# The question this answers is NOT "did the code break?" but "is main
# blocking the merge queue?" — because that is what the auto-merge
# green-base guard asks, and the two must agree.
#
# So anything that is not success-or-skipped counts as red, including
# `cancelled`. A cancelled job on main blocks every open PR exactly as
# hard as a failing one; treating it as a non-event is what let main
# sit red and SILENT for ~14h on 2026-08-07 while 11 PRs waited.
# (`skipped` stays green: jobs here are conditional on event type.)
results="$R_QUALITY $R_AUTH $R_INVENTORY $R_MIGRATIONS $R_E2E $R_TEST $R_SECURITY"
conclusion=success
for result in $results; do
case "$result" in
success|skipped) ;;
*) conclusion=failure ;;
esac
done
echo "conclusion=$conclusion" >> "$GITHUB_OUTPUT"
echo "verdict: $conclusion (from: $results)"
- name: File or resolve the main-red issue
env:
CONCLUSION: ${{ steps.verdict.outputs.conclusion }}
RUN_SHA: ${{ github.sha }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: bash scripts/ci/main-red-alert.sh