Architecture¶
Chargate is one chargate Python CLI (src/chargate/cli.py:main) behind two
GitHub surfaces (a composite action and a pre-commit hook). The design splits
cleanly into a pure core and a thin set of side-effecting edges.
Module map¶
src/chargate/
cli.py # argparse dispatch: filter-sarif | ci | local | install-hooks | uninstall-hooks | version
sarif/ # ★ THE PURE CORE — deterministic, no I/O, heavily tested
diff.py # unified-diff text -> DiffIndex (changed files + added line ranges)
model.py # defensive SARIF result accessors (uri, startLine, level, severity)
filter.py # net-new classification + FilterPolicy + filter_sarif()
counts.py # totals + per-severity breakdowns; owns COUNTS_SCHEMA_VERSION
sops.py # detect SOPS-encrypted values so secret scanners don't gate on them
dedup.py # collapse the same finding when several linters report it
git.py # the ONLY git/subprocess boundary (merge-base, diff, shallow detect)
gate.py # net-new verdicts + fail_on threshold -> pass/fail + exit code
megalinter.py # build env/command, run, locate the merged SARIF
defectdojo.py # SARIF import/reimport client (urllib, failure-isolated, never raises)
dependencytrack.py # CycloneDX BOM upload client (urllib, failure-isolated, never raises)
github_comment.py # GHAS-style PR comment client (urllib, failure-isolated, never raises)
install_hooks.py # global git-hook installer (backs install-hooks / uninstall-hooks)
modes.py # PR (gate) vs baseline (no gate) resolution
report.py # GitHub job summary + PR comment bodies + step outputs
local.py # pre-commit fast staged-file runner
A structured, machine-readable version (exports, dependencies, call graph,
hotspots) lives at PROJECT_INDEX.json
in the repo root.
Separate from the CLI, the token broker (broker/) is its own deployable. See
The token broker below.
The design rule¶
sarif/ is pure: it takes already-parsed data (a SARIF dict + a DiffIndex)
and returns verdicts. git.py is the only thing that shells out, so the filter is
unit-tested with synthetic diff text and SARIF dicts, with no real repository
required.
Keep the boundary
Do not import subprocess, os, network code, or GitHub Actions into
sarif/. That separation is what makes the crown-jewel filter trivially
testable and deterministic.
Data flow (PR / gate mode)¶
modes.resolve_modedecides PR (gate) vs baseline (no gate) fromGITHUB_EVENT_NAMEor an explicit flag.megalinter.runruns MegaLinter whole-repo withDISABLE_ERRORS=true(so MegaLinter never sets the exit code) and locates the merged SARIF.git.compute_changed_linesresolvesmerge-base(base, head), runsgit diff --unified=0, and hands the text tosarif.diff.parse_unified_diff→ aDiffIndex.sarif.filter.filter_sarifclassifies every result as net-new or pre-existing under aFilterPolicy, returning a pruned deep copy (net-new only), per-result verdicts, andCounts. The input SARIF is never mutated.gate.decide_gateapplies thefail_onthreshold to the net-new set → aGateDecisionand exit code.defectdojo.import_sarifanddependencytrack.upload_bom(both optional, each active iff its host/URL is set) ship the full SARIF and a CycloneDX BOM respectively. Both are failure-isolated: they never raise, so a sink outage can't fail the gate.github_comment.post_pr_feedback(PR events, opt-out) posts the net-new findings as one updatable summary comment + inline review comments. Also failure-isolated: a GitHub API error never changes the gate outcome.reportwrites the GitHub job summary and step outputs.
Baseline mode skips steps 3-5's gating: it counts everything against an empty
DiffIndex with fail_on=none, ships the full SARIF, and never blocks.
Exit-code contract¶
| Code | Meaning |
|---|---|
0 |
pass |
1 |
blocking net-new finding(s) |
2 |
setup / tool / usage error |
A broken scanner is a tool error (2), never a finding. A MegaLinter tool
failure only fails the job under --strict.
One condition is fatal without --strict: a SARIF carrying no runs at all.
That is not a linter misbehaving, it is the gate having scanned nothing, so a pass
carries no information. Since strict defaults to off, routing it through
strict would leave a repo green on an empty report indefinitely. That is precisely
how the relative-REPORT_OUTPUT_FOLDER bug survived for months.
The output documents are written before that decision. cmd_ci writes
--filtered-out and --counts-json as soon as the filter has run, then reaches the
runs-less check and exits 2, so a run that scanned nothing still leaves a well-formed
counts file of zeros behind. The documents describe whatever was classified, which is
the right contract — but it means a downstream consumer must read the exit code, not the
file's existence. See Consuming the output.
The token broker¶
To author PR comments as Chargate[bot] rather than github-actions[bot], the
action exchanges the run's GitHub Actions OIDC token for a short-lived
Chargate App installation token. That exchange is done by
broker/, a separate
deployable that is not part of the CLI wheel. It keeps its pyjwt and httpx
dependencies in its own broker/pyproject.toml and virtualenv, so the CLI stays
runtime-dependency-free.
app/broker.py holds the decisions with no HTTP framework attached.
app/lambda_handler.py is what production runs. app/main.py is a FastAPI shell for
local development and the test suite, and the build excludes it from the deployed zip
by name.
POST /token verifies the OIDC token (issuer-pinned, audience chargate, and the
repository claim must equal the requested owner/repo) and mints a token
scoped to that repo with pull_requests: write only. The whole flow is
fail-soft: without id-token: write, or if the App isn't installed, the action
silently falls back to github-actions[bot]. That also means a broken broker is
silent, so treat "nothing failed" as no evidence at all. The service ships as a
version-scoped zip in S3 and runs as an AWS Lambda behind an API Gateway HTTP API; the
Terraform module and leaf live in
magmamoose/infra under terraform/aws/chargate/,
and deploying is a reviewed one-line bump of broker_artifact_version there. Operating it
(the GitHub App, its private key in SSM Parameter Store, and installing the App on
consumer orgs) is the operator's responsibility; see
Token broker deployment. Also see
PR comments → Comment as Chargate[bot] for the
consumer-side setup.
Testing¶
Tests mirror modules 1:1 under tests/ (e.g. test_sarif_filter.py,
test_gate.py, test_git.py); the broker has its own tests under broker/tests.
The pure core is tested with synthetic inputs; the subprocess and HTTP boundaries
inject their runner/opener so they are exercised without Docker, git, or a live
DefectDojo / Dependency-Track.