Postmortem CI failure analyzer: deterministic phase attribution plus one LLM explanation. Read-only, air-gap friendly.
Project description
Root-cause analysis for CI/CD.
A postmortem step that runs when a pipeline fails, works out where and why it broke,
and emits a structured report — without ever touching your code.
When a pipeline fails, the logs are long and loud. An LLM handed the whole trace will
confidently blame the runner or a missing cache — because that text is alarming — even
when the real failure was an exit code 1 three sections down. ci-doctor fixes that with
one rule:
Deterministic code decides where the job failed. The LLM only explains why.
Phase attribution is a pure function of job metadata and log structure, computed before
any model is called. A loud, non-fatal WARNING: can never outrank the actual failure. If
the classifier is wrong, that's a bug with a failing test — not a prompt to tune.
Features
- Deterministic phase attribution — a pure, fully-tested classifier decides where a job
broke (
provision·prepare·fetch·script·post) from metadata and log structure. Non-fatalWARNING:lines can never be blamed. - Read-only and safe — never edits code, commits, or opens MRs, and always exits 0, so it can't change your pipeline's status or hide the real failure.
- Useful with no LLM — ships a deterministic report out of the box: phase, reason, terminal command, evidence excerpt, and templated remediation.
- Bring-your-own model — optional LLM step via
openai(any OpenAI-compatible endpoint),litellm(Bedrock/Vertex/Azure), or the localclaudeCLI, selected by config. - GitLab & GitHub — one provider-neutral core; the GitHub adapter was added with zero changes to core.
- Air-gap friendly — no telemetry, no update checks, no runtime downloads; ship as a self-contained Docker image or an offline wheel bundle.
- Secret redaction — scrubs secrets from the prompt and the report (round-trip tested).
- Structured output — a rich terminal report,
report.md+report.jsonartifacts (with a self-containedhandoff_promptyou can paste into a coding agent), and one idempotent MR/PR note.
How it works
acquire → segment → attribute → denoise → extract → budget → (LLM) → render / deliver
└── deterministic: decides WHERE it failed ──┘ └── explains WHY ──┘
- Acquire the failed jobs and their logs (a missing log is valid data — the "never got a runner" case).
- Segment the trace into sections (GitLab
section_*markers / GitHub##[group]). - Attribute the failure to a phase — the pure classifier, first-match-wins precedence
ladder with a
WARNING:-is-never-fatal rule. - Denoise / extract / budget the blamed section into a small, high-signal evidence slice
(ANSI/CR/dedup denoising, anchored windows, token budgeting — every truncation is visible).
Shipped matcher packs cover pytest/jest/go/maven/gradle/bazel, Rust, .NET, Ruby, PHP,
node + npm/pnpm/yarn/bun, Playwright/Cypress, tsc/eslint/mypy, Docker and Terraform;
add your own under
extraction.matchers— they merge onto the shipped packs by id. - LLM (optional) explains the cause within the already-decided phase, returning a validated JSON report. Disabled or unreachable → deterministic report instead.
- Render / deliver to the terminal,
report.md/report.json, and an idempotent MR/PR note.
Install
On PyPI the distribution is
ci-doctorr(theci-doctorname was taken); the import package isci_doctorand the command staysci-doctor.
git clone https://github.com/fennet82/ci-doctor
cd ci-doctor
uv sync # or: pip install .
# optional LLM backends:
uv sync --extra litellm # or: pip install '.[litellm]' — only for Bedrock/Vertex/Azure
Or build the self-contained image:
docker build -t ci-doctor .
Quickstart
# Replay a captured log offline — no network, no LLM:
uv run ci-doctor analyze failing-job.log
# Against a live pipeline (reads $CI_PIPELINE_ID etc. inside CI):
uv run ci-doctor analyze "$CI_PIPELINE_ID"
# From your laptop, in a clone — the repo is taken from `git remote origin`
# (with a warning) when GITHUB_REPOSITORY / CI_PROJECT_ID isn't set:
uv run ci-doctor analyze 18234567890
ci-doctor config prints the effective config; --diff shows only what your layers
changed, --schema emits the JSON Schema. Full CLI and config reference on the
documentation site.
Configuration
Layered and pydantic-validated — defaults.yml < repo .ci-doctor.yml < CI_DOCTOR_* env
< CLI flags, with nested env vars using __ (CI_DOCTOR_LLM__MODEL=…). Unknown keys are
an error. Minimal config:
provider: gitlab
gitlab:
base_url: https://gitlab.com # default; override for self-hosted
token_env: CI_DOCTOR_GITLAB_TOKEN # or gitlab.token_file for a secret mount
llm:
enabled: true # false => deterministic-only report
backend: openai # openai | litellm | claude_code
model: qwen2.5-coder:32b
api_base: http://openai-compatible-endpoint.internal:8000/v1
Every knob, and the three LLM backends, are documented on the configuration page. The LLM step is optional throughout — disabled, unconfigured or unreachable, ci-doctor emits the deterministic report instead of failing.
Use it in CI
GitHub Actions — one step; run-id defaults to the run that just failed:
permissions: { actions: read, contents: read, pull-requests: write }
steps:
- id: doctor
uses: fennet82/ci-doctor@master # or pin a release tag
with:
post-pr-note: 'true'
- if: steps.doctor.outputs.is-infra-not-code == 'true'
run: echo "::notice::Infrastructure, not the change."
Outputs phase, category, confidence, is-infra-not-code, and the two report
paths, so a workflow can branch on the verdict. Inputs, PR comments and GitHub
Enterprise are on the action page;
the full workflow is in
examples/github-actions.example.yml.
Listing on GitHub Marketplace is a one-time manual step and cannot be scripted: draft or edit a release in the GitHub UI, tick Publish this Action to the GitHub Marketplace, and accept the terms.
action.ymlalready carries the metadata that requires. None of this affectsuses:— that resolves straight from the repo either way.
GitLab (.gitlab-ci.yml) — runs only on failure, always exits 0:
ci-doctor:
stage: .post
image: registry.internal.example.com/ci-doctor:latest
rules:
- when: on_failure
allow_failure: true
variables:
CI_DOCTOR_GITLAB_TOKEN: "$CI_DOCTOR_TOKEN"
script:
- ci-doctor analyze "$CI_PIPELINE_ID"
artifacts:
when: always
paths: [report.md, report.json]
Both providers have a complete, commented workflow in examples/.
Air-gapped / offline
ci-doctor makes no network calls except to the GitLab/GitHub and LLM endpoints you configure — no telemetry, no update checks, no runtime model/rule/schema downloads. Build once where there is internet, then ship inside:
docker build -t ci-doctor . # a self-contained image
pip wheel . -w ./wheels # ...or a wheel bundle
pip install --no-index --find-links ./wheels ci-doctorr # on the air-gapped host
Custom CA bundles (gitlab.ca_bundle, github.ca_bundle, llm.ca_bundle) and the
standard HTTPS_PROXY / NO_PROXY / REQUESTS_CA_BUNDLE variables are honoured.
Documentation
fennet82.github.io/ci-doctor — overview,
requirements, configuration, usage, the GitHub Action, and CI/CD examples. Source in
docs/site/ (Astro); mise run docs previews it locally.
Start with Concepts — the pipeline
end to end, what each module decides, and what a phase, a matcher or a redaction pass
actually is. Matchers lists every
shipped pack; that page is generated from config/defaults.yml, so run
mise run docs:data after changing a pack (a test fails if it drifts).
For contributors: how the code is built — where things go, the invariants, how to write tests — is GUIDELINES.md; how to get a change in — setup, commits, pre-push checks — is CONTRIBUTING.md. The original design spec, kept for provenance, is docs/PLAN.md.
Development
mise run setup # uv sync + the repo's git hooks (or just: uv sync)
mise run check # everything CI runs: tests, lint, guardrails, secret scan
The suite blocks real sockets and never calls an LLM, so it runs in seconds.
License
MIT © Elad Cohen
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ci_doctorr-2.0.1.tar.gz.
File metadata
- Download URL: ci_doctorr-2.0.1.tar.gz
- Upload date:
- Size: 671.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4eb5ce3011bdbca6efa5ee431d3c5c9703ba3938195e51651fc6135efe8915cd
|
|
| MD5 |
7cabab011b5bdd3d0e30084880d042f7
|
|
| BLAKE2b-256 |
a4fd3c0ce3f4a2396b2d511aeb01e9f42f3da1f9a2223da83eb6dd5e51b84ca1
|
Provenance
The following attestation bundles were made for ci_doctorr-2.0.1.tar.gz:
Publisher:
release.yml on fennet82/ci-doctor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ci_doctorr-2.0.1.tar.gz -
Subject digest:
4eb5ce3011bdbca6efa5ee431d3c5c9703ba3938195e51651fc6135efe8915cd - Sigstore transparency entry: 2293910335
- Sigstore integration time:
-
Permalink:
fennet82/ci-doctor@3deb3048c49023f843b4ba6581fbddc19ad382fd -
Branch / Tag:
refs/heads/master - Owner: https://github.com/fennet82
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3deb3048c49023f843b4ba6581fbddc19ad382fd -
Trigger Event:
push
-
Statement type:
File details
Details for the file ci_doctorr-2.0.1-py3-none-any.whl.
File metadata
- Download URL: ci_doctorr-2.0.1-py3-none-any.whl
- Upload date:
- Size: 76.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eda156d4082289b3db6c1d4e4301d0d6b623711b9f113ad1c8c1c66252c7a0b3
|
|
| MD5 |
b06c0259a87a3d4ac7fbe36e0674f6a5
|
|
| BLAKE2b-256 |
4bdcb31d5b1ecbbce05714a794fab2a96a7642b18653b16d7ce5d3e718c50fb0
|
Provenance
The following attestation bundles were made for ci_doctorr-2.0.1-py3-none-any.whl:
Publisher:
release.yml on fennet82/ci-doctor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ci_doctorr-2.0.1-py3-none-any.whl -
Subject digest:
eda156d4082289b3db6c1d4e4301d0d6b623711b9f113ad1c8c1c66252c7a0b3 - Sigstore transparency entry: 2293910436
- Sigstore integration time:
-
Permalink:
fennet82/ci-doctor@3deb3048c49023f843b4ba6581fbddc19ad382fd -
Branch / Tag:
refs/heads/master - Owner: https://github.com/fennet82
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3deb3048c49023f843b4ba6581fbddc19ad382fd -
Trigger Event:
push
-
Statement type: