-
Notifications
You must be signed in to change notification settings - Fork 9
279 lines (237 loc) · 10.2 KB
/
Copy pathci.yml
File metadata and controls
279 lines (237 loc) · 10.2 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
name: CI
on:
pull_request:
push:
branches:
- "**"
permissions:
contents: read
jobs:
commits:
name: Commit Messages (Conventional Commits)
runs-on: ubuntu-latest
steps:
- name: Checkout (full history)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install commitlint
run: npm ci --ignore-scripts
- name: Lint commits (PR)
if: ${{ github.event_name == 'pull_request' }}
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.sha }} --verbose
- name: Lint commits (push)
if: ${{ github.event_name == 'push' && github.ref != 'refs/heads/main' && github.event.before != '0000000000000000000000000000000000000000' }}
# github.event.before can be a commit no longer reachable from github.sha (e.g. after a
# rebase or force-push rewrites history), which makes the range invalid and this step
# fail even though nothing is actually wrong with the commit messages. When that happens,
# skip rather than fail: if this branch has an open PR, the pull_request-triggered lint
# job above already covers the same commits against a stable base.sha.
run: |
if git merge-base --is-ancestor ${{ github.event.before }} ${{ github.sha }} 2>/dev/null; then
npx commitlint --from ${{ github.event.before }} --to ${{ github.sha }} --verbose
else
echo "::notice::${{ github.event.before }} is not an ancestor of ${{ github.sha }} (likely a rebase/force-push) — skipping push-triggered lint for this range."
fi
ui:
name: UI Typecheck + Test + Build
runs-on: ubuntu-latest
steps:
- name: Checkout (full history for diff coverage)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install UI deps
working-directory: apps/ui
run: bun install --frozen-lockfile
- name: Typecheck
working-directory: apps/ui
run: bun run typecheck
- name: Lint UI
working-directory: apps/ui
run: bun run lint
# Runs all tests and enforces the global coverage floor (apps/ui/vitest.config.ts
# test.coverage.thresholds) — a regression guard, not an aspirational target; most
# app/** page components aren't unit-tested yet. New/changed lines are held to a much
# higher bar by the diff-coverage step below.
- name: Test UI (with coverage)
working-directory: apps/ui
run: bun run test:coverage
- name: Build UI
working-directory: apps/ui
run: bun run build
- name: Setup Python (for diff-cover)
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install diff-cover
run: python -m pip install diff-cover==9.4.1
# vitest's lcov report has SF: paths relative to apps/ui (its configured `root`), but
# diff-cover matches against `git diff` paths, which are repo-root-relative. Rewrite the
# paths to be repo-root-relative before handing off, then run diff-cover from the repo
# root so its own path resolution lines up. (Verified locally: without this rewrite,
# diff-cover silently reports "No lines with coverage information" instead of failing.)
- name: Diff coverage (new/changed UI lines must be tested)
run: |
sed -E 's#^SF:(.*)$#SF:apps/ui/\1#' apps/ui/coverage/lcov.info | tr '\134' '/' > apps/ui/coverage/lcov-root-relative.info
diff-cover apps/ui/coverage/lcov-root-relative.info --compare-branch=origin/main --fail-under=90
python:
name: Python Tests + Compile
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_DB: clevis
POSTGRES_USER: clevis
POSTGRES_PASSWORD: clevis
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U clevis"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgresql+psycopg://clevis:clevis@localhost:5432/clevis
JOB_SECRET_KEY: ${{ secrets.JOB_SECRET_KEY || 'ci-dummy-secret-key-not-for-production-use-00' }}
AUTH_SECRET: ${{ secrets.AUTH_SECRET || 'ci-dummy-auth-secret-not-for-production-use-000' }}
steps:
- name: Checkout (full history for diff coverage)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install API/Worker + test requirements
run: |
python -m pip install --upgrade pip
python -m pip install -r apps/api/requirements.txt
python -m pip install -r apps/worker/requirements.txt
python -m pip install -e packages/checks
python -m pip install -r requirements-test.txt
- name: Run migrations
working-directory: apps/api
run: python -m alembic upgrade head
- name: Check for model / migration drift
working-directory: apps/api
run: python -m alembic check
# Global floor (.coveragerc / --cov-fail-under) is a regression guard measured against
# the current baseline (~87%), not an aspirational target — it stops overall coverage
# from silently eroding. New/changed lines are held to a much higher bar by the diff
# coverage step below.
- name: Run Python tests (with coverage)
run: >
python -m pytest -q
--cov=apps/api/src --cov=apps/worker/src --cov=packages/checks/src
--cov-report=xml --cov-report=term --cov-fail-under=85
- name: Diff coverage (new/changed Python lines must be tested)
run: diff-cover coverage.xml --compare-branch=origin/main --fail-under=90
- name: Compile Python sources
run: python -m compileall apps/api/src apps/worker/src
e2e:
name: E2E Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# Dummy secrets, same pattern as the "python" job — this stack is thrown away at the
# end of the job, nothing here is a real credential.
- name: Write CI env file
run: |
cat > .env <<'EOF'
DB_USER=clevis
DB_PASSWORD=clevis
DB_NAME=clevis
JOB_SECRET_KEY=ci-e2e-secret-key-not-for-production-use-0000
AUTH_SECRET=ci-e2e-auth-secret-not-for-production-use-00000
NEXT_PUBLIC_API_BASE=http://localhost:8080
NEXT_PUBLIC_GITHUB_APP_SLUG=
EOF
# Full docker-compose stack (not direct processes) so this exercises the real images —
# the same ones verified in Docker Build Verification — not just app code in isolation.
- name: Start stack
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile backend --profile frontend up --build -d
- name: Wait for API and UI to be reachable
run: |
for i in $(seq 1 60); do
api_ok=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/healthz || echo "000")
ui_ok=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/login || echo "000")
if [ "$api_ok" = "200" ] && [ "$ui_ok" = "200" ]; then
echo "API and UI are up"
exit 0
fi
sleep 2
done
echo "Timed out waiting for API/UI"
docker compose -f docker-compose.yml -f docker-compose.ci.yml logs
exit 1
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install UI deps
working-directory: apps/ui
run: bun install --frozen-lockfile
- name: Install Playwright browser (Chromium only)
working-directory: apps/ui
run: bunx playwright install --with-deps chromium
- name: Run E2E tests
working-directory: apps/ui
env:
E2E_BASE_URL: http://localhost:3000
E2E_API_BASE: http://localhost:8080
CI: "true"
run: bun run test:e2e
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: apps/ui/playwright-report/
retention-days: 7
- name: Stack logs (on failure)
if: failure()
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml logs
- name: Tear down stack
if: always()
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml down -v
docker:
name: Docker Build Verification
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build API image
run: docker build -t clevis-api -f apps/api/Dockerfile .
- name: Build Worker image
run: docker build -t clevis-worker -f apps/worker/Dockerfile .
- name: Build UI image
run: docker build -t clevis-ui -f apps/ui/Dockerfile apps/ui
# Building an image only proves the Dockerfile's instructions succeed — it does not
# prove the app can actually import/start (e.g. a missing runtime dependency like
# packages/checks not being installed in an image). Smoke-test by importing the
# entrypoint module directly, bypassing entrypoint.sh so no live DB is needed.
- name: Smoke-test API image
run: |
docker run --rm --entrypoint python \
-e DATABASE_URL=postgresql+psycopg://smoke:smoke@localhost:5432/smoke \
-e JOB_SECRET_KEY=ci-smoke-test-key-not-for-production-use-000 \
-e AUTH_SECRET=ci-smoke-test-secret-not-for-production-use-00 \
clevis-api -c "import src.main"
- name: Smoke-test Worker image
run: |
docker run --rm --entrypoint python \
-e DATABASE_URL=postgresql://smoke:smoke@localhost:5432/smoke \
-e JOB_SECRET_KEY=ci-smoke-test-key-not-for-production-use-000 \
clevis-worker -c "import worker"