wip(feat/1.6.0-oauth-cimd): agent work preserved, interrupted by an A… #914
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| # TWO TIERS, AND WHY. | |
| # | |
| # This workflow used to trigger on `push` to main/dev/qa ONLY, plus `pull_request`. That left every | |
| # feature branch COMPLETELY UNGATED until someone opened a PR. During 1.5.4 development two work | |
| # items were reported green from local runs and had never been CI-verified at all; the gap was only | |
| # discovered when the PR was opened, days of work later. "It passed locally" is not a CI result, and | |
| # a branch nobody has opened a PR for is exactly where that mistake is cheapest to make and most | |
| # expensive to discover. | |
| # | |
| # So: `branches: ['**']`, and EVERY branch push now runs CI. But the full set is 12 jobs including a | |
| # Windows runner, a release-mode timing gate, a loom model and a full-history migration corpus, and | |
| # paying all of that on every intermediate commit of a long-lived feature branch is real money for | |
| # very little marginal signal. So pushes to NON-PROMOTION branches run the FAST TIER: | |
| # | |
| # FAST TIER (every branch push) structure lint, fmt/clippy/build/test, config-stability, | |
| # public-hygiene. This answers the only questions that matter | |
| # mid-feature: does it build, does it lint, do the tests pass, is | |
| # the layout/config-grammar/public-prose still clean. | |
| # FULL TIER (dev, qa, main, and everything above PLUS openapi-schema, migration-corpus, | |
| # EVERY pull request, and executable-config, no-default-features, no-plugins-gate, | |
| # workflow_dispatch) txn-guards, timing, windows. | |
| # | |
| # THE TRADEOFF, STATED PLAINLY: a Windows-only or featureless-build-only regression is now caught at | |
| # PR time rather than at push time. That is exactly where it was caught before this change, so the | |
| # fast tier costs nothing anyone had; it is pure addition. The cost paid is that a branch with an | |
| # open PR runs the fast tier (push) alongside the full tier (pull_request) for the same commit. The | |
| # two events land in DIFFERENT concurrency groups (`github.ref` is `refs/heads/<branch>` for push and | |
| # `refs/pull/N/merge` for pull_request) and they are deliberately NOT unified: a shared group would | |
| # let the cheap push run cancel the full PR run that the required checks depend on. Four duplicated | |
| # jobs is the accepted price of never again shipping an unverified branch. | |
| # | |
| # ESCAPE HATCH: `workflow_dispatch` forces the FULL tier on any ref, so a feature branch can buy the | |
| # whole gate before opening a PR without waiting for one: | |
| # gh workflow run ci.yml -R GetBusbar/busbar --ref feat/my-branch | |
| # | |
| # `branches:` never matches tag pushes, so the release tag (v1.5.3 et al) still does not run CI here. | |
| on: | |
| push: | |
| branches: ['**'] | |
| pull_request: | |
| workflow_dispatch: | |
| # 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. | |
| # Doubly load-bearing now that every branch push triggers a run. | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUSTFLAGS: "-D warnings" | |
| jobs: | |
| # WHICH TIER RAN, SAID OUT LOUD. A green check mark is read as "the gate passed", and after this | |
| # change a green check on a feature branch means something WEAKER than a green check on a PR. | |
| # An operator who cannot see the difference will assume the strong one, which is the precise | |
| # mistake this workflow's two tiers otherwise re-introduce in a new place. So every run states its | |
| # own tier in the job summary and names the jobs it did NOT run. Costs one echo; buys the run's | |
| # verdict being self-describing instead of needing someone to remember the rule. | |
| gate-tier: | |
| name: gate tier (what this run actually proved) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Declare the tier | |
| # Ref and event come in as ENV, not as `${{ }}` spliced into the script text. A branch name | |
| # is attacker-influenced data (anyone who can open a PR picks it) and GitHub allows plenty of | |
| # shell metacharacters in one; splicing it into a `[ ... ]` test is a script-injection seam | |
| # that happens to be quiet until someone names a branch to exploit it. | |
| env: | |
| EVENT: ${{ github.event_name }} | |
| REF: ${{ github.ref }} | |
| REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$EVENT" != "push" ] || \ | |
| [ "$REF" = "refs/heads/main" ] || \ | |
| [ "$REF" = "refs/heads/dev" ] || \ | |
| [ "$REF" = "refs/heads/qa" ]; then | |
| { | |
| echo "### CI tier: FULL" | |
| echo "" | |
| echo "Every CI job ran. Event \`$EVENT\`, ref \`$REF\`." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| { | |
| echo "### CI tier: FAST" | |
| echo "" | |
| echo "Push to a non-promotion branch (\`$REF\`), so this run proved:" | |
| echo "structure lint, fmt/clippy/build/test, config-stability, public-hygiene." | |
| echo "" | |
| echo "It did NOT run: openapi-schema, migration-corpus, executable-config," | |
| echo "no-default-features, no-plugins-gate, txn-guards, timing, windows." | |
| echo "" | |
| echo "Those run on every pull request, on dev/qa/main, and on demand via" | |
| echo "\`gh workflow run ci.yml -R GetBusbar/busbar --ref $REF_NAME\`." | |
| echo "" | |
| echo "A green check here is NOT the full gate. Do not promote on it." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| cat "$GITHUB_STEP_SUMMARY" | |
| # 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 | |
| # RELEASE-ORDER lint: nothing may be tagged until it has been verified from the consumer side, | |
| # and nothing may be cut from a red commit. Both of those live in the SHAPE of the workflow | |
| # graph (which job depends on which), which is edited by people in a hurry during an incident | |
| # and has no other place to be asserted. Self-test runs FIRST, and it earns its place: it | |
| # caught this lint's own `--draft` rule passing against a release that had had `--draft` | |
| # deleted, because `--draft=false` elsewhere in the file contained the substring. | |
| - name: Release-order lint self-test | |
| run: python3 scripts/release-order-lint.py --selftest | |
| - name: Release-order lint | |
| run: python3 scripts/release-order-lint.py --root . | |
| # AND WATCH IT FAIL. `--prove` fails each job of the release graph in turn and asserts that no | |
| # name-minting job runs afterwards. It is the executable form of "a failure leaves nothing | |
| # public", so that property is re-proven on every push rather than argued once in a comment. | |
| - name: Release-order proof (a failure leaves nothing public) | |
| run: python3 scripts/release-order-lint.py --prove | |
| # WORKSPACE-DEPS lint: one version requirement per external dependency, written once in | |
| # [workspace.dependencies]. The table makes that POSSIBLE; only this lint makes it TRUE — | |
| # nothing in Cargo stops a member re-stating a version, and before the table `hex`, `sha2` and | |
| # `tracing` were each declared two different ways and agreed purely by luck. It sits in the | |
| # FAST tier deliberately: a manifest can drift on any branch push, and this costs one python | |
| # process. Self-test runs FIRST and covers dev-dependencies and | |
| # `[target.'cfg(...)'.dependencies]` explicitly, because a rule enforced only on | |
| # `[dependencies]` is a rule scoped to where the bug was first seen — and the jemalloc pins, | |
| # the ones that differ per shipped target, live in a target table. | |
| - name: Workspace-deps lint self-test | |
| run: python3 scripts/workspace-deps-lint.py --selftest | |
| - name: Workspace-deps lint | |
| run: python3 scripts/workspace-deps-lint.py --root . | |
| # QA-GATE DISPATCHER DRIFT. `workflow_run` ALWAYS loads the workflow file from the DEFAULT | |
| # branch, so the gate that actually fires after a push to `qa` is `main`'s copy, never the | |
| # promoted commit's. That has already cost a silent green: measured on qa c736177 the | |
| # auto-fired gate ran ONE job while the segmentation umbrella sat unused, and the run passed | |
| # having done far less than anyone believed. Gate LOGIC now rides the commit (the dispatcher | |
| # checks out the triggering SHA and runs scripts/qa-gate-run.sh from there), but everything | |
| # GitHub reads BEFORE a checkout exists — `on:`, concurrency, permissions, the needs/if graph, | |
| # the matrix expression — still comes from `main` and cannot. | |
| # | |
| # This compares the PARSED structure, not the bytes, so comments drift freely while the run | |
| # graph may not. That split is what stops the check deadlocking: `main` only moves at a | |
| # release, so a byte-identical rule would make every prose edit red until the very release it | |
| # is meant to gate. Self-test first, as everywhere else here. | |
| - name: qa-gate dispatcher drift self-test | |
| run: python3 scripts/qa-gate-dispatch-lint.py --selftest | |
| # `fetch-depth: 0` is not set on this job's checkout, so origin/main may be absent; fetch it | |
| # explicitly rather than letting the lint fail closed for a reason that is not drift. | |
| - name: Fetch the default branch (the dispatcher that will actually fire) | |
| run: git fetch --no-tags --depth=1 origin main | |
| - name: qa-gate dispatcher drift | |
| run: python3 scripts/qa-gate-dispatch-lint.py | |
| # ARTIFACT CONTRACT wholeness. The contract is data (.github/artifact-contract.json) and the | |
| # verifier implements it, so the two can drift: a row declared with no implementation is a | |
| # property everybody believes is checked and nobody checks, and a check outside the contract is | |
| # invisible to anyone reading it. The guard asserts SET EQUALITY both ways plus a row floor, | |
| # and this self-test proves that guard still discriminates rather than merely returning the | |
| # rows unchanged. Its eight cases each construct one malformation and require it be refused. | |
| - name: Artifact-contract wholeness self-test | |
| run: python3 scripts/verify-artifact.py --selftest | |
| # RELEASE-CHECK VERDICT ACCOUNTING. release-check.sh used to end with an unconditional | |
| # "RELEASE GATE PASSED" banner even when phases had not run at all, so a green gate did not | |
| # prove those phases executed. This selftest proves the two are now distinguishable: a | |
| # coverage gap changes the banner, names the phase, and is fatal under --require-siblings, | |
| # while a by-design segment skip stays a clean pass. Offline, no gate run, seconds. | |
| - name: Release-check verdict self-test | |
| run: scripts/release-check.sh --selftest | |
| # 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: | |
| # PINNED BY DIGEST for the same reason as release.yml's gate: `postgres:16` and | |
| # `valkey/valkey:8` are moving tags, and these containers are load-bearing (without them the | |
| # store roundtrip tests hard-fail rather than skip). What they resolve to must be a fact about | |
| # the commit, not about the day. Re-pin with `docker buildx imagetools inspect <tag>`; the | |
| # monthly-refresh PR is the natural place. | |
| # | |
| # NO `credentials:` HERE, DELIBERATELY, AND IT LEAVES HALF THE HOLE OPEN. Authenticating would | |
| # raise Docker Hub's anonymous per-IP rate limit, which is the half a digest pin cannot fix -- | |
| # but this workflow runs on `pull_request`, where secrets are withheld from fork runs, so an | |
| # unconditional `credentials:` block would hand every fork PR empty credentials. release.yml's | |
| # gate (push to main only) IS authenticated. Closing this half properly means either | |
| # restricting the credentials to non-fork events, which GitHub does not allow on a service | |
| # container, or mirroring both images into GHCR and pulling from there. Written down rather | |
| # than quietly left, because `branch-green` requires this workflow green: a throttled pull | |
| # here stops a release. | |
| postgres: | |
| image: postgres:16@sha256:95206741a5b214807675e14165369d05b93a9cf692223b616d07cca227e74b0b | |
| 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@sha256:495e4fecdc98ee48a20b207726caa5ab6451e0fac3642a9be10d9e70b3068df6 | |
| 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 | |
| # FULL TIER ONLY. Skipped on pushes to non-promotion branches; runs on every pull | |
| # request, on dev/qa/main, and on workflow_dispatch. See the tier note at the top. | |
| if: github.event_name != 'push' || contains(fromJSON('["refs/heads/main", "refs/heads/dev", "refs/heads/qa"]'), github.ref) | |
| 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: | | |
| set -euo pipefail | |
| # FLOOR ON THE MATCH COUNT. `openapi` is a SUBSTRING filter; a filter that matches nothing | |
| # prints "running 0 tests / test result: ok" and EXITS 0. The floor is not 1 — this step | |
| # claims to run the drift guard AND the coverage lock AND their neighbours, so a filter | |
| # that quietly collapsed to a single surviving test would still be a coverage hole. | |
| out="$(cargo test -p busbar --features openapi-schema --locked openapi -- --nocapture 2>&1 | tee /dev/stderr)" | |
| n="$(echo "$out" | sed -nE 's/^test result: ok\. ([0-9]+) passed.*/\1/p' | awk '{s+=$1} END{print s+0}')" | |
| if [ "$n" -lt 8 ]; then | |
| echo "::error::the OpenAPI suite ran only ${n} tests, expected >= 8." | |
| echo "::error::A zero/low-match 'cargo test' exits 0 — this is NOT a pass. Fix the filter" | |
| echo "::error::(or lower this floor deliberately, in the same commit that removes tests)." | |
| exit 1 | |
| fi | |
| echo "OpenAPI suite: ${n} tests ran (floor 8)" | |
| # 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 | |
| # FULL TIER ONLY. Skipped on pushes to non-promotion branches; runs on every pull | |
| # request, on dev/qa/main, and on workflow_dispatch. See the tier note at the top. | |
| if: github.event_name != 'push' || contains(fromJSON('["refs/heads/main", "refs/heads/dev", "refs/heads/qa"]'), github.ref) | |
| 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 | |
| # FULL TIER ONLY. Skipped on pushes to non-promotion branches; runs on every pull | |
| # request, on dev/qa/main, and on workflow_dispatch. See the tier note at the top. | |
| if: github.event_name != 'push' || contains(fromJSON('["refs/heads/main", "refs/heads/dev", "refs/heads/qa"]'), github.ref) | |
| 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 | |
| # --min-docs: the gate is vacuously green over an empty extraction set. 50 documents are | |
| # found today; the floor is 40 so genuine churn passes and a collapsed extractor cannot. | |
| run: python3 scripts/executable-config-lint.py --busbar target/debug/busbar --root . --min-docs 40 | |
| # 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 | |
| # FULL TIER ONLY. Skipped on pushes to non-promotion branches; runs on every pull | |
| # request, on dev/qa/main, and on workflow_dispatch. See the tier note at the top. | |
| if: github.event_name != 'push' || contains(fromJSON('["refs/heads/main", "refs/heads/dev", "refs/heads/qa"]'), github.ref) | |
| 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 | |
| # FULL TIER ONLY. Skipped on pushes to non-promotion branches; runs on every pull | |
| # request, on dev/qa/main, and on workflow_dispatch. See the tier note at the top. | |
| if: github.event_name != 'push' || contains(fromJSON('["refs/heads/main", "refs/heads/dev", "refs/heads/qa"]'), github.ref) | |
| 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 | |
| # FULL TIER ONLY. Skipped on pushes to non-promotion branches; runs on every pull | |
| # request, on dev/qa/main, and on workflow_dispatch. See the tier note at the top. | |
| if: github.event_name != 'push' || contains(fromJSON('["refs/heads/main", "refs/heads/dev", "refs/heads/qa"]'), github.ref) | |
| 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 | |
| # FULL TIER ONLY. Skipped on pushes to non-promotion branches; runs on every pull | |
| # request, on dev/qa/main, and on workflow_dispatch. See the tier note at the top. | |
| if: github.event_name != 'push' || contains(fromJSON('["refs/heads/main", "refs/heads/dev", "refs/heads/qa"]'), github.ref) | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Hot-path timing gate | |
| run: | | |
| set -euo pipefail | |
| # FLOOR ON THE MATCH COUNT. `timing_gate` is a SUBSTRING filter and these tests are | |
| # `#[ignore]`d, so this job is their ONLY execution anywhere. A filter that matches nothing | |
| # prints "running 0 tests / test result: ok" and EXITS 0 — rename the tests and the hot-path | |
| # timing gate is green forever having measured nothing, with no other job to notice. | |
| out="$(cargo test --release --locked timing_gate -- --ignored 2>&1 | tee /dev/stderr)" | |
| echo "$out" | grep -qE 'test result: ok\. [1-9][0-9]* passed' || { | |
| echo "::error::the hot-path timing gate ran ZERO tests (renamed, moved, or un-ignored?)." | |
| echo "::error::A zero-match 'cargo test' exits 0 — this is NOT a pass." | |
| exit 1 | |
| } | |
| # Portability gate: busbar must build + pass tests on Windows too (no OS-specific code). | |
| windows: | |
| name: windows build · test | |
| runs-on: windows-latest | |
| # FULL TIER ONLY. Skipped on pushes to non-promotion branches; runs on every pull | |
| # request, on dev/qa/main, and on workflow_dispatch. See the tier note at the top. | |
| if: github.event_name != 'push' || contains(fromJSON('["refs/heads/main", "refs/heads/dev", "refs/heads/qa"]'), github.ref) | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Build | |
| run: cargo build --workspace --verbose | |
| - name: Test | |
| run: cargo test --workspace --verbose |