Troubleshooting¶
Symptom, cause, fix. Start with the exit code or the error string you actually saw.
Gate exit codes¶
The chargate CLI exits with one of three codes. Your CI step's pass or fail comes
straight from these.
| Code | Name | Meaning | What to do |
|---|---|---|---|
0 |
EXIT_OK |
No blocking net-new findings. Pre-existing findings never block. | Nothing. |
1 |
EXIT_BLOCKED |
The diff introduced at least one finding at or above --fail-on. |
Fix the finding, or lower the threshold if the severity is wrong for you. |
2 |
EXIT_ERROR |
Setup or tool error. The gate could not scan, so a pass would be meaningless. | Read the step log. A shallow clone and a runs-less SARIF are the two common causes, both below. |
Exit 2 is not a pass
A tool error never degrades into a pass. If MegaLinter itself blew up, the gate scanned nothing and says so.
Gate problems¶
The gate blocks on findings the PR did not introduce¶
Net-new classification compares against the merge-base, which needs full history. With a shallow clone there is no merge-base, so the comparison cannot be made.
Set fetch-depth: 0 on your checkout step:
- uses: actions/checkout@v5
with:
fetch-depth: 0
Chargate fails loudly rather than guessing. See Net-new gating for the exact classification rules.
A finding in a SOPS-encrypted file blocks the PR¶
Chargate reads the working-tree file, not the redacted SARIF snippet, and drops secret
findings whose value is an ENC[AES256_GCM,...] ciphertext. Plaintext in the same file
still gates, which is deliberate.
If a genuinely encrypted value still blocks, check that the file is actually SOPS output and that the finding is classified as a secret.
A linter runs (or is skipped) and its findings never reach the gate¶
The gate only ever reads the merged SARIF, so a linter whose descriptor does not set
can_output_sarif is invisible to it however cleanly it runs. Standalone mode therefore
skips those by name, with the reason linter emits no SARIF — it could never reach the
gate, and lists them in linters_skipped and on the PR rather than dropping them
silently.
Three entries used to be missing from that list. ACTION_ACTIONLINT, PYTHON_PYLINT
and POWERSHELL_POWERSHELL carried sarif=True in Chargate's registry purely because
that is the _entry default — none had ever been probed, and none of them sets
can_output_sarif at MegaLinter v10.0.0. Each one cost a container pull per run and
contributed nothing the gate could read, which looks exactly like a clean repo. They are
now recorded as sarif=False and skipped with that reason.
tests/test_linters_registry.py cross-references every sarif flag against MegaLinter's
own descriptors at the pinned tag, so the table and upstream cannot drift apart again —
the same way the arm64 flags are re-probed against the live registry.
If you enabled one of these expecting findings: there are none to have. Use a linter that
emits SARIF (the quality set is five of them,
see The quality flavor), or accept that this one gates
only via strict, as a tool error.
The scan reports nothing and the gate passes¶
A SARIF report with zero runs means nothing was scanned. Chargate fails this on its
own rather than behind --strict, because a pass on an empty report is meaningless.
If you see it, the linter container failed to start. Read the MegaLinter output above
the gate step.
A downstream tool read counts.json and saw a clean pull request¶
chargate ci writes --filtered-out and --counts-json before it decides its own
exit code. A run whose SARIF carried no runs therefore leaves a well-formed counts
document full of zeros on disk and then exits 2 for exactly that reason. A consumer
reading only the file sees net_new_count: 0 and reports green.
The file's existence is not evidence that anything was scanned. Gate on the step's
outcome (or the CLI's exit code) as well as the document, and treat exit 2 as no
answer rather than no findings. Full contract:
Consuming the output.
Chargate[bot] comments are attributed to github-actions[bot]¶
This is the most common report, and it is almost never the broker being down. The
token exchange is fail-soft: on any error scripts/request-app-token.sh emits an
empty token and exits 0, the comment is still posted, and the only symptom is the
byline. Nothing goes red.
The reason is always in the step log as a warning:
::warning::Chargate[bot] token unavailable (<reason>); PR comments fall back to github-actions[bot].
| Reason string | Cause | Fix |
|---|---|---|
missing 'id-token: write' |
The job cannot request an OIDC token. | Add id-token: write to the workflow's permissions. See Setup. |
curl not installed on this runner |
Minimal self-hosted or ARC image. | Install curl and jq in the runner image. |
jq not installed on this runner |
Same. | Same. |
broker disabled |
token_broker_url was set to an empty string. |
Intentional. Set it to re-enable. |
bad GITHUB_REPOSITORY |
The variable is not owner/name. |
Only happens outside GitHub Actions. |
OIDC request failed |
The runner could not reach GitHub's OIDC endpoint. | Network or egress policy on a self-hosted runner. |
OIDC token missing |
GitHub returned a response with no value. |
Retry. Report if it persists. |
broker unreachable |
The HTTP request itself failed, so the hostname did not resolve or refused the connection. | Check DNS for the broker hostname. |
broker HTTP <status> |
The broker answered with a non-200. Look the status up below. | See the next section. |
broker returned no token |
The broker answered 200 with no token field. |
Report. This should not happen. |
Never read a green build as proof the broker works
Every failure above still produces a passing job and a posted comment. The weekly
broker-smoke.yml run and the comment byline are the only signals.
Broker responses¶
POST /token returns one of these. The body is always {"error": "<code>"} except on
success. The Lambda also writes exactly one structured line per request,
{"outcome": "<code>"}, which is what to grep for in CloudWatch Logs.
| Status | Code | Cause | Fix |
|---|---|---|---|
200 |
(none) | Token minted. Body is {"token", "expires_at", "repository"}. |
|
400 |
invalid_json |
Body was not JSON, or was JSON but not an object. | Client bug. |
400 |
missing_fields |
One of oidcToken, owner, repo was absent or empty. |
Client bug. |
400 |
invalid_repository |
owner or repo is not a plain GitHub identifier, or is not a string. |
Rejected before the value can reach an API URL. |
401 |
invalid_oidc |
The OIDC token failed verification: signature, issuer, audience, or expiry. | Check that the workflow requests the same audience the broker expects. |
403 |
repo_mismatch |
The token's repository claim does not equal the requested owner/repo. |
This is the control that stops one repo minting a token for another. |
403 |
repo_not_allowed |
An allowlist is configured and this repository is not on it. | Add the repository, or clear the allowlist to accept any repo the App is installed on. |
403 |
app_not_installed |
GitHub returned 404 for the repository's installation. | Install the Chargate App on that repository. |
502 |
mint_failed |
The GitHub API rejected the mint for some other reason. | Check the App's private key and permissions. |
503 |
config_unavailable |
The broker could not resolve its own configuration. | The deployment is unfinished: the App ID or private key is missing, or the function cannot read them. |
503 |
jwks_unavailable |
GitHub's JWKS could not be fetched. | Upstream. The caller's token may be perfectly valid, which is why this is not a 401. |
Two more the router returns before any of the above:
| Status | Code | Cause |
|---|---|---|
404 |
not_found |
Path is not /healthz, /readyz or /token. |
405 |
method_not_allowed |
Right path, wrong method. /token is POST; the health endpoints are GET. |
Reading the logs¶
Every /token request emits one line with a fixed vocabulary, so a CloudWatch filter
can count them:
{"outcome": "mint_ok"}
{"outcome": "app_not_installed"}
The vocabulary is closed and never contains caller-supplied text. OIDC failures log the exception class name only, for the same reason.
/healthz returning 200 proves almost nothing
It answers before configuration is read, so it returns 200 on a broker with no
credentials and no App installed. /readyz is the one that resolves
configuration. A real signed POST /token is the only full check.
See Broker deployment for the deployment side.