Architecture¶
Brimyr is one brimyr Python CLI (src/brimyr/cli.py:main) behind three GitHub
surfaces, running two gates: patch coverage and net-new quality findings. The
design splits cleanly into a pure core and a thin set of side-effecting edges.
Module map¶
src/brimyr/
cli.py # argparse dispatch: coverage | ci | local | lint | version
coverage/ # ★ THE PURE CORE: deterministic, no I/O, heavily tested
diff.py # unified-diff text -> DiffIndex (changed files + added line ranges)
model.py # CoverageReport / FileCoverage + CoverageBuilder (covered-wins merge)
lcov.py # lcov .info -> CoverageReport
cobertura.py # Cobertura XML -> CoverageReport
jacoco.py # JaCoCo XML -> CoverageReport (its own format, not Cobertura)
patch.py # DiffIndex ∩ CoverageReport -> PatchCoverage (the gate's heart)
# + compute_total_coverage: the reported, never-gated total
git.py # the ONLY git/subprocess boundary (merge-base, diff, shallow detect)
detect.py # ecosystem markers -> Ecosystem (test command + coverage format)
runner.py # run tests with coverage, locate + ingest the file (broken-run rule)
sonar.py # sonar-scanner runner (failure-isolated, never raises)
sonar_dotnet.py # dotnet sonarscanner begin/end WRAPPING the build (.NET only)
html_report.py # ReportGenerator wrapper -> browsable HTML artifact (optional)
gate.py # patch % + threshold -> pass/fail + exit code
quality.py # pure: chargate's net-new counts + fail_on level -> a verdict
modes.py # PR (gate) vs baseline (no gate) resolution
report.py # GitHub job summary + step outputs (also renders the PR comment)
github_comment.py # marker-based upsert of the ONE PR comment (never raises)
broker_client.py # Actions OIDC -> Brimyr[bot] token (fails soft, falls back)
local.py # local base resolution for the pre-push check
The design rule¶
coverage/ is pure: it takes already-parsed data (unified-diff text + a
coverage report) and returns numbers. git.py, runner.py, sonar.py,
sonar_dotnet.py and html_report.py are the only modules that shell out, and each
injects its runner, so the core is unit-tested with synthetic diff text and coverage
strings, no real repository or toolchain required. quality.py is pure in the same
way without living under coverage/: it is handed already-parsed JSON and returns a
verdict, and cli.py does the reading. The two network edges, github_comment.py and
broker_client.py, follow the same shape: stdlib urllib only, with the opener
injected so the tests need no network, and neither ever raises out into the gate.
Keep the boundary
Do not import subprocess, os, network code, or GitHub Actions into
coverage/. That separation is what makes the crown-jewel patch.py trivially
testable and deterministic.
Data flow (PR / gate mode)¶
modes.resolve_modedecides PR (gate) vs baseline (no gate) fromGITHUB_EVENT_NAMEor an explicit flag.detect.detect_ecosystemssniffs marker files → the ecosystem(s) and their test commands (or the escape hatch / forced ecosystem is used instead).runner.run_testsruns each ecosystem's command with coverage on, locates the emitted file, and parses it (coverage.lcov/coverage.cobertura) into aCoverageReport. A failed/empty run setsRunResult.broken.git.compute_changed_linesresolvesmerge-base(base, head), runsgit diff --unified=0, and hands the text tocoverage.diff.parse_unified_diff→ aDiffIndex.coverage.patch.compute_patch_coverageintersects the diff with the report → aPatchCoverage(covered / total changed-executable lines, per-file misses).gate.decide_gateapplies the threshold (and the broken-run rule) → aGateDecisionand exit code.sonar.run_scanner(optional) ships quality + coverage to SonarQube. It is failure-isolated: it never raises, so a Sonar outage can't fail the gate.quality.decide_quality_gate(optional) decides the net-new half onfail_on, over the countscliread andquality.parse_countsvalidated out of the JSON the nested Chargate step left behind — or, told the scan never completed,quality.broken_decisionreads nothing and reports a tool error. SameMode.gatesflag as coverage, so baseline gates neither.reportwrites the GitHub job summary and step outputs —render_summary's coverage block, withrender_quality_summary's net-new block appended when that half ran.github_comment.post_pr_comment(optional) puts that same rendered summary on the PR as a single marker-owned comment, creating it once, thenPATCHing it on every later push. Whentoken_broker_urlis set,broker_client.mint_bot_tokenfirst exchanges the job's Actions OIDC token for aBrimyr[bot]installation token; on any failure it returnsNoneand the job'sGITHUB_TOKENis used instead. Both are failure-isolated, like Sonar.
Baseline mode skips the gating — both halves. It computes coverage against an empty
DiffIndex, ships to Sonar, and never blocks; a baseline quality run is report-only
for its own reason (no diff, so no net-new set worth gating), which the summary names
rather than blaming the threshold.
The process exits with the worse of the two halves' codes.
The quality half calls Chargate instead of importing it¶
Brimyr is quality assurance, and coverage is only half of that. The other half judges
the net-new findings a MegaLinter quality run produced. Chargate already owns a
finished net-new engine (chargate filter-sarif), so brimyr does not vendor it or
re-implement it: action.yml runs magmamoose/chargate as a nested step, and
quality.py reads the two files that step leaves behind. The --counts-json document
is the verdict's only input; the filtered SARIF is display-only, skimmed for the
path:line [rule] strings the summary lists. Anything unreadable, unrecognised, or
self-contradicting is exit 2, because a half that cannot evaluate must not report a
pass — and so is a scan that never completed, which action.yml detects from the nested
step's outcome and passes on as --quality-scan-broken, reading no file at all.
brimyr lint runs that half on its own, under its own PR-comment marker.
brimyr ci --quality-counts instead folds the same verdict into the one job summary
and the one PR comment coverage already writes, which is the consolidated view and the
reason to prefer it. What the threshold means, and how to turn the half on, are on
Quality findings.
The token broker is not part of the CLI¶
broker/ in the repository is a separate deployable: the AWS Lambda service
that mints Brimyr[bot] tokens, with its own pyproject.toml, lockfile, Ruff
config and CI job. Nothing under src/brimyr imports it; broker_client.py talks
to it over HTTPS like any other remote service, which is what keeps the CLI
stdlib-only and dependency-free. Its architecture, local LocalStack loop and go-live
runbook live with the code, in
broker/README.md.
Exit-code contract¶
| Code | Meaning |
|---|---|
0 |
pass |
1 |
patch coverage below threshold, or blocking net-new quality findings |
2 |
broken test run / setup / usage error, or unreadable quality input |
A broken test run is a tool error (2), never "0% patch coverage". When both halves
run in one brimyr ci, the process exits with the worse of the two codes on that same
0 < 1 < 2 scale: clean coverage never launders a blocking quality finding.
Testing¶
Tests mirror modules 1:1 under tests/ (e.g. test_patch.py, test_gate.py,
test_cobertura.py). The pure core is tested with synthetic inputs; the
subprocess boundaries inject their runner so they are exercised without a real
toolchain, git, or a live SonarQube.