-
Notifications
You must be signed in to change notification settings - Fork 0
672 lines (593 loc) · 25.3 KB
/
Copy pathci.yml
File metadata and controls
672 lines (593 loc) · 25.3 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
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
# GitHub Actions: lint + type-check + test the gw2analytics monorepo.
#
# Split into parallel jobs for faster feedback:
# lint-python ──> test-python (needs postgres)
# lint-web ──> playwright (chromium + visual-regression)
# arq-integration (parallel, needs postgres + redis)
#
# Previously a single monolithic ``lint-and-test`` job ran all 16+
# steps sequentially (~12-15 min wall clock). The parallel split
# brings the critical path to ~6-7 min by running Python lint, web
# lint, and ARQ integration simultaneously, with heavier test/e2e
# jobs starting immediately after their respective lint gates.
#
# Uses local composite actions (.github/actions/setup-*) to keep
# setup blocks DRY.
#
# pytest-env injects the docker-compose dev credentials at session
# startup, so this workflow does not need any GitHub repository
# secrets. A ``postgres`` service runs alongside the test-python job
# so the end-to-end ``apps/api/tests/test_uploads_e2e.py`` test can
# hit a real database at ``localhost:5432``.
name: CI
on:
# v0.15.2: repo flipped from private → public (LICENSE-cleanup
# series), so the ``spending_limit`` block no longer applies and
# auto-triggers are restored. ``push`` / ``pull_request`` cover
# normal development; ``workflow_dispatch`` stays for ad-hoc
# manual runs (e.g. cache-warmup, debugging a single job).
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
# Minimum-privilege token default for the public-repo era. Without
# this, the GITHUB_TOKEN inherits the repo's default permissions
# (potentially ``write-all`` on legacy settings), which is a pivot
# risk if a malicious PR ever exfiltrates it. See GitHub docs:
# https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
# Cancel PR runs on rapid-fire pushes to save contributor minutes,
# but let ``main`` runs complete to preserve CI history for each
# landed commit (so a flaky rerun doesn't lose visibility).
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
# ---------------------------------------------------------------------------
# Python lint: ruff, mypy, and security audit.
# Uses the full workspace sync (setup-python composite action) so
# workspace members are available as editable packages for mypy.
# ---------------------------------------------------------------------------
jobs:
lint-python:
name: Lint Python
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup-python
# Cache pip-audit's OSV vulnerability database so subsequent
# runs skip the ~15s download on every invocation.
- name: Cache pip-audit OSV data
uses: actions/cache@v4
with:
path: ~/.cache/pip-audit
key: pip-audit-${{ hashFiles('uv.lock') }}
- name: Ruff check
run: uv run ruff check --output-format=concise
- name: Ruff format check
run: uv run ruff format --check
# v0.13.9 guard: block reintroduction of legacy db.query()
- name: Guard against legacy db.query()
run: |
echo "Checking for legacy db.query() calls (modern select() required)..."
! grep -rn 'db\.query(' apps/api/src/ libs/ --include='*.py'
- name: Mypy
# Split into two invocations so libs/__main__.py and
# apps/__main__.py don't collide (each invocation only
# discovers one __main__.py).
run: |
uv run mypy libs --no-incremental
uv run mypy apps/api/src --no-incremental
- name: pip-audit (Python deps, vulnerability scan)
# ponytail: suppress known pre-existing CVEs (protobuf PYSEC-2026-1805,
# setuptools PYSEC-2026-3447). These are transitive deps pinned by
# opentelemetry & OTel instrumentation compatibility constraints.
run: uv run pip-audit --strict --vulnerability-service osv --ignore-vuln PYSEC-2026-1805 --ignore-vuln PYSEC-2026-3447
- name: Lint summary
# Only on success to avoid showing ✅ when steps actually failed.
if: success()
run: |
cat <<'EOF' >> $GITHUB_STEP_SUMMARY
## 🔍 Python lint results
| Check | Status |
|-------|--------|
| Ruff check | ✅ |
| Ruff format | ✅ |
| db.query() guard | ✅ |
| Mypy | ✅ |
| pip-audit | ✅ |
EOF
# ---------------------------------------------------------------------------
# Python tests: DB-backed pytest suite.
# Depends on lint-python so lint failures abort before running 4 min of tests.
# ---------------------------------------------------------------------------
test-python:
name: Test Python
needs: lint-python
runs-on: ubuntu-latest
env:
DATABASE_URL: "postgresql+psycopg://gw2analytics:gw2analytics@localhost:5432/gw2analytics"
S3_ENDPOINT: "localhost:9000"
S3_ACCESS_KEY: "gw2analytics"
S3_SECRET_KEY: "gw2analytics-secret"
S3_BUCKET: "gw2analytics"
# NOTE: SECRETS_KEK is hardcoded to the deterministic
# test-fixture value (matches pyproject.toml
# ``[tool.pytest_env]``). Rationale:
# (a) pytest-env injects this DURING pytest runtime, but
# the alembic ``env.py`` instantiates Settings()
# which requires SECRETS_KEK -- and alembic runs
# BEFORE pytest, so the env block MUST provide it
# explicitly. pytest-env alone is insufficient.
# (b) ``${{ secrets.SECRETS_KEK }}`` would BREAK fork
# PRs (GitHub strips secrets from forks -> empty
# string -> Fernet crashes with ``ValueError: Fernet
# key must be 32 url-safe base64-encoded bytes.``).
# (c) The dummy value is already public in
# pyproject.toml (# ``base64.urlsafe_b64encode(b"a"*32)``),
# so hardcoding here leaks no additional information.
SECRETS_KEK: "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: gw2analytics
POSTGRES_PASSWORD: gw2analytics
POSTGRES_DB: gw2analytics
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U gw2analytics"
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup-python
- name: Start MinIO
# CI-infra only: neither registry source is usable anonymously any more.
# quay.io/minio/minio now refuses anonymous pull (JWT grant returns an
# empty action list -> 401), and minio/minio was deleted from Docker Hub
# on 2026-09-11. MinIO Community Edition is source-only since the
# upstream repo was archived (2026-04-25) and dl.min.io/server/ now 410s,
# but github.com/minio/minio GitHub Releases still serves the official
# binary for this exact tag. MINIO_SHA256 pins the content so the
# download stays integrity-checked independently of that endpoint.
env:
MINIO_RELEASE: "RELEASE.2025-09-07T16-13-09Z"
MINIO_SHA256: "7c5bd8512c6e966455b1d198209358b2d191c77a83ab377c4073281065fb855f"
run: |
curl -fLsS --retry 3 -o "$RUNNER_TEMP/minio" \
"https://github.com/minio/minio/releases/download/${MINIO_RELEASE}/minio.linux-amd64.${MINIO_RELEASE}"
echo "${MINIO_SHA256} $RUNNER_TEMP/minio" | sha256sum -c -
chmod +x "$RUNNER_TEMP/minio"
mkdir -p "$RUNNER_TEMP/minio-data"
MINIO_ROOT_USER=gw2analytics \
MINIO_ROOT_PASSWORD=gw2analytics-secret \
nohup "$RUNNER_TEMP/minio" server "$RUNNER_TEMP/minio-data" \
--console-address :9001 \
> "$RUNNER_TEMP/minio.log" 2>&1 &
echo "Waiting for MinIO to be ready..."
for i in $(seq 1 15); do
if curl -sf http://localhost:9000/minio/health/live > /dev/null 2>&1; then
echo "MinIO is ready!"
break
fi
if [ "$i" -eq 15 ]; then
echo "::error::MinIO did not become ready within 30s"
cat "$RUNNER_TEMP/minio.log"
exit 1
fi
sleep 2
done
- name: Run database migrations
working-directory: apps/api
run: uv run alembic upgrade head
- name: Health probe baseline
run: uv run python -m gw2analytics_api.scripts.health_gate --save-baseline /tmp/health_baseline.json
- name: Script tests
run: uv run pytest tests/scripts/ --tb=line -q
- name: Pytest
# --cov-report=xml generates coverage.xml for Codecov ingestion.
run: uv run pytest --tb=line -q --cov-report=xml
- name: Upload coverage to Codecov
# Only on main (PRs don't have the CODECOV_TOKEN secret).
# Uses codecov-action@v5 which gracefully skips upload when
# no token is available (e.g. on PRs from forks).
if: github.ref == 'refs/heads/main'
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
flags: python
fail_ci_if_error: false
- name: Health probe CI gate (regression check)
run: uv run python -m gw2analytics_api.scripts.health_gate --check-delta /tmp/health_baseline.json
- name: Cleanup /tmp/health_baseline.json
if: always()
run: rm -f /tmp/health_baseline.json
- name: Test summary
if: success()
run: |
cat <<'EOF' >> $GITHUB_STEP_SUMMARY
## 🧪 Python test results
| Step | Status |
|------|--------|
| Database migrations | ✅ |
| Health probe baseline | ✅ |
| Script tests | ✅ |
| Pytest | ✅ |
| Codecov upload | ✅ |
| Health probe gate | ✅ |
EOF
# ---------------------------------------------------------------------------
# Web lint: OpenAPI codegen, TypeScript type-check, vitest, and pnpm audit.
# Needs Python (for dump_openapi.py) + Node/pnpm.
# ---------------------------------------------------------------------------
lint-web:
name: Lint Web
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
# Python setup for the OpenAPI codegen step (dump_openapi.py)
- uses: ./.github/actions/setup-python
- name: Dump FastAPI OpenAPI spec
run: uv run python web/scripts/dump_openapi.py > /tmp/openapi.json
# Node + pnpm for the web toolchain
- uses: ./.github/actions/setup-web
- name: Regenerate web TypeScript client from OpenAPI spec
working-directory: web
run: pnpm exec openapi-typescript /tmp/openapi.json -o src/lib/api/schema.d.ts
- name: Detect API client drift
run: git diff --exit-code -- web/src/lib/api/schema.d.ts
- name: Type-check web
working-directory: web
run: pnpm exec tsc --noEmit
- name: Web unit tests (vitest)
working-directory: web
run: pnpm exec vitest run --coverage --reporter=verbose
- name: Upload vitest coverage to Codecov
if: github.ref == 'refs/heads/main'
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: web/coverage/lcov.info
flags: javascript
fail_ci_if_error: false
- name: pnpm audit (Node deps, fail on HIGH)
working-directory: web
# v0.16.3: suppress known pre-existing advisory GHSA-mh99-v99m-4gvg
# (brace-expansion, a transitive dev-dep DoS vulnerability that
# does not affect the production application). Remove the --ignore
# flag once the transitive dependency chain is updated past
# brace-expansion@5.0.7.
run: pnpm audit --audit-level=high --ignore GHSA-mh99-v99m-4gvg
- name: Cleanup /tmp/openapi.json
if: always()
run: rm -f /tmp/openapi.json
- name: Web lint summary
if: success()
run: |
cat <<'EOF' >> $GITHUB_STEP_SUMMARY
## 🌐 Web lint results
| Check | Status |
|-------|--------|
| OpenAPI codegen | ✅ |
| API client drift | ✅ |
| TypeScript type-check | ✅ |
| Vitest unit tests | ✅ |
| Vitest coverage upload | ✅ |
| pnpm audit | ✅ |
EOF
# ---------------------------------------------------------------------------
# Playwright E2E tests (chromium).
# Runs on every push/PR. Browser cache (~/.cache/ms-playwright) and
# the cache-warmup workflow mitigate setup cost.
# ---------------------------------------------------------------------------
playwright-chromium:
name: Playwright (chromium)
needs: lint-web
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: ./.github/actions/setup-web
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ hashFiles('web/pnpm-lock.yaml') }}
- name: Install Playwright chromium
if: steps.playwright-cache.outputs.cache-hit != 'true'
working-directory: web
run: pnpm exec playwright install chromium
- name: Install Playwright system dependencies
working-directory: web
run: pnpm exec playwright install-deps chromium
- name: Playwright tests (chromium)
working-directory: web
run: pnpm exec playwright test --project=chromium
- name: Upload Playwright report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report-chromium
path: web/playwright-report/
retention-days: 7
- name: Upload Playwright traces on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-traces-chromium
path: web/test-results/
retention-days: 7
- name: Playwright summary
if: success()
run: |
cat <<'EOF' >> $GITHUB_STEP_SUMMARY
## 🎭 Playwright (chromium) results
| Check | Status |
|-------|--------|
| E2E tests | ✅ |
EOF
# ---------------------------------------------------------------------------
# Playwright visual regression tests.
# PR-only because there's no baseline to compare against on main.
# ---------------------------------------------------------------------------
playwright-visual-regression:
name: Playwright (visual-regression)
needs: lint-web
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
# v0.16.x: the committed PNG baselines at docs/screenshots/
# are from a pre-v0.16.x commit era (last touched 2026-07-07
# to 2026-07-15, ~10 days before the 2026-07-24 release;
# verifiable directly via
# ``git log -1 --format=%H -- docs/screenshots/<file>`` --
# see the "Exit criterion" block below for the verbatim
# recipe). Real diffs vs the committed baselines are NOT
# regressions -- they are baseline staleness. Until a
# follow-up PR refreshes the 9 baselines via a CI-equivalent
# standalone prod build, the ``continue-on-error: true`` flag
# stops this job from blocking PR merges while still running
# the suite (and uploading the diff artifacts via the
# ``failure()`` conditionals below -- they're still useful for
# whoever refreshes the baselines next). Local devs retain
# the same test signal via
# ``pnpm exec playwright test --project=visual-regression``.
#
# Why ``continue-on-error: true`` is sufficient as a gate:
# the flag only prevents workflow FAILURE on this job's
# failure; the underlying status check still reports ``failure``.
# It is currently safe because CONTRIBUTING.md §"Branch
# protection for main" lists 9 required-status-checks (see
# that section for the current list), and ``Playwright
# (visual-regression)`` is NOT among them -- so a failed run
# here does NOT block the merge gate. WARNING: a future
# maintainer who adds this job to ``required_status_checks``
# MUST also remove ``continue-on-error: true`` (or the
# gate will silently start blocking merges). Cite
# CONTRIBUTING.md "Branch protection for main" by section
# when revisiting.
#
# Exit criterion: remove this ``continue-on-error: true`` flag
# in a follow-up commit once (a) the docs/screenshots/*.png
# baselines have been refreshed against the v0.16.x UI via a
# CI-hosted capture (NOT a developer laptop -- host-specific
# font/chromium drift will re-introduce the diff); AND (b) 5+
# consecutive green main-push runs after the refresh confirm
# the strict 1.5% threshold is no longer flaky. The flag
# exists ONLY because the committed baselines are pre-v0.16.x;
# verifying baseline provenance is via
# ``git log -1 --format=%H -- docs/screenshots/<file>``.
continue-on-error: true
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: ./.github/actions/setup-web
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ hashFiles('web/pnpm-lock.yaml') }}
- name: Install Playwright chromium
if: steps.playwright-cache.outputs.cache-hit != 'true'
working-directory: web
run: pnpm exec playwright install chromium
- name: Install Playwright system dependencies
working-directory: web
run: pnpm exec playwright install-deps chromium
- name: Playwright tests (visual-regression)
working-directory: web
run: pnpm exec playwright test --project=visual-regression
- name: Upload Playwright report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report-visual-regression
path: web/playwright-report/
retention-days: 7
- name: Upload Playwright traces on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-traces-visual-regression
path: web/test-results/
retention-days: 7
- name: Upload visual regression diffs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: visual-regression-diffs
path: web/tests/e2e/.visual-regression-output/
retention-days: 7
- name: Playwright summary
if: success()
run: |
cat <<'EOF' >> $GITHUB_STEP_SUMMARY
## 🎭 Playwright (visual-regression) results
| Check | Status |
|-------|--------|
| Visual regression tests | ✅ |
EOF
# ---------------------------------------------------------------------------
# ARQ worker integration tests (parallel, needs postgres + redis)
# ---------------------------------------------------------------------------
arq-integration:
name: ARQ worker integration
runs-on: ubuntu-latest
env:
DATABASE_URL: "postgresql+psycopg://gw2analytics:gw2analytics@localhost:5432/gw2analytics"
S3_ENDPOINT: "localhost:9000"
S3_ACCESS_KEY: "gw2analytics"
S3_SECRET_KEY: "gw2analytics-secret"
S3_BUCKET: "gw2analytics"
# See test-python env block above for the full rationale.
# pytest-env alone cannot satisfy the alembic-pre-pytest
# injection window, so the test-fixture value is hardcoded
# here too.
SECRETS_KEK: "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="
ARQ_REDIS_HOST: localhost
ARQ_REDIS_PORT: 6379
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: gw2analytics
POSTGRES_PASSWORD: gw2analytics
POSTGRES_DB: gw2analytics
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U gw2analytics"
--health-interval 5s
--health-timeout 5s
--health-retries 10
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: ./.github/actions/setup-python
- name: Start MinIO
# CI-infra only: neither registry source is usable anonymously any more.
# quay.io/minio/minio now refuses anonymous pull (JWT grant returns an
# empty action list -> 401), and minio/minio was deleted from Docker Hub
# on 2026-09-11. MinIO Community Edition is source-only since the
# upstream repo was archived (2026-04-25) and dl.min.io/server/ now 410s,
# but github.com/minio/minio GitHub Releases still serves the official
# binary for this exact tag. MINIO_SHA256 pins the content so the
# download stays integrity-checked independently of that endpoint.
env:
MINIO_RELEASE: "RELEASE.2025-09-07T16-13-09Z"
MINIO_SHA256: "7c5bd8512c6e966455b1d198209358b2d191c77a83ab377c4073281065fb855f"
run: |
curl -fLsS --retry 3 -o "$RUNNER_TEMP/minio" \
"https://github.com/minio/minio/releases/download/${MINIO_RELEASE}/minio.linux-amd64.${MINIO_RELEASE}"
echo "${MINIO_SHA256} $RUNNER_TEMP/minio" | sha256sum -c -
chmod +x "$RUNNER_TEMP/minio"
mkdir -p "$RUNNER_TEMP/minio-data"
MINIO_ROOT_USER=gw2analytics \
MINIO_ROOT_PASSWORD=gw2analytics-secret \
nohup "$RUNNER_TEMP/minio" server "$RUNNER_TEMP/minio-data" \
--console-address :9001 \
> "$RUNNER_TEMP/minio.log" 2>&1 &
echo "Waiting for MinIO to be ready..."
for i in $(seq 1 15); do
if curl -sf http://localhost:9000/minio/health/live > /dev/null 2>&1; then
echo "MinIO is ready!"
break
fi
if [ "$i" -eq 15 ]; then
echo "::error::MinIO did not become ready within 30s"
cat "$RUNNER_TEMP/minio.log"
exit 1
fi
sleep 2
done
- name: Run database migrations
working-directory: apps/api
run: uv run alembic upgrade head
- name: ARQ worker integration tests
working-directory: apps/api
run: uv run pytest tests/test_parser_worker.py tests/test_uploads_arq.py -v
- name: ARQ summary
if: success()
run: |
cat <<'EOF' >> $GITHUB_STEP_SUMMARY
## ⚡ ARQ worker integration results
| Check | Status |
|-------|--------|
| Database migrations | ✅ |
| ARQ worker tests | ✅ |
EOF
# ---------------------------------------------------------------------------
# DCO sign-off check.
# PR-only. GitHub's web-DCO toggle (separate repo setting) covers
# github.com web-editor commits natively; this inline check covers
# CLI/IDE commits via ``git commit -s`` per the DCO model
# documented in CONTRIBUTING.md.
#
# Implemented as plain bash (``git log`` + ``grep``) so there is no
# external action dependency — the previous ``crazy-max/ghaction-dco@v1``
# was removed upstream, breaking the check silently.
#
# Part of ``main``'s ``required_status_checks`` (9 required checks —
# see CONTRIBUTING.md §"Branch protection for main"). The job FAILs
# (not errors) on unsigned commits, and because it is a required gate
# that failure now blocks the merge until the commit is re-signed with
# ``git commit -s``.
# ---------------------------------------------------------------------------
dco-check:
name: DCO check
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Full history needed so ``git log <base>..<head>`` resolves.
fetch-depth: 0
persist-credentials: false
# Inline DCO check: iterates every commit in the PR range and
# verifies a ``Signed-off-by:`` trailer exists in its body.
# Avoids any third-party action dependency.
- name: Run DCO check
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set +e
bad=0
for commit in $(git log --format='%H' "${BASE_SHA}..${HEAD_SHA}"); do
if ! git log --format='%B' -1 "$commit" | grep -qi 'Signed-off-by:'; then
echo "::error::Missing Signed-off-by: trailer in commit ${commit}"
bad=$((bad+1))
fi
done
if [ $bad -gt 0 ]; then
echo "::warning::${bad} commit(s) missing Signed-off-by: trailer — DCO check FAILED"
exit 1
fi
echo "All commits have a Signed-off-by: trailer — DCO check PASSED"