CI gates¶
GitHub Actions runs every gate on every PR. The matrix below is the source of truth.
Gate matrix¶
| Gate | npm script | Failure mode | Where to look |
|---|---|---|---|
| Format | format:check | Prettier reports unformatted files | Run npm run format locally |
| Type check | type-check | tsc --noEmit errors | tsconfig.json — strict mode is on, no any, no unused |
| ESLint | lint (also runs architecture + canaries + format:check) | Any rule violation; --max-warnings 0 | eslint.config.mjs |
| Biome | lint:biome | Biome rule violation | biome.json |
| Architecture | lint:architecture src/Scrapers/Pipeline | Cross-layer import violation | Lives under src/Tests/Tools/lint-and-validate.ts |
| Canaries | lint:canaries | One of the 33 TypeScript canaries didn't trigger its expected error | src/Scrapers/Pipeline/EslintCanaries/verify.sh |
| Dead code | lint:dead-code | Unused exports / unreachable code | src/Tests/Tools/detect-dead-code.ts |
| Phase isolation lint | lint:phases:strict | Phase H test code outside the allowed pattern | src/Tests/Unit/Pipeline/CrossValidation/Phases/ |
| Unit tests | test:unit | 4807 tests; all must pass | --testPathIgnorePatterns=E2eReal --testPathIgnorePatterns=E2eMocked |
| Pipeline + coverage | test:pipeline | Coverage drops below 97/95/97/98 | src/coverage/lcov-report/ |
| Mock E2E | test:e2e:mock | Fixture-driven E2E for 3 banks | src/Tests/E2eMocked/ |
| Mock suite (orchestrated) | test:mock | scripts/run-mock-suite.ts driving all configured banks | Same fixtures |
| Bank tests | test:e2e-factory-tests | Phase H cross-bank factory drives every phase per bank | src/Tests/Unit/Pipeline/CrossValidation/Phases/ |
| Build | build | tsup ESM + CJS bundle | lib/index.{mjs,cjs,d.ts,d.cts} produced |
| PR body compliance | n/a (server-side actions/github-script) | PR body missing one of the 3 mandatory sections (## Why, ## What, ## Guideline compliance) | .github/workflows/pr-body-check.yml — mirrored locally by npm run lint:pr-body |
Coverage thresholds¶
| Metric | Threshold | Source |
|---|---|---|
| Statements | ≥ 97% | jest.pipeline.config.cjs |
| Branches | ≥ 95% | same |
| Functions | ≥ 97% | same |
| Lines | ≥ 98% | same |
A PR that drops any threshold fails test:pipeline. The post-Commit-1 numbers (v8.4.0) are 97.20% / 95.23% / 97.19% / 98.45%.
Where to find logs¶
| Where it ran | Log location |
|---|---|
Local npm run <script> | stdout |
| Pre-commit hook | .pre-commit-output.log at repo root (overwritten each run) |
| GitHub Actions | The PR's "Checks" tab → workflow logs |
Forensic diagnostics artifacts¶
BLUF: CI diagnostics never leave the access-controlled private store. FORENSIC_TRACE=true enables one per-run folder from TraceConfig.getRunFolder containing pipeline.log, network/*.json, and screenshots. On a failed real-E2E job that whole folder uploads only to the private OCI diagnostics store — nothing goes to a public GitHub artifact, because the bundle can carry rendered PII.
On failed real-E2E jobs, .github/scripts/ci/upload-private-diagnostics.sh uploads the full run folder to the access-controlled OCI diagnostics store when OCI_DIAG_PAR_URL is available. The step is best-effort and keeps forked PRs green when the private upload secret is absent.
Object keys are laid out <bank>/<run_id>-<run_attempt>/forensic-<bank>-<tag>.zip, so each CI run groups under its own <run_id>-<run_attempt> segment.
Retention¶
BLUF: a one-time bucket Lifecycle Policy deletes diagnostics older than 7 days server-side; CI never deletes, because the PAR cannot.
OCI Pre-Authenticated Requests grant only GET (read) and PUT (write/overwrite) — no PAR access type can issue an HTTP DELETE. The CI job therefore only ever uploads; it cannot prune the bucket with OCI_DIAG_PAR_URL. Retention is enforced outside the upload step:
- Age — active. A server-side Object Lifecycle Policy on the diagnostics bucket deletes objects 7 days after creation. Set once in the OCI Console (Bucket → Lifecycle Policy Rules → Create Rule → Action Delete, Target Objects, 7 days) or via
oci os object-lifecycle-policy put. No CI code, no extra secret, runs daily server-side. - Count — keep newest 5 runs — deferred. Count-based pruning needs a real
DeleteObjectcall, which a PAR cannot make. It is deferred until an OCI API-key secret is added to CI; the prune would then list + delete viaoci os object, grouping by the<run_id>-<run_attempt>key segment and keeping the 5 newest run tags.
What changed in v8.4¶
- 3 new ESLint canaries for BALANCE-RESOLVE boundary enforcement:
balance-resolve-isolation,no-balance-in-scrape,balance-fetch-only-in-balance-resolve. - 12 new tests in
BalanceResolveActionsCoverage.test.ts+BalanceResolvePhase.test.ts+ScrapePostDetectionBranches.test.tspushing branch coverage from 94.59% → 95.23%. - TypeDoc output relocated from
docs/totypedoc-build/sodocs/can host mkdocs sources.