-
Notifications
You must be signed in to change notification settings - Fork 5
643 lines (617 loc) · 35.5 KB
/
Copy pathci.yml
File metadata and controls
643 lines (617 loc) · 35.5 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
name: CI
on:
push:
branches: [main, dev, qa]
pull_request:
# Without this, every push queues a fully independent run that competes for the same runners
# instead of cancelling the one it superseded -- qa-gate.yml already has this; ci.yml never did,
# so a burst of rapid pushes (e.g. iterating on a CI fix) pays for N full runs instead of 1.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings"
jobs:
# LAYOUT GATE — its OWN job, deliberately. It used to be the first step of `check`, ahead of
# clippy/build/test, which made it a MASK: any layout violation aborted the job before a single
# test ran, and the `Test` step is the only place BUSBAR_TEST_POSTGRES_URL / VALKEY_URL are set
# against the service containers, so the two 1.5.0 store backends' live-DB coverage — the coverage
# the comment on `services:` calls out as load-bearing, hardened to HARD-FAIL rather than skip —
# silently stopped executing on every push and PR. A layout debt and a broken test are independent
# facts about a commit; ordering them in one job means the second is unobservable until the first
# is paid off. Both jobs are required checks, and neither can now hide the other.
structure-lint:
name: structure lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# The lint's `#[cfg(test)]` scope scanner decides which lines are EXEMPT from every
# choke-point bypass rule, so a scanner that can be lied to reports "no bypass" while the
# bypass sits in production. Its self-test runs FIRST: never trust the lint's verdict before
# proving the lint still works.
- name: Structure lint self-test
run: scripts/structure-lint.sh --selftest
- name: Structure lint
run: scripts/structure-lint.sh
# RELEASE-SCRIPT lint — durable guard against the 1.5.2 gate's 2h31m hang: a backgrounded
# server (serve_forever) whose stdout was NOT redirected, captured via `$(...)`, held the
# substitution's pipe open until the job timeout. Self-test runs FIRST (never trust the lint's
# verdict before proving the scanner still catches the real antipattern), then it scans
# scripts/release-check*.sh and verifies the 1.5.2 watchdog is intact.
- name: Release-script lint self-test
run: scripts/release-script-lint.sh --selftest
- name: Release-script lint
run: scripts/release-script-lint.sh
# RESPONSE-HEADER lint: every busbar-INJECTED response header
# (`Server-Timing: busbar;dur=`, `x-busbar-route-policy`/`-target`) must be emitted from its ONE
# sanctioned, config-gated site — never a hand-rolled second emission that bypasses the
# `advanced.response_headers` opt-in. Self-test runs FIRST (never trust the lint's verdict before
# proving the scanner still catches a real bypass), then it scans crates/busbar/src.
- name: Response-header lint self-test
run: scripts/response-header-lint.sh --selftest
- name: Response-header lint
run: scripts/response-header-lint.sh
# TRACING-SEAM lint: every `#[tracing::instrument]` must carry an explicit
# `level =` so a hot-path span can never again silently default to INFO (always-on). Self-test
# runs FIRST (never trust the lint's verdict before proving the scanner still catches a real
# bypass), then it scans crates/**/*.rs.
- name: Tracing lint self-test
run: scripts/tracing-lint.sh --selftest
- name: Tracing lint
run: scripts/tracing-lint.sh
# SETTINGS-LEAK lint: an admin-facing projection may serve an opaque `settings:` bag's KEY
# NAMES (`settings_keys` / `service::redact_settings_bags`) but NEVER its values — that bag is
# where an operator's credentials legitimately live and the reads are READ-ONLY scope. Added
# after the SAME defect was found in FOUR independent projections (see the script header).
# Self-test runs FIRST (never trust the lint's verdict before proving the scanner still catches
# a real leak), then it scans crates/busbar/src/admin.
- name: Settings-leak lint self-test
run: scripts/settings-leak-lint.sh --selftest
- name: Settings-leak lint
run: scripts/settings-leak-lint.sh
# BLOCKING-FFI lint (1.5.3): a synchronous call into a dlopened PLUGIN (`transport_call`, and
# the `dlopen` + constructor before it) must never run inline in an `async fn` — one such call
# parks a Tokio worker for the plugin's full network timeout, and N concurrent callers stop the
# runtime polling anything, `/healthz` included. Added after the SAME defect was found in five
# independent places, the last of them on the ANONYMOUSLY-reachable `/auth/token` (see the
# script header). Self-test runs FIRST (never trust the lint's verdict before proving the
# scanner still catches a real inline call), then it scans crates/busbar/src.
- name: Blocking-FFI lint self-test
run: scripts/blocking-ffi-lint.sh --selftest
- name: Blocking-FFI lint
run: scripts/blocking-ffi-lint.sh
# THE REGISTRY GATE (plugins.yaml is the single source of truth for first-party plugins):
# red when a registered plugin lacks coverage anywhere — no qa-gate checkout, no
# release-check phase, no published release (or a phantom release with zero assets) — or
# when a plugin-shaped org repo exists unregistered. Found necessary during the 1.5.0 ship:
# the plugin list was duplicated across 5+ places and the "full plugin gate" silently ran
# 6 of 8. Adding a plugin = one plugins.yaml entry; this gate stays red until every
# consumer actually covers it.
- name: Plugin registry gate
env:
GH_TOKEN: ${{ github.token }}
run: scripts/plugin-registry-check.sh
# QA-GATE SEGMENTATION self-test. qa/segments.toml is the umbrella's single
# source of truth (registry-driven fan-out, mirroring plugins.yaml). This self-test proves the
# manifest's SHAPE before any segment is trusted to run: it lists both active and reserved
# entries, the preserved core-data-plane/plugins coverage is present (union ⊇ today's gate),
# every reserved slot is defined-but-inert (PASS/SKIP), and every segment names a run command.
# Same "prove the gate before you trust its verdict" discipline as the lints above.
- name: qa-gate segmentation self-test
run: scripts/qa-segments.sh --selftest
check:
name: fmt · clippy · build · test
runs-on: ubuntu-latest
# Live-DB service containers for the store-postgres / store-valkey roundtrip tests. Without these
# the roundtrip tests skip (their URLs are unset) and the two 1.5.0 store backends ship with ZERO
# CI coverage of the delete_key cascade + credential cleanup. The tests read BUSBAR_TEST_POSTGRES_URL
# / VALKEY_URL (set on the Test step below) and, because `CI` is set in Actions, HARD-FAIL rather
# than silently skip if a service is misconfigured - so this coverage cannot vanish unnoticed.
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: busbar
POSTGRES_PASSWORD: busbar
POSTGRES_DB: busbar_test
ports:
- 5432:5432
# Wait until the DB accepts connections before the job's steps run.
options: >-
--health-cmd "pg_isready -U busbar"
--health-interval 10s
--health-timeout 5s
--health-retries 5
valkey:
image: valkey/valkey:8
ports:
- 6379:6379
options: >-
--health-cmd "valkey-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
# NOTE: there used to be a `vault:` service here for busbar-secret-vault /
# busbar-secret-vault-plugin's live-Vault tests. That coverage moved with the crates to
# GetBusbar/hashicorp-vault (its own ci.yml boots the same real hashicorp/vault dev-mode
# container) — see docs/plugins.md and scripts/release-check.sh's Vault phase for how the
# monorepo now gates on that repo's own test suite via a sibling checkout instead.
steps:
- uses: actions/checkout@v7
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Cache cargo
uses: Swatinem/rust-cache@v2
- name: Format check
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --workspace --all-targets --locked -- -D warnings
- name: Build
run: cargo build --workspace --locked --verbose
- name: Test
# Point the store-postgres / store-valkey roundtrip tests at the service containers above so
# they RUN (not skip) in CI. The services publish on localhost via the mapped ports.
env:
BUSBAR_TEST_POSTGRES_URL: postgres://busbar:busbar@localhost:5432/busbar_test
VALKEY_URL: redis://localhost:6379
run: cargo test --workspace --locked --verbose
# A test that writes into the repo is a test that can leak. The docs example harness once
# wrote its patched config and its secret stand-in next to the SHIPPED file it was validating,
# which put four generated artifacts in the working tree, two of them carrying the absolute
# home-directory path of the machine that ran them. They were committed, and only the
# public-hygiene gate downstream noticed. This makes the suite itself prove it wrote nothing:
# scratch belongs in a temp dir, always.
- name: Test suite left the working tree clean (no test writes into the repo)
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "The test suite modified the working tree. Tests must write scratch to a temp dir."
echo "Offending paths:"
git status --porcelain
git diff --stat
exit 1
fi
echo "working tree clean after the full test suite"
# OpenAPI schema gate (CI-ONLY `openapi-schema` feature). `openapi_doc()` derives typed response
# schemas via schemars — a dependency deliberately kept OUT of the shipped binary (the `check` job
# above builds/tests WITHOUT the feature, proving that). This job compiles the feature, lints it,
# and runs the openapi tests: the DRIFT GUARD (`openapi_json_matches_committed_file`) fails the PR
# if the committed `openapi.json` — the file the runtime serves via `include_str!` — no longer
# matches what the code generates, and the COVERAGE LOCK proves every operation has a typed body.
# Regenerate a stale file with:
# UPDATE_OPENAPI=1 cargo test -p busbar --features openapi-schema openapi_json_matches_committed_file
openapi-schema:
name: openapi-schema clippy · drift · coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- name: Clippy (--features openapi-schema)
run: cargo clippy -p busbar --all-targets --features openapi-schema --locked -- -D warnings
- name: OpenAPI tests (drift guard + coverage lock)
run: cargo test -p busbar --features openapi-schema --locked openapi -- --nocapture
# CONFIG-STABILITY gate. 1.5.3 is the LAST config-breaking release; after
# it the config grammar is FROZEN and every future feature may add only NEW OPTIONAL keys/sections/
# enum-variants. This job ENFORCES that per-PR, exactly like the openapi drift guard above but with
# the mechanic that guard lacks: an ADDITIVE-ONLY classifier. The self-test runs FIRST (never trust
# the gate's verdict before proving its RED/GREEN discipline still holds — like every sibling lint),
# then the gate: (1) DRIFT — the committed config-schema.snapshot.json must byte-match a fresh render
# of the config surface (a config-type change that forgot to regen fails loud with UPDATE_CONFIG_SCHEMA=1);
# (2) ADDITIVE-ONLY — the committed baseline (read from a git ref, NEVER the working tree, so a
# snapshot refresh cannot launder a break) vs the fresh render, classified: new optional field / new
# section / enum-append = OK; field removed/retyped, newly-required, enum-drop = FAIL naming the field.
# Every config busbar ever SHIPPED must still migrate to the current shape. The corpus is the real
# `config.yaml` (and companion `providers.yaml`) from every non-rc tag, so this asks the only
# question that matters to an operator upgrading: does the migrator still work for the version I
# am actually on? Motivated by the terraform provider, where a config shape aged out, broke
# that consumer, and nothing noticed until a daily poll went red.
#
# Needs full history: the corpus is checked in, but `refresh.sh` reads tags, and a shallow clone
# would make a regenerated corpus silently smaller rather than failing loudly.
migration-corpus:
name: migration corpus (every shipped config still migrates)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: Swatinem/rust-cache@v2
- name: Corpus is present and covers the tags
run: |
set -euo pipefail
n=$(find tests/migration-corpus/from-tags -name '*.yaml' | wc -l)
tags=$(git tag | grep -vcE 'rc' || true)
echo "corpus: $n config(s) across $tags non-rc tag(s)"
# A corpus that silently shrank is the failure mode this whole job exists to prevent, so
# an empty or truncated one fails HERE with a clear message rather than passing a test
# that iterated over nothing.
if [ "$n" -lt 20 ]; then
echo "::error::migration corpus has only $n config(s); regenerate with tests/migration-corpus/refresh.sh"
exit 1
fi
- name: Every shipped config migrates to a valid current config
run: cargo test -p busbar --test migration_corpus --locked -- --nocapture
config-stability:
name: config-stability gate (frozen grammar · additive-only)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
# Need history so the additive check can diff against the PR base (not just the tip).
fetch-depth: 0
# On a PR, the anti-launder baseline is the BASE branch's snapshot (a break committed alongside
# a refreshed snapshot on the PR tip is still caught against the base). On a push, HEAD is the
# baseline (no-op delta). schemars is NOT needed — the gate is a stdlib-python fingerprint, so
# this job needs no Rust toolchain and stays cheap.
- name: Config-stability gate self-test (prove RED/GREEN before trusting the verdict)
run: scripts/config-stability-gate.sh --selftest
- name: Config-stability gate (drift + additive-only)
env:
CONFIG_SCHEMA_BASELINE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || 'HEAD' }}
run: scripts/config-stability-gate.sh --check
# PUBLIC-HYGIENE gate. busbar is sold to enterprises, and this repo, the docs site and the
# generated openapi.json are all public. A customer who finds an internal tracking id, a developer's
# home directory or test-process narration in a shipped file does not conclude "untidy" — they
# conclude the product was assembled by a process they were not shown.
# That is a commercial fact about the product, so it gets a control rather than a sweep.
#
# Each of these is a TEXT class. A manual sweep removes instances; this removes the class, and
# keeps it removed.
#
# Eleven rules, each derived from text really found in this repo's history, each carrying its own
# RED fixture AND a GREEN twin. The false-positive controls are as load-bearing as the rules — the
# vendor names this gateway routes to (Anthropic, OpenAI, Claude, Gemini), technical invariants
# like `fail closed`, and identifiers that merely contain a flagged word are all explicit GREEN
# controls — because a gate that cries wolf gets switched off, and then it protects nothing.
#
# No toolchain and no build: it is pure stdlib python over the file list `git ls-files` reports,
# which is the exact definition of "what the public gets". Self-test FIRST, and a scan that
# discovers ZERO files is a HARD FAILURE, so neither a broken rule table nor a mistyped path can
# read as "clean". The same script runs over every PLUGIN repo via the plugin-ci reusable workflow.
public-hygiene:
name: docs hygiene (public prose describes the software, not the process)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Public-hygiene self-test (every rule proven RED, every twin proven GREEN)
run: python3 scripts/public-hygiene-lint.py --selftest
- name: Public-hygiene gate
run: python3 scripts/public-hygiene-lint.py --root .
# EXECUTABLE-CONFIG gate. Every config-grammar guard that existed before this one was scoped to
# DOCS — `crates/busbar/tests/docs_examples.rs` reads `docs/**`, marketing's check-config-blocks.mjs
# reads what it PUBLISHES. Nothing ever looked at the configs a MACHINE runs: the `cat > config.yaml
# <<EOF` heredocs in CI workflows and shell scripts, the config strings inside Rust integration
# tests, the yaml under examples/ and docker/. Those are exactly the ones that rot, because a docs
# example gets read by a human every release and an e2e heredoc gets read by nobody until an engine
# upgrade refuses to boot it — which is how the 1.5.3 retired-auth-grammar defect reached a long
# list of plugin repos, core's own plugin-ci.yml, signing-gate.sh, release-check.sh, the shipped
# docker/config.yaml and a shipped example, all at once and all invisible.
#
# It judges with the REAL binary (`busbar --validate`, the same mechanism docs_examples.rs uses), so
# it can never drift from `detect_legacy_markers`. Its own job because it needs a compiled busbar;
# only the one binary is built, not the workspace. Self-test FIRST — a scanner that has quietly
# stopped extracting anything would otherwise pass vacuously, which is worse than no gate at all.
# The same script runs over every PLUGIN repo via the plugin-ci reusable workflow, so the fleet
# inherits it with no per-repo work.
executable-config-lint:
name: executable-config gate (heredocs · test literals · shipped yaml)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: Swatinem/rust-cache@v2
- name: Build busbar (the validator this gate judges with)
run: cargo build --locked --bin busbar
- name: Executable-config gate self-test (RED/GREEN before the verdict is trusted)
run: |
python3 -c "import yaml" 2>/dev/null || pip install --quiet pyyaml
python3 scripts/executable-config-lint.py --busbar target/debug/busbar --selftest
- name: Executable-config gate
run: python3 scripts/executable-config-lint.py --busbar target/debug/busbar --root .
# Compliance-by-compilation gate: busbar must build + lint clean with the built-in auth plugin
# COMPILED OUT (`--no-default-features`), so a regulated deployment can ship a binary that provably
# contains no such auth code. Build, clippy, AND the test suite all stay green on the featureless
# binary — the feature-dependent behavior tests (admin-token auth, native ranking policies) are
# `#[cfg(feature = ...)]`-gated, so what remains still passes.
#
# SCOPE, STATED HONESTLY: this job proves the featureless binary COMPILES, LINTS, and passes the
# unit tests that remain compiled into it. It does NOT boot the binary and does NOT serve a request,
# so it cannot see a core path that compiles fine and then fails at runtime because the module it
# reaches for is absent. Proving the featureless binary WORKS is the `no-plugins-gate` job below,
# which boots it and drives real HTTP through it.
no-default-features:
name: no-default-features build · clippy · test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- name: Clippy (no-default-features)
run: cargo clippy --no-default-features --locked -- -D warnings
- name: Build (no-default-features)
run: cargo build --no-default-features --locked --verbose
- name: Test (no-default-features)
run: cargo test --no-default-features --locked --verbose
# THE MECHANICAL DEFINITION OF "PLUGIN". Anything busbar calls a plugin must be 100% a plugin: make
# it downloadable-only tomorrow, ship it uninstalled, and CORE MUST STILL WORK. If core stops
# working, core assumed it always had that module — and that module is not a plugin, it is part of
# the engine wearing a plugin's name.
#
# This job RUNS the binary; `no-default-features` above only compiles it. Two axes, because a plugin
# can be wrongly assumed two independent ways and neither axis can see the other's failures:
# COMPILED OUT (`--no-default-features` — the built-in plugin features are not in the binary) and
# NOT INSTALLED (default features, but `plugins.dir` holds zero artifacts — catches core assuming a
# `dlopen`ed store/auth/hook/secret plugin is on disk). Against a config that references ZERO
# plugins, each axis must boot, serve `/healthz`, ANSWER ITS ADMIN PLANE (real reads and a real
# write — not just the unauthenticated liveness route), and proxy a real request end-to-end to a
# real mock upstream, asserted on the upstream's unique marker.
#
# It has its own job for the reason txn-guards states: a gate nothing runs is not a gate. The
# self-test runs FIRST and is a hard prerequisite — it drives the featureless binary against
# fixtures that genuinely depend on a compiled-out plugin, plus stub servers that pass every earlier
# assertion and break exactly one later one, so a gate that had rotted into passing vacuously fails
# here instead of manufacturing false confidence.
no-plugins-gate:
name: no-plugins gate (compiled out · not installed)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: No-plugins gate SELF-TEST (prove RED/GREEN before trusting the verdict)
run: scripts/no-plugins-gate.sh --selftest
- name: No-plugins gate (axis 1 compiled out · axis 2 not installed)
run: scripts/no-plugins-gate.sh --check
# CONCURRENCY GATES for the config-mutation transaction (choke point C). Neither of these can run
# inside `cargo test --workspace`: the compile fence must FAIL to build (it is a negative test
# behind the `txn-fence-red` feature) and the loom model explores interleavings exhaustively behind
# `loom-model`, far too slowly for the default suite. A gate nothing runs is not a gate, so they
# get their own job — otherwise the transaction guard could be dismantled with the tree still green.
txn-guards:
name: txn compile fence · loom model
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Transaction compile fence (must fail to compile)
run: scripts/txn-fence.sh
- name: Loom model of the swap invariant
run: scripts/loom.sh
# TIMING GATE: the ignored hot-path latency test, in release mode. Its bounds are deliberately
# generous (25ms p50 / 250ms p99 through the full in-process router+mock round trip) so runner
# noise can never trip it — it exists to catch GROSS hot-path regressions (sync I/O, stray
# sleeps) the instant they land. Fine-grained overhead numbers are bench/latency/'s job.
timing:
name: timing gate (release)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Hot-path timing gate
run: cargo test --release --locked timing_gate -- --ignored
# Portability gate: busbar must build + pass tests on Windows too (no OS-specific code).
windows:
name: windows build · test
runs-on: windows-latest
env:
# TWO numbers, in ONE place, so a change to either is unmissable in a diff.
#
# WINDOWS_MIN_TESTS is the HARD floor: below it the job fails. WINDOWS_EXPECTED_TESTS is the
# observed high-water mark: below it the job WARNS but still passes.
#
# Why two rather than one. A single floor pinned at the observed total makes a required check
# on `main` fail every time anyone legitimately consolidates or renames a test, which trains
# people to edit the number without reading it — and a floor nobody reads is a floor that gets
# lowered past a real regression. A single floor with slack, on the other hand, cannot see a
# whole suite disappear. So: the hard floor has slack, and the high-water mark carries the
# sensitivity, as a warning that costs nothing to be wrong about.
#
# BOTH ARE MEASURED, NOT GUESSED — and measured on THIS branch's suite. windows-latest
# observed `3387 tests passed across the workspace` for main's 1.5.4 tree (CI run
# 31828339952). dev's ci.yml carries 4700/4755 for the larger 1.6.0 suite; when dev promotes
# here through qa, its numbers ride along in its own copy of this file and are correct for
# the suite they arrive with. Pinning dev's numbers on main's smaller suite makes a required
# check permanently red, which is how these two values were first calibrated.
#
# RAISE THESE AS THE SUITE GROWS. Lowering either is only correct when a diff explains why.
WINDOWS_MIN_TESTS: "3300"
WINDOWS_EXPECTED_TESTS: "3387"
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --workspace --verbose
- name: Test
# `shell: bash` is REQUIRED and not a style choice: `run:` on a windows runner defaults to
# pwsh, where none of the pipeline below parses.
shell: bash
run: |
set -uo pipefail
# `tee` keeps the full log in the job output (the point of --verbose) while the same bytes
# are captured for the count. PIPESTATUS[0], not $?, is cargo's own exit status.
cargo test --workspace --verbose 2>&1 | tee "$RUNNER_TEMP/windows-test.log"
rc=${PIPESTATUS[0]}
# SUM across every test binary, not a single grep line: `test result: ok. N passed`
# is printed once PER binary, so a single-line match would floor against one binary and
# miss a whole suite vanishing. Sum every binary's `passed` count.
total=$(grep -oE 'test result: (ok|FAILED)\. [0-9]+ passed' "$RUNNER_TEMP/windows-test.log" \
| grep -oE '[0-9]+ passed' | grep -oE '[0-9]+' \
| awk '{s+=$1} END {print s+0}')
echo "windows: ${total} tests passed across the workspace (floor: ${WINDOWS_MIN_TESTS}, expected: ${WINDOWS_EXPECTED_TESTS})"
if [ "$rc" -ne 0 ]; then
echo "::error::cargo test --workspace FAILED on windows (exit ${rc})."
exit "$rc"
fi
# The sensitive half: a drop of even one test is surfaced, without failing a required
# check over legitimate churn. A drop nobody can explain is the shape this job exists for.
if [ "$total" -lt "$WINDOWS_EXPECTED_TESTS" ]; then
echo "::warning::windows ran ${total} tests, ${WINDOWS_EXPECTED_TESTS} were expected."
echo "::warning::If tests were legitimately removed or merged, LOWER WINDOWS_EXPECTED_TESTS in this job."
echo "::warning::If not, something stopped being collected — a cfg gate, a renamed harness, a dropped target."
elif [ "$total" -gt "$WINDOWS_EXPECTED_TESTS" ]; then
echo "::notice::windows ran ${total} tests, above the recorded ${WINDOWS_EXPECTED_TESTS}. Raise WINDOWS_EXPECTED_TESTS."
fi
if [ "$total" -lt "$WINDOWS_MIN_TESTS" ]; then
echo "::error::the windows portability gate ran ${total} tests, below its floor of ${WINDOWS_MIN_TESTS}."
echo "::error::A workspace whose suites compiled to nothing still prints 'test result: ok' and exits 0."
echo "::error::This is NOT a pass. Either a suite stopped being built/collected, or a cfg gate erased one."
echo "::error::If the drop is intended, LOWER the WINDOWS_MIN_TESTS floor in this job deliberately."
exit 1
fi
# COVERAGE — instrumented run of the same workspace suite `check` runs, uploaded to Codecov for
# the README badge and per-PR line-coverage context. Deliberately ADDITIVE and NON-GATING:
#
# * It is NOT in the ci-umbrella `needs`/RESULTS below, so it is never a required check and can
# never turn `dev`/`qa`/`main` red. Coverage reporting is an OBSERVATION, not a correctness
# gate — the correctness gate is `check`, which this must never be able to mask or block.
# * It is a SEPARATE job, not a step folded into `check`, on purpose: `cargo llvm-cov` compiles
# with `-C instrument-coverage` and drops `*.profraw` files, which would trip `check`'s
# "working tree clean" assertion. Isolating it keeps that assertion honest.
# * The upload uses `fail_ci_if_error: false` so a Codecov outage or a tokenless-rate-limit is a
# no-op here, not a red job — a reporting dependency must never be able to fail the build.
# * FULL TIER ONLY, same `if:` as windows/openapi/etc., so a feature-branch push doesn't pay for
# a full instrumented rebuild; it runs on every PR and on dev/qa/main/dispatch.
#
# Same digest-pinned postgres/valkey services + BUSBAR_TEST_POSTGRES_URL / VALKEY_URL as `check`,
# so the store-postgres / store-valkey roundtrip tests are covered here too rather than skipped.
coverage:
name: coverage (llvm-cov · codecov)
runs-on: ubuntu-latest
if: github.event_name != 'push' || contains(fromJSON('["refs/heads/main", "refs/heads/dev", "refs/heads/qa"]'), github.ref)
services:
postgres:
image: postgres:16@sha256:95206741a5b214807675e14165369d05b93a9cf692223b616d07cca227e74b0b
env:
POSTGRES_USER: busbar
POSTGRES_PASSWORD: busbar
POSTGRES_DB: busbar_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U busbar"
--health-interval 10s
--health-timeout 5s
--health-retries 5
valkey:
image: valkey/valkey:8@sha256:495e4fecdc98ee48a20b207726caa5ab6451e0fac3642a9be10d9e70b3068df6
ports:
- 6379:6379
options: >-
--health-cmd "valkey-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v7
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- name: Cache cargo
uses: Swatinem/rust-cache@v2
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Collect coverage (workspace, live-DB tests wired)
env:
BUSBAR_TEST_POSTGRES_URL: postgres://busbar:busbar@localhost:5432/busbar_test
VALKEY_URL: redis://localhost:6379
run: cargo llvm-cov --workspace --locked --ignore-run-fail --lcov --output-path lcov.info
- name: Upload to Codecov (never fails the build)
uses: codecov/codecov-action@v5
with:
# Codecov rejects tokenless uploads on protected branches (dev/qa/main) with
# "Token required because branch is protected". This org-level secret authenticates
# the upload on every branch. Still never gates CI (fail_ci_if_error: false).
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info
slug: GetBusbar/busbar
fail_ci_if_error: false
# ── ci umbrella: the SINGLE required status for branch protection on `qa` and `main`.
#
# Branch protection should require ONE context per stage, not a growing list that silently drifts
# when a job is renamed (the `windows build · test` trap: `main` required a context name that
# `dev` had already renamed, so any PR hung forever on a check that could no longer be reported).
# With this umbrella, protection never changes as CI evolves, and a renamed or deleted job can
# never silently drop out of the gate — it turns this job red instead.
#
# `if: always()` so it reports even when a need FAILED, was CANCELLED, or was SKIPPED. It then
# asserts each need's result itself. The rule on THIS branch's ci.yml is simple: this workflow
# has no job-level `if:` anywhere, so every job runs on every trigger, and NOTHING may skip —
# a skipped required job must never read as a pass. (dev's ci.yml carries a fast/full tier split
# with a documented skip allowlist; when that arrives here via normal promotion it brings its own
# umbrella. Until then, any skip is RED.)
#
# It reports ALL offenders (the loop never short-circuits), reads each need's own `result`
# (never a pipe's exit), and fails once at the end with a count.
ci-umbrella:
name: ci umbrella
if: always()
needs:
- structure-lint
- check
- openapi-schema
- migration-corpus
- config-stability
- public-hygiene
- executable-config-lint
- no-default-features
- no-plugins-gate
- txn-guards
- timing
- windows
runs-on: ubuntu-latest
timeout-minutes: 5
env:
# jobkey|result — one line per need, read back in the loop below. Every job in this workflow
# is listed; adding a job to this file without adding it here leaves it un-gated, which is
# why the needs list and this list are adjacent and identical.
RESULTS: |
structure-lint|${{ needs.structure-lint.result }}
check|${{ needs.check.result }}
openapi-schema|${{ needs.openapi-schema.result }}
migration-corpus|${{ needs.migration-corpus.result }}
config-stability|${{ needs.config-stability.result }}
public-hygiene|${{ needs.public-hygiene.result }}
executable-config-lint|${{ needs.executable-config-lint.result }}
no-default-features|${{ needs.no-default-features.result }}
no-plugins-gate|${{ needs.no-plugins-gate.result }}
txn-guards|${{ needs.txn-guards.result }}
timing|${{ needs.timing.result }}
windows|${{ needs.windows.result }}
steps:
- name: Assert every required job reported green (no skip is ever allowed here)
run: |
set -u
fails=0
echo "ci umbrella: every job in this workflow must have run and passed."
while IFS='|' read -r job result; do
[ -n "$job" ] || continue
if [ "$result" = "success" ]; then
status="OK"
else
status="RED (${result})"
fails=$((fails + 1))
fi
printf ' %-24s %s\n' "$job" "$status"
done <<< "$RESULTS"
if [ "$fails" -ne 0 ]; then
echo "::error::ci umbrella: RED — ${fails} required job(s) did not pass on this ref."
echo "::error::A skipped, failed, or cancelled required job is NOT a pass. DO NOT PROMOTE."
exit 1
fi
echo "ci umbrella: GREEN"