-
-
Notifications
You must be signed in to change notification settings - Fork 8.6k
1538 lines (1493 loc) · 74.9 KB
/
Copy pathci.yml
File metadata and controls
1538 lines (1493 loc) · 74.9 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
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
# CI must never mutate the runner's OS trust store (2026-07-05: a cert-flow
# test installed a fake PEM on a persistent self-hosted runner and broke all
# system TLS). Belt-and-suspenders with tests/_setup/isolateDataDir.ts.
OMNIROUTE_SKIP_SYSTEM_TRUST: "1"
CI_NODE_VERSION: "24"
CI_NODE_24_VERSION: "24"
CI_NODE_26_VERSION: "26"
jobs:
changes:
name: Change Classification
runs-on: ubuntu-latest
outputs:
code: ${{ steps.classify.outputs.code }}
docs: ${{ steps.classify.outputs.docs }}
i18n: ${{ steps.classify.outputs.i18n }}
workflow: ${{ steps.classify.outputs.workflow }}
testsOnly: ${{ steps.classify.outputs.testsOnly }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
# Refuse a PR that targets its own head branch before spending anything on it. #8912 has
# head == base == release/v3.8.50: no diff, can never merge, and it sits in the queue with
# a full check board attached on every push to that branch. One field comparison.
- name: Reject a PR that targets its own branch
if: github.event_name == 'pull_request'
env:
HEAD_REF: ${{ github.head_ref }}
BASE_REF: ${{ github.base_ref }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: node scripts/check/check-pr-self-target.mjs
- id: classify
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# Single source of truth: scripts/quality/classify-pr-changes.mjs
# (unit-tested). Push/dispatch always enable every lane.
if [ "$EVENT_NAME" != "pull_request" ]; then
{
echo "code=true"
echo "docs=true"
echo "i18n=true"
echo "workflow=true"
} >> "$GITHUB_OUTPUT"
exit 0
fi
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt
node scripts/quality/classify-pr-changes.mjs changed-files.txt >> "$GITHUB_OUTPUT"
lint:
name: Lint
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
# Path filter: pure docs / pure message-catalog PRs skip the code lint bag + typecheck.
# Existence reason of this job is code regression; docs/i18n have dedicated jobs.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true') }}
env:
# tsx gates below (known-symbols, route-guard-membership) import modules that
# open SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: npm run audit:deps
- name: Restore ESLint file cache
uses: actions/cache@v6
with:
path: |
.eslintcache
.eslintcache-complexity
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
restore-keys: |
eslint-${{ runner.os }}-
# Single ESLint inventory (JSON) — quality-gate reuses the artifact instead of
# a second cold full-tree pass for eslintWarnings ratchet counts.
- name: ESLint (JSON report)
run: npm run lint:json
- name: Upload ESLint results
if: always()
uses: actions/upload-artifact@v7
with:
name: eslint-results
path: .artifacts/eslint-results.json
if-no-files-found: warn
retention-days: 7
- run: npm run check:cycles
- run: npm run check:route-validation:t06
- run: npm run check:any-budget:t11
- run: npm run check:provider-consistency
- run: npm run check:model-lifecycle
- run: npm run check:provider-asset-provenance
- run: npm run check:fetch-targets
- run: npm run check:deps
- run: npm run check:file-size
- run: npm run check:error-helper
- run: npm run check:migration-numbering
- run: npm run check:public-creds
- run: npm run check:db-rules
- run: npm run check:known-symbols
- run: npm run check:route-guard-membership
- run: npm run check:test-discovery
- run: npm run check:tracked-artifacts
# (gap 30) Also lives in quality.yml's PR-only "Merge integrity" job — because the
# CHANGELOG half of that job needs a base to diff against. This half does NOT: the
# generator either reproduces the committed SKILL.md files or it does not.
#
# Keeping it PR-only left a real hole. This cycle's merge trains landed in batches with
# `--admin`, which bypasses required checks, so three SKILL.md files drifted from the route
# catalog, rode the release squash into `main`, and the next cycle's sync-back turned them
# into a base-red that blocked EVERY PR into release/v3.8.50 until #8954. Running it here
# means a push to `main` catches the drift at the source instead of the next cycle
# inheriting it.
- run: npm run check:agent-skills-sync
# WS1.7 (v3.8.49 plan): Dockerfile lint (hadolint, pinned by digest).
# failure-threshold=error keeps the 5 pre-existing warnings (DL3008/DL3003/
# DL3016 version pinning / WORKDIR) visible without blocking; any ERROR fails.
- name: hadolint (Dockerfile)
run: docker run --rm -i hadolint/hadolint@sha256:27086352fd5e1907ea2b934eb1023f217c5ae087992eb59fde121dce9c9ff21e hadolint --failure-threshold error - < Dockerfile
- run: npm run check:lockfile
- run: npm run check:licenses
# check:docs-sync is run by the docs-sync-strict job (via check:docs-all) and the
# husky pre-commit hook; the standalone copy here was redundant (ROI dedup).
- run: npm run typecheck:core
# #7033: typecheck:core's curated file allowlist does not cover
# src/app/(dashboard) TSX (and next.config.mjs sets ignoreBuildErrors:
# true, so `next build` never type-checks it either) — orphaned
# identifiers there (see #6625/#6909) were invisible to CI. This gate
# runs tsc scoped to the dashboard tree against a frozen baseline of
# pre-existing errors; only NEW errors fail it.
- run: npm run check:dashboard-typecheck
# typecheck:noimplicit:core dropped from this job (2026-07 optimize):
# it was advisory (continue-on-error) and largely subsumed by the blocking
# check:type-coverage ratchet in quality-gate. Local: npm run typecheck:noimplicit:core.
quality-gate:
name: Quality Ratchet
runs-on: ubuntu-latest
# needs lint so eslint-results artifact is available (same inventory as the
# blocking lint step). Allow lint failure so other ratchets still run.
needs: [changes, test-coverage, lint]
# Run even when test-coverage was SKIPPED/FAILED (e.g. a single flaky Coverage
# Shard breaks the shard→coverage→ratchet chain). The DETERMINISTIC ratchets
# (eslint / complexity / cognitive-complexity / duplication / codeql) do NOT need
# the coverage artifact and MUST still run so cycle drift is measured on the
# release PR — otherwise a flake silently skips the whole gate (incident: v3.8.36
# release PR #4854, where the drift cascade only surfaced post-merge in #5029).
# The coverage.* metrics degrade gracefully: the download is continue-on-error and
# the ratchet runs with --allow-missing, so absent coverage is skipped, not failed.
# Path filter: code-only — pure docs/i18n PRs have nothing for these ratchets to guard.
if: ${{ !cancelled() && !contains(github.event.pull_request.labels.*.name, 'hotfix') && (github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true' && (needs.lint.result == 'success' || needs.lint.result == 'failure'))) }}
# security-events: read lets the CodeQL ratchet read open code-scanning alerts
# via `gh api .../code-scanning/alerts`. contents: read keeps checkout working.
permissions:
contents: read
security-events: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- name: Restore ESLint file cache
uses: actions/cache@v6
with:
path: |
.eslintcache
.eslintcache-complexity
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
restore-keys: |
eslint-${{ runner.os }}-
# Coverage mergeada (coverage-summary.json) p/ o ratchet de cobertura.
# continue-on-error: o artifact pode não existir se a job test-coverage foi
# SKIPPED (shard flaky). Nesse caso collect-metrics pula coverage.* (ausente sem
# erro) e o ratchet roda com --allow-missing — as métricas determinísticas
# (eslint/complexity/cognitive) seguem BLOQUEANTES.
- uses: actions/download-artifact@v8
continue-on-error: true
with:
name: coverage-report
path: coverage/
# Prefer lint job's ESLint JSON (one inventory, two consumers).
- name: Download ESLint results
continue-on-error: true
uses: actions/download-artifact@v8
with:
name: eslint-results
path: .artifacts/
- run: npm run quality:collect
# Catraca: falha se qualquer métrica regredir vs quality-baseline.json (commitado).
# Hoje: contagem de warnings do ESLint. Fase 4 estende com cobertura (lida do
# coverage mergeado). Tamanho de arquivo e duplicação têm gates dedicados.
# --allow-missing: pula métricas do baseline ausentes do collect (coverage.* quando
# o artifact não veio) em vez de falhar — mantém os gates determinísticos ativos.
- name: Ratchet check
run: node scripts/quality/check-quality-ratchet.mjs --allow-missing --summary .artifacts/quality-ratchet.md
# Fase 6A.5: require-tighten — BLOQUEANTE (promovido de advisory no fim do ciclo
# v3.8.27). Falha quando uma métrica MELHOROU sem o baseline ter sido apertado no
# mesmo PR (força capturar ganhos permanentes). As métricas coverage.* carregam
# tightenSlack para o gap anti-flake (CI mergeado > baseline) não disparar falso-
# positivo. Verificado limpo (exit 0) no tip de release/v3.8.27 com a cobertura
# mergeada == baseline (ver a nota _require_tighten_flip_blocking em
# config/quality/quality-baseline.json).
- name: Require-tighten (blocking)
run: node scripts/quality/check-quality-ratchet.mjs --allow-missing --require-tighten
# Catraca de duplicação (jscpd@4 sobre src+open-sse). Roda neste job (paralelo)
# para não pesar no caminho crítico do lint.
- name: Duplication ratchet
run: npm run check:duplication
# Complexity + cognitive: one ESLint walk, two independent baselines (by ruleId).
- name: Complexity + cognitive ratchets
run: npm run check:complexity-ratchets
# Fase 7 INT: dead-code, type-coverage promovidos de advisory (quality-extended)
# para BLOQUEANTES aqui. cognitive-complexity is folded into the step above.
- name: Dead-code ratchet (knip)
run: npm run check:dead-code
- name: Type coverage ratchet
run: npm run check:type-coverage
- name: Compression budget ratchet (F2.4 / N4)
run: npm run check:compression-budget
# CodeQL alerts ratchet — BLOQUEANTE (promovido de advisory na v3.8.26).
# Lê metrics.codeqlAlerts.value de quality-baseline.json e sai 1 SOMENTE numa
# regressão real (alertas abertos > baseline). Falha de medição (gh/auth/api)
# é skip gracioso com exit 0 — security-events:read no job-level permissions.
- name: CodeQL alerts ratchet (blocking)
run: npm run check:codeql-ratchet
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Append summary
if: always()
run: cat .artifacts/quality-ratchet.md >> "$GITHUB_STEP_SUMMARY"
- name: Upload ratchet report
if: always()
uses: actions/upload-artifact@v7
with:
name: quality-ratchet
path: .artifacts/quality-ratchet.md
if-no-files-found: warn
# Phase 7/8 extended quality gates — MIXED (Etapa 2, v3.8.26). The job no longer
# carries a job-level continue-on-error: the three ratchet-blocking steps below
# (Secret scan / Workflow lint / Bundle size, all passing --ratchet) FAIL the job
# on a measured regression vs config/quality/quality-baseline.json. The remaining
# steps stay ADVISORY via step-level continue-on-error (scanner install,
# vuln/osv ratchet, OpenAPI/oasdiff breaking-change, circular-deps/dpdm): they
# depend on external binaries/state that may legitimately self-skip, so they must
# never block. The blocking gates themselves SKIP (exit 0) when their binary/plugin
# is absent — only a measured regression on the SAME metric the baseline froze
# blocks. The CodeQL ratchet was PROMOTED to the quality-gate job (v3.8.26).
# SonarQube needs SONAR_TOKEN/SONAR_HOST_URL secrets.
quality-extended:
name: Quality Gates (Extended)
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
# Path filter: code-only (scanners/ratchets target production surface).
if: ${{ !contains(github.event.pull_request.labels.*.name, 'hotfix') && (github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true')) }}
steps:
# fetch-depth: 0 — the OpenAPI breaking-change gate (oasdiff) reads the base
# spec via `git show <base_ref>:docs/openapi.yaml`; a shallow clone
# would lack the base ref and the gate would self-skip (base-unresolved).
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
# Dead-code, cognitive-complexity, type-coverage foram promovidos ao job
# quality-gate (bloqueante) na Fase 7 INT — não rodam aqui para evitar duplo custo.
- name: Circular deps (dpdm; advisory)
continue-on-error: true
run: npm run check:circular-deps
# BLOCKING ratchet (Etapa 2): bundleSize must not regress vs the baseline
# (gzip via @size-limit/file, installed by `npm ci`). --ratchet exits 1 on a
# measured regression; it SKIPs (exit 0) when the size-limit plugin/build is
# absent (a non-comparable measurement never blocks).
- name: Bundle size (ratchet, blocking)
run: npm run check:bundle-size -- --ratchet
# CodeQL ratchet foi PROMOVIDO a BLOQUEANTE no job quality-gate (v3.8.26) —
# não roda aqui para evitar duplo run/duplo report.
# Install the advisory security scanners so the gates below actually run
# (they self-skip when the binaries are absent). Robustness lessons baked in:
# • `go install …/gitleaks/v8@latest` produces a binary WITHOUT the version
# ldflags gitleaks needs (and often fails) — avoided.
# • `curl …api.github.com/…/releases/latest` is UNAUTHENTICATED and
# rate-limited to 60 req/hr/IP; when throttled it returns an empty body,
# so the asset URL resolves to nothing and the install silently no-ops —
# every gate then self-skips and the metric is never produced. We instead
# use `gh release download`, which is preinstalled on GitHub runners and
# authenticated via GITHUB_TOKEN (5000 req/hr) — robust under load.
# • actionlint keeps its official download script; zizmor stays on pipx.
# We `set +e` (no single failure aborts the step), ALWAYS export $GITHUB_PATH
# at the end, and print diagnostics so the next CI run proves exactly what
# installed. The job is continue-on-error too, so an install hiccup never
# blocks the build.
- name: Install advisory security scanners (gitleaks/osv/actionlint/zizmor)
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set +e
mkdir -p "$HOME/.local/bin"
# gitleaks — download latest linux x64 tarball via gh (authed), extract binary
rm -rf /tmp/gl && mkdir -p /tmp/gl
gh release download --repo gitleaks/gitleaks --pattern '*linux_x64.tar.gz' --dir /tmp/gl
tar -xzf /tmp/gl/*linux_x64.tar.gz -C "$HOME/.local/bin" gitleaks
# osv-scanner — download latest linux amd64 bare binary via gh (authed)
rm -rf /tmp/osv && mkdir -p /tmp/osv
gh release download --repo google/osv-scanner --pattern '*linux_amd64' --dir /tmp/osv
install -m 0755 /tmp/osv/*linux_amd64 "$HOME/.local/bin/osv-scanner"
# actionlint — official download script
bash <(curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) latest "$HOME/.local/bin"
# zizmor — PyPI (pipx preferred, pip --user fallback); lands in ~/.local/bin.
# PINNED on purpose. Unpinned, the runner installed whatever PyPI served that day and
# measured 1 finding MORE than the devbox on the identical commit (190 vs 189) during
# the v3.8.49 cycle — which cost a second rebaseline push per release, chasing a
# number that was never the code's. The ratchet compares counts across machines, so
# the auditor version has to be the same on both. Bump this deliberately, and
# rebaseline in the same commit: check-workflows.mjs now prints `zizmorVersion=` next
# to the count so the new number is traceable to the tool that produced it.
ZIZMOR_VERSION=1.25.2
pipx install "zizmor==$ZIZMOR_VERSION" || pip install --user "zizmor==$ZIZMOR_VERSION"
# oasdiff — download latest linux amd64 tarball via gh (authed), extract binary
rm -rf /tmp/oasd && mkdir -p /tmp/oasd
gh release download --repo oasdiff/oasdiff --pattern '*linux_amd64.tar.gz' --dir /tmp/oasd
tar -xzf /tmp/oasd/*linux_amd64.tar.gz -C "$HOME/.local/bin" oasdiff
# ALWAYS export the bin dir (even if any step above failed)
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
# diagnostics — prove what installed on the next CI run
ls -la "$HOME/.local/bin"
"$HOME/.local/bin/gitleaks" version || true
"$HOME/.local/bin/actionlint" -version || true
"$HOME/.local/bin/osv-scanner" --version || true
"$HOME/.local/bin/oasdiff" --version || true
zizmor --version || true
# BLOCKING ratchet (Etapa 2): secretFindings must not regress vs the baseline.
# --ratchet exits 1 on a measured regression; it SKIPs (exit 0) when gitleaks
# is absent (a missing binary never blocks).
- name: Secret scan (gitleaks, ratchet, blocking)
run: npm run check:secrets -- --ratchet
# BLOCKING ratchet (v3.8.27 cycle-end): vulnCount must not regress vs the
# baseline. --ratchet exits 1 on a measured regression (measured > baseline);
# it SKIPs (exit 0) when osv-scanner is absent or osv.dev is unreachable (a
# missing/failed measurement never blocks). See the CVE-variance note in
# docs/security/SUPPLY_CHAIN.md — a newly-disclosed CVE on an unchanged dep can
# red this gate; the fix is to bump the dep or re-baseline metrics.vulnCount.
- name: Vulnerability ratchet (osv-scanner, ratchet, blocking)
run: npm run check:vuln-ratchet -- --ratchet
# BLOCKING ratchet (Etapa 2): zizmorFindings must not regress vs the baseline.
# ONLY zizmor is ratcheted — actionlint findings are reported, not blocking.
# --ratchet exits 1 on a measured zizmor regression; it SKIPs (exit 0) when
# zizmor is absent (a missing binary never blocks).
- name: Workflow lint (actionlint+zizmor, ratchet, blocking)
run: npm run check:workflows -- --ratchet
# OpenAPI breaking-change detection (oasdiff). Diffs the PR's public API
# contract (docs/openapi.yaml) against the base branch's spec.
# BLOCKING ratchet (Fase 9 Onda 0): reads metrics.openapiBreaking.value and
# exits 1 ONLY on a measured regression (count > baseline). It SKIPs (exit 0)
# when oasdiff is absent or the base spec can't be resolved — a missing
# measurement never blocks. BASE_REF is read by the script from the env
# (never interpolated into a shell body) — workflow-injection-safe.
- name: OpenAPI breaking-change (oasdiff, ratchet, blocking)
env:
BASE_REF: ${{ github.base_ref }}
run: npm run check:openapi-breaking -- --ratchet
docs-sync-strict:
name: Docs Sync (Strict)
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
# Run when docs OR code change: API/route code can break doc/OpenAPI contract gates.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.code == 'true')) }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:docs-all
# Previously-orphaned contract gates (existed as files, never wired anywhere).
# All exit 0 today: cli-i18n is a hard gate, openapi-coverage is a ratchet
# (floor ~36), openapi-security-tiers is advisory (Hard Rules #15/#17).
- name: CLI i18n consistency
run: npm run check:cli-i18n
- name: OpenAPI route coverage (ratchet)
run: npm run check:openapi-coverage
- name: OpenAPI security-tier consistency (advisory)
run: npm run check:openapi-security-tiers
# One FS inventory of src/app/api for both anti-hallucination directions.
- name: API docs refs (openapi + prose → routes)
run: npm run check:api-docs-refs
- name: i18n translation drift (warn)
run: node scripts/i18n/check-translation-drift.mjs --warn
docs-lint:
name: Docs Lint (prose — advisory)
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
# Prose/markdown only — skip when the PR has no doc surface.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.docs == 'true') }}
# Advisory (warning-first): prose/markdown style must not block merges while the
# existing doc corpus is brought up to style. Promote to blocking once it converges.
continue-on-error: true
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- name: markdownlint (docs + root, advisory)
run: npx --yes markdownlint-cli2 "docs/**/*.md" "*.md" "!docs/i18n" "!docs/research" || true
- name: Vale prose lint (Microsoft style, advisory)
# Non-fatal: a Vale/reviewdog setup error must not turn this advisory job red.
continue-on-error: true
uses: errata-ai/vale-action@reviewdog
with:
files: docs
fail_on_error: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
i18n-ui-coverage:
name: i18n UI Coverage
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
# UI keys move with dashboard code OR message catalogs.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.i18n == 'true' || needs.changes.outputs.code == 'true')) }}
steps:
# fetch-depth: 0 — the value-drift gate diffs en.json against the merge base to
# find rewritten English strings. On a shallow clone the base ref is missing and
# the gate self-skips (base-unresolved), so it would never actually run.
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: node scripts/i18n/check-ui-keys-coverage.mjs --threshold=65
# #8463: a rewritten English value used to leave its 39 translations behind
# silently (googleOAuthWarning shipped wrong copy in 39 locales for months).
# Key parity above cannot see it — a stale translation counts as covered.
- name: i18n UI value drift (stale translations)
env:
BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }}
run: node scripts/i18n/check-ui-value-drift.mjs
# #8038: cheap glossary/protected-terms consistency gate —
# complements i18n-ui-coverage (key parity) and the ICU `i18n` job below
# without needing app-boot/Playwright infra. Same gating as i18n-ui-coverage.
# ko added after the #8224 ko.json mistranslation cleanup so the fixed
# terminology cannot silently regress on the next machine-translation run.
i18n-glossary-zhcn:
name: i18n Glossary (zh-CN, ko)
runs-on: ubuntu-latest
needs: changes
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.i18n == 'true' || needs.changes.outputs.code == 'true')) }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: node scripts/i18n/check-glossary-consistency.mjs --locale=zh-CN
- run: node scripts/i18n/check-glossary-consistency.mjs --locale=zh-TW
# D4 (plano mestre testes+CI): a matrix de ~40 jobs de <1min por idioma saturava sozinha
# a concorrência de jobs da conta (Free = 20 slots, compartilhados entre TODOS os repos)
# e pagava spin-up + arredondamento de billing por idioma. Um único job itera os idiomas
# (mesmo script), com grupo de log por idioma e artifact único de resultados nomeados por
# idioma (a matrix antiga subia 40 artifacts cujo result.txt colidia no merge-multiple).
i18n:
name: i18n Validation (all languages)
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
# Message-catalog / i18n-tooling only — pure code without i18n surface skips this lane.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.i18n == 'true') }}
continue-on-error: true
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- name: Validate all languages
run: |
set -uo pipefail
mkdir -p i18n-results
FAIL=0
for f in src/i18n/messages/*.json; do
lang=$(basename "$f" .json)
[ "$lang" = "en" ] && continue
echo "::group::i18n $lang"
if python3 scripts/i18n/validate_translation.py quick -l "$lang" > "i18n-results/$lang.txt" 2>&1; then
echo "OK $lang"
else
FAIL=1
echo "FAIL $lang"
cat "i18n-results/$lang.txt"
fi
echo "::endgroup::"
done
exit "$FAIL"
- name: Upload results
if: always()
uses: actions/upload-artifact@v7
with:
name: i18n-results
path: i18n-results/
pr-test-policy:
name: PR Test Policy
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
- name: Fetch base branch
run: git fetch --no-tags origin "${GITHUB_BASE_REF}"
- name: Validate source changes include tests
run: node scripts/check/check-pr-test-policy.mjs --summary-file .artifacts/pr-test-policy.md
# Anti test-masking: flag net assert removal / new assert.ok(true) in changed tests.
- name: Detect test-masking (weakened assertions)
run: npm run check:test-masking
# Evidence-in-PR-body (Hard Rule #18 mechanized): claims of "tests pass" must carry output.
- name: Require evidence in PR body
run: npm run check:pr-evidence
env:
PR_BODY: ${{ github.event.pull_request.body }}
- name: Publish PR test policy summary
if: always()
run: |
if [ -f .artifacts/pr-test-policy.md ]; then
cat .artifacts/pr-test-policy.md >> "$GITHUB_STEP_SUMMARY"
fi
build:
name: Build
# Dynamic runner: when the release captain flips the USE_VPS_RUNNER repo var to
# 'true' (scripts/vps/release-runner-up.sh does it after the self-hosted VM is
# online), the heavy jobs run on the dedicated 32-core VPS runners (label
# omni-build) instead of queueing on the 20-concurrent-job hosted pool.
# Safety: fork PRs NEVER reach the self-hosted runner — the expression falls
# back to ubuntu-latest unless the PR head repo is this repository (push /
# dispatch events are own-origin by definition). Any failure path (VM down,
# var unset/false) also falls back to ubuntu-latest.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-build"]') || 'ubuntu-latest' }}
needs: changes
# The .113 pool runs ONE next-build with room to spare and two at the edge: the
# box has 31 GB and a single next-build peaks at 14–16 GB RSS. On 2026-08-28
# 13:50Z the kernel OOM-killed main's build while a PR build ran beside it
# (five Build jobs had been queued by a burst of PRs). Two lanes: main keeps
# its own so a release is never queued behind PR traffic; PR builds serialize
# among themselves. GitHub keeps one running + one pending per group and
# CANCELS older pendings — a cancelled PR build is re-runnable; a dead main
# build costs the publish its artefact and a 40-minute rebuild that OOMs.
concurrency:
group: heavy-build-${{ github.ref == 'refs/heads/main' && 'main' || 'pr' }}
cancel-in-progress: false
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
# NOTE: the webpack `.build/next/cache` actions/cache step was removed with the
# Turbopack switch below — Turbopack does not read/write the webpack cache dir
# (its persistent FS cache is still experimental and intentionally NOT enabled),
# so restoring the old ~0.5 GB webpack cache would only waste download time.
# Rolling back to webpack = revert this commit (cache step comes back with it).
#
# Turbopack production build (Next 16, stable): benchmarked 1.9× faster than the
# webpack pass (9min0s vs 17min15s on a 32-core box; multi-core Rust vs webpack's
# single-threaded compile). Standalone output smoke-validated (server boots,
# /api/monitoring/health 200). Downstream jobs (e2e ×9, package-artifact,
# electron-package-smoke) consume this artifact, so a green run here validates
# the Turbopack artifact end-to-end.
- run: npm run build
env:
OMNIROUTE_USE_TURBOPACK: "1"
- name: Archive Next.js build for downstream jobs
# Use tar so the archive preserves paths relative to CWD (.build/next/...).
# upload-artifact path-stripping is ambiguous when exclude patterns are used;
# an explicit tar avoids the double-nesting issue (.build/next/next/...).
# Keep standalone/node_modules intact: package/electron jobs consume the
# Next-traced standalone tree and must not replace it with root node_modules.
run: |
tar -czf "$RUNNER_TEMP/e2e-build.tar.gz" \
--exclude='.build/next/cache' \
.build/next
- name: Upload Next.js build for downstream jobs
uses: actions/upload-artifact@v7
with:
name: next-build
path: ${{ runner.temp }}/e2e-build.tar.gz
retention-days: 1
package-artifact:
name: Package Artifact
runs-on: ubuntu-latest
needs: build
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- name: Download Next.js build artifact
uses: actions/download-artifact@v8
with:
name: next-build
# Workspace-relative on purpose: the matrix below includes windows-latest, whose
# default shell is pwsh, where $RUNNER_TEMP is empty (it is $env:RUNNER_TEMP) —
# #11896's first cut broke the Electron smoke on exactly that. A relative path
# works in bash and pwsh alike; hosted workspaces are ephemeral.
path: next-build-artifact
- name: Extract Next.js build artifact
run: |
tar -xzf next-build-artifact/e2e-build.tar.gz
# build:cli consumes the downloaded .build/next standalone artifact and assembles dist/;
# it only rebuilds if the downloaded standalone artifact is missing.
- run: npm run build:cli
- name: Assert dist/server.js exists
run: test -f dist/server.js || (echo "dist/server.js missing — build:cli did not assemble correctly" && exit 1)
# `build:cli` monta dist/ mas NAO grava dist/BUILD_SHA — so `build:release` faz
# isso, chamando write-build-sha.mjs. O guard de proveniencia do #10427, dentro
# de check:pack-artifact, rejeita um artefato sem SHA (e rejeita mesmo com
# OMNIROUTE_ALLOW_CANARY_BUILD=1: o que nao da para identificar nao da para
# vouchear). Sem este passo o par build+validate deste job e estruturalmente
# incompativel e falha 100% das vezes.
- name: Stamp dist/BUILD_SHA for the provenance guard (#10427)
# O SHA TEM de vir do head da PR, nao de `git rev-parse HEAD`. Este workflow
# roda em `pull_request`, entao o checkout e o MERGE COMMIT efemero que o
# GitHub cria — um commit que nao existe em branch nenhuma e portanto nunca e
# ancestral da release. O guard de proveniencia (#10427) rejeita exatamente
# isso, e com razao: um artefato carimbado com o merge commit nao pode ser
# rastreado ate codigo que passou pelos gates.
env:
OMNIROUTE_BUILD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
export OMNIROUTE_BUILD_SHA="${OMNIROUTE_BUILD_SHA:0:7}"
node scripts/build/write-build-sha.mjs
# O guard de proveniencia checa ancestralidade contra `origin/main` por padrao.
# Esse e o ref certo na PUBLICACAO (npm-publish.yml roda em main), mas em
# `pull_request` e estruturalmente impossivel: enquanto a PR esta aberta o head
# dela NUNCA e ancestral de main — e o checkout raso nem traz `origin/main` para
# o grafo local, entao a sonda responde `false` de qualquer jeito. Resultado: o
# gate falhava 100% das vezes em PR. Pre-merge o unico invariante checavel e "o
# stamp corresponde a branch sob teste", entao apontamos o ref para o head da PR.
# Usamos `refs/pull/<N>/head` e nao `head.ref` porque aquele existe no PROPRIO
# origin mesmo quando a PR vem de um fork; `head.ref` so existe no repo do autor.
- name: Resolve the provenance ref for the pack gate (#10427)
id: provenance-ref
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
if [ -n "$PR_NUMBER" ]; then
git fetch --no-tags --depth=50 origin \
"+refs/pull/$PR_NUMBER/head:refs/remotes/origin/pr-head"
echo "ref=origin/pr-head" >> "$GITHUB_OUTPUT"
else
git fetch --no-tags --depth=50 origin \
"+refs/heads/$GITHUB_REF_NAME:refs/remotes/origin/$GITHUB_REF_NAME"
echo "ref=origin/$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
fi
- run: npm run check:pack-artifact
env:
OMNIROUTE_RELEASE_REF: ${{ steps.provenance-ref.outputs.ref }}
# WS1.2 (#7065 class): pack the real tarball, install it into a clean prefix and
# BOOT it to a healthy /api/monitoring/health — the gate that structure checks
# cannot provide (3 releases shipped boot-crashing tarballs with green lists).
- name: Boot-smoke the packed tarball
run: npm run check:pack-boot
electron-package-smoke:
name: Electron Package Smoke (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 30
needs: build
# WS1.5 (v3.8.49 plan): the Electron native-module path previously executed for
# the FIRST time on the release tag — the v3.8.48 Windows bug (npx.cmd spawned
# without shell, CVE-2024-27980 behavior change) could only surface at release.
# windows-latest runs prepare:bundle (better-sqlite3 prebuild verification since
# v13 — the node-gyp rebuild is gone) per release PR; ubuntu keeps the full
# pack + headless smoke.
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
CSC_IDENTITY_AUTO_DISCOVERY: "false"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- name: Download Next.js build artifact
uses: actions/download-artifact@v8
with:
name: next-build
# Workspace-relative on purpose: the matrix below includes windows-latest, whose
# default shell is pwsh, where $RUNNER_TEMP is empty (it is $env:RUNNER_TEMP) —
# #11896's first cut broke the Electron smoke on exactly that. A relative path
# works in bash and pwsh alike; hosted workspaces are ephemeral.
path: next-build-artifact
- name: Extract Next.js build artifact
run: |
tar -xzf next-build-artifact/e2e-build.tar.gz
- name: Install Electron dependencies
working-directory: electron
run: npm install --no-audit --no-fund
- name: Pack Electron app
if: runner.os == 'Linux'
working-directory: electron
run: npm run pack
# ADVISORY while the new Windows leg matures (repo convention, dast-smoke
# precedent): its first-ever real run (2026-07-15, run 29457533565) died in
# 0.7s with the error swallowed by pwsh — bash shell captures stderr and
# continue-on-error keeps the heavy gate green while we harden it (#7336).
- name: Prepare Electron standalone (Windows prebuild verification)
if: runner.os == 'Windows'
working-directory: electron
continue-on-error: true
shell: bash
run: npm run prepare:bundle 2>&1
- name: Smoke packaged Electron app
if: runner.os == 'Linux'
env:
ELECTRON_SMOKE_TIMEOUT_MS: 60000
run: xvfb-run -a npm run electron:smoke:packaged
test-unit:
name: Unit Tests (${{ matrix.shard }}/8)
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
# self-hosted is strictly worse here and there is nothing to configure.
runs-on: ubuntu-latest
timeout-minutes: 25
# needs: changes (not build) — this job never downloads the next-build artifact;
# gating it on Build only serialized ~20min of wall-clock for nothing. Jobs that
# DO consume the artifact (e2e, package-artifact, electron-package-smoke) keep
# needs: build. The `if` mirrors Build's own skip condition so docs-only PRs
# still skip the suite.
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8]
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
# QW-d (plano mestre): fonte única — o MESMO npm script dos runs locais (adiciona o
# setupPolyfill que o comando inline omitia; o glob canônico vive só no package.json).
# D3 (plano mestre): a coverage é coletada NESTE mesmo run (c8/NODE_V8_COVERAGE propaga
# aos filhos através do npm) — elimina a matrix Coverage Shard ×8, que re-executava a
# suíte inteira só para medir o gate. Padrão usado pelo CI do próprio nodejs/node.
# Heap: os shards rodam sob instrumentacao de cobertura do V8, que retem muito
# mais memoria que a suite crua. Com o teto antigo de 4096 MB os shards passaram
# a abortar com SIGABRT (exit 134, "Ineffective mark-compacts near heap limit")
# ao redor de 4086 MB conforme o catalogo de providers cresceu no ciclo v3.8.50 —
# todos os testes passavam e o processo morria no fim, o que le como falha de
# teste sem ser. O teto vive em `test:unit:ci:shard` (package.json) e agora
# acompanha os 8192 MB ja usados pelas variantes nao-shardadas; os runners
# GitHub-hosted tem 16 GB.
- name: Unit tests (shard ${{ matrix.shard }}/8) with V8 coverage
env:
TEST_SHARD: ${{ matrix.shard }}/8
# NODE_OPTIONS (nao so o flag em test:unit:ci:shard) porque quem estoura o
# heap e o processo `c8` que embrulha a suite — ele agrega ~577 MB de JSON
# de cobertura bruta. Subir o teto so no node filho deixa o pai no default
# do V8 (~4 GB) e o OOM continua igual, em ~4083 MB. Mesmo padrao ja usado
# pelo job de merge de cobertura mais abaixo.
NODE_OPTIONS: --max-old-space-size=8192
run: |
rm -rf coverage-shard coverage-shard-report
npx c8 \
--temp-directory=coverage-shard \
--reports-dir=coverage-shard-report \
--reporter=json \
--exclude=tests/** \
--exclude=**/*.test.* \
npm run test:unit:ci:shard
- name: Upload raw shard coverage
if: always()
uses: actions/upload-artifact@v7
with:
name: coverage-shard-${{ matrix.shard }}
path: coverage-shard/*.json
if-no-files-found: error
test-bun-sqlite:
name: Bun SQLite Compatibility
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.os == 'windows-latest' }}
timeout-minutes: 10
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- name: Install Bun (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
powershell -c "iwr bun.sh/install.ps1 -useb | iex"
echo "$env:USERPROFILE\.bun\bin" | Out-File -FilePath $env:GITHUB_PATH -Append
- name: Install Bun (non-Windows)
if: runner.os != 'Windows'
run: npm install -g bun
- run: npm run test:bun:db
test-vitest:
name: Vitest (MCP / autoCombo / UI components)
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
# self-hosted is strictly worse here and there is nothing to configure.
runs-on: ubuntu-latest
timeout-minutes: 15
# needs: changes (not build) — no artifact consumed; see test-unit note.
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
# The second test runner (CLAUDE.md: "Both test runners must pass") — was never
# wired into CI until the 2026-06-09 quality audit (Fase 6A.2).
# WS5.2/5.3 (v3.8.49 plan): JUnit output feeds Trunk Flaky Tests (advisory upload
# below). node:test stays OUT of the first wave (fd1-sensitive reporter stream).
- run: npm run test:vitest -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-mcp.xml
# vitest:ui went back to 870/870 green in the v3.8.49 quality plan (WS6.1,
# PR #7127 — 69 fails triaged: matchMedia polyfill, node:test→vitest migration,
# CompareTab D22 cap). Promoted to BLOCKING per the plan's post-merge step.
- run: npm run test:vitest:ui -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-ui.xml
# Trunk Flaky Tests upload — advisory (never blocks), own-origin only (fork PRs
# have no TRUNK_TOKEN). Pinned by SHA (tag v2.1.2).
- name: Upload test results to Trunk (advisory)
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
continue-on-error: true
uses: trunk-io/analytics-uploader@385f1ccdf345b4532dc4b6c665dd432b702b8e28 # v2.1.2
with:
junit-paths: trunk-junit/**/*.xml
org-slug: omniroute
token: ${{ secrets.TRUNK_TOKEN }}
# Node 24/26 compatibility matrices moved to .github/workflows/nightly-compat.yml
# (plano mestre testes+CI, Eixo D2 — they cost ~28% of every heavy run to catch a
# failure class that rarely originates in a PR; nightly catches it within 24h and
# the release gate can still exercise them via workflow_dispatch when needed).
test-coverage:
name: Coverage
runs-on: ubuntu-latest
# 10min was sized before #7114 added the lcov reporter (Codecov/Sonar need it);
# merging 8 shard JSONs + text+json+lcov now takes ~10-12min — three consecutive
# release-tip runs died at exactly 10m as job-timeout "cancelled" (2026-07-15/16).
timeout-minutes: 20
needs: test-unit
if: ${{ !cancelled() && needs.test-unit.result == 'success' && !contains(github.event.pull_request.labels.*.name, 'hotfix') }}
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- name: Download all shard coverage
uses: actions/download-artifact@v8
with:
pattern: coverage-shard-*