-
Notifications
You must be signed in to change notification settings - Fork 0
167 lines (144 loc) · 6.21 KB
/
Copy pathci.yml
File metadata and controls
167 lines (144 loc) · 6.21 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
name: CI
on:
push:
branches: [main, develop]
# `branches` filters on the PR's BASE, not its head. Without it every pull
# request runs — including feature -> sandbox, where sandbox is the shared
# integration branch and CI is not wanted. The trade-off is deliberate:
# nothing gates a merge into sandbox, and breakage surfaces one step later at
# the sandbox -> develop PR.
pull_request:
branches: [main, develop]
workflow_dispatch:
# One run per ref. A force-push to a PR cancels the superseded run instead of
# leaving two full pipelines (including the 25-minute docker smoke) racing.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Read-only by default; no job here writes to the repo.
permissions:
contents: read
jobs:
ci:
runs-on: ubuntu-latest
# The postgres service health-check can hang; fail fast rather than sitting
# until GitHub's 6h default.
timeout-minutes: 20
services:
# apps/api's integration tests hit a real Postgres and TRUNCATE all 13
# tables between suites (apps/api/test/helpers.ts). Port 5434 matches the
# DATABASE_URL default in apps/api/src/env.ts and docker-compose.yml, so
# nothing needs a special-case URL.
postgres:
image: postgres:17-alpine
env:
POSTGRES_USER: toggleflow
POSTGRES_PASSWORD: toggleflow
POSTGRES_DB: toggleflow
ports:
- 5434:5432
options: >-
--health-cmd "pg_isready -U toggleflow -d toggleflow"
--health-interval 5s
--health-timeout 3s
--health-retries 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- name: Install dependencies
run: npm ci
# Turbo's cache is local-only, so without this every run rebuilds and
# re-tests from cold. Keyed on the lockfile plus the SHA so a run restores
# the nearest previous cache and then writes its own.
- name: Restore turbo cache
uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-
turbo-${{ runner.os }}-
- name: Lint
run: npm run lint
- name: Format check
run: npm run format:check
- name: Typecheck
run: npm run typecheck
- name: Build
run: npm run build
# One merged coverage run across every workspace instead of `npm test`.
# It runs the same 179 tests, so nothing is lost by replacing the plain
# test step — and it must come after Build because apps/edge-worker's
# suite boots dist/index.js in workerd. Thresholds live in
# vitest.config.ts and fail the step on their own.
- name: Test + coverage
run: npm run test:coverage
env:
# Set explicitly rather than relying on the zod default: every var in
# apps/api/src/env.ts has one, so a mis-wired database never throws at
# boot — it fails at the first query, much further from the cause.
DATABASE_URL: postgres://toggleflow:toggleflow@localhost:5434/toggleflow
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
retention-days: 7
if-no-files-found: warn
docker-smoke:
name: Docker prod smoke
runs-on: ubuntu-latest
# Fail fast rather than sitting until GitHub's 6h default if something hangs.
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
# ../velobits-infra owns velobits-net and does not exist in CI. api-prod
# joins it as external, so it must exist or `up` errors out. No Keycloak is
# needed here: /health sits outside the /v1/ auth hook and jose's
# createRemoteJWKSet fetches lazily — only an authenticated request would
# try to reach it.
- name: Create the external network the compose file expects
run: docker network create velobits-net || true
- name: Start the prod stack
run: docker compose --profile prod up --build -d
- name: Wait for the router
run: |
for i in $(seq 1 60); do
if curl -sf -o /dev/null http://localhost:3200/healthz; then
echo "router up after ${i} attempts"; exit 0
fi
sleep 5
done
echo "router did not become ready" >&2
exit 1
- name: Smoke — the stack through one origin
run: |
set -euo pipefail
# Dashboard: nginx serves the built SPA at the webroot.
curl -sf http://localhost:3200/ | grep -q '<div id="root"'
# SPA history fallback — react-router-dom owns /tools client-side, so a
# deep link must return the app shell rather than a 404.
curl -sf -o /dev/null http://localhost:3200/tools
# Control plane, through the /api prefix strip.
curl -sf http://localhost:3200/api/health | grep -q '"status":"ok"'
# Migrations actually ran (api-prod gates on service_completed_successfully).
docker compose --profile prod exec -T postgres \
psql -U toggleflow -d toggleflow -c '\dt' | grep -q ' tools '
# Unauthenticated /v1/* is rejected — proves the auth hook is wired.
test "$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3200/api/v1/me)" = 401
# The delivery plane is Cloudflare-only in prod, so /edge/* must 404
# rather than fall through to the SPA fallback and return HTML with a 200.
test "$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3200/edge/health)" = 404
# Exactly one of each security header — the router hides the static
# image's duplicates.
test "$(curl -sI http://localhost:3200/ | grep -ci '^x-frame-options:')" = 1
- name: Dump logs
if: always()
run: docker compose --profile prod logs --no-color | tail -200
- name: Tear down
if: always()
run: docker compose --profile prod down -v