Skip to main content

pipelinemd

CI License: MIT

GitLab CI/CD failure doctor — takes a failed pipeline, works out what actually broke, and tells you how to fix it.

A failed job's log is a terminal recording, not a report. It can run to tens of thousands of lines, most of them progress bars that rewrote themselves five hundred times, and the one line that matters is somewhere in the middle. pipelinemd does two things about that:

  1. A deterministic distiller replays the trace the way a terminal would, strips the noise, scores every line for failure-likeness, and keeps only the regions that explain the outcome — then matches them against a catalog of 59 known CI failure signatures, each with a real fix. Every report is placed in one of seven v1 failure classes — yaml, ci_vars, image_pull, cache_artifact, test, runner, flaky — with the kind of fix it wants. Flaky comes from GitLab's retry history, not from a guess: if another attempt of the same job passed on the same commit, it says so.
  2. An optional Claude diagnosis reads only that distilled evidence and names the root cause, separating the actual fault from its fallout — and must cite the evidence lines it relied on. A diagnosis that cites nothing real is rejected rather than reported.

Every report says how far to trust it and what it cost. The confidence is never more than the weakest signal behind it; at low, the report says needs human review and why. The cost is $0 for rules alone, and an estimate from the API's own token counts when Claude was asked.

The first half needs no API key, no model, and no third-party packages at all. The second is the upgrade.

$ pipelinemd diagnose https://gitlab.com/acme/web/-/jobs/98765

pipelinemd  build (test) #98765
  acme/web · ref main · exit code 1 · 47s
  ERROR: Job failed: exit code 1
  test → code_patch  ·  high  ·  from npm.lockfile-out-of-sync
  confidence high
  est. cost $0.0519 · claude-opus-5 · 5,210 in / 1,034 out tokens

Diagnosis   high confidence · dependency

  npm ci refused to install because package-lock.json no longer matches package.json.

  Line 41203 shows npm rejecting the install outright rather than resolving it.
  The lockfile still pins react@17 while package.json now asks for ^18 (line
  41211), which is the change that broke the pair.

Suggested fixes
  1. Regenerate the lockfile and commit it

       npm install
       git add package-lock.json && git commit -m 'Regenerate lockfile'

Rule matches
  ● npm.lockfile-out-of-sync  package-lock.json is out of sync with package.json  L41203

Evidence   9 of 41,284 lines · 100.0% reduced

Install

pip install pipelinemd              # core: distiller + rules, zero dependencies
pip install 'pipelinemd[llm]'       # adds the Claude diagnosis layer

Requires Python 3.11+.

Or skip Python altogether. The image has the [llm] extra installed, runs as an unprivileged user, and is built for amd64 and arm64:

docker run --rm ghcr.io/rbalukja15/pipelinemd:0.1 --version
docker run --rm -i ghcr.io/rbalukja15/pipelinemd:0.1 distill - < build.log

The image runs as uid 10001, so to save a report redirect stdout rather than passing -o into a mounted directory, which that user may not be allowed to write to:

docker run --rm -i ghcr.io/rbalukja15/pipelinemd:0.1 distill - < build.log > report.txt

On rootful Linux Docker, --user "$(id -u):$(id -g)" also lets -o write into a mount; rootless Docker and Podman map that uid elsewhere, so redirect there.

Tags follow the package: 0.1.0 exactly, 0.1 for the latest patch release, and latest. The image and the PyPI package are published by one workflow from one commit, so a version means the same thing in both (docs/releasing.md).

Use

Diagnose a failed job or pipeline

pipelinemd diagnose https://gitlab.com/acme/web/-/jobs/98765
pipelinemd diagnose https://gitlab.com/acme/web/-/pipelines/12345   # picks the failed job
pipelinemd diagnose --project acme/web --pipeline 12345

Give it a pipeline and it finds the failed jobs for you; with more than one it diagnoses the first and names the rest (--all-jobs does them all).

Credentials come from --token, $PIPELINEMD_TOKEN, $GITLAB_TOKEN, $GITLAB_PRIVATE_TOKEN or $CI_JOB_TOKEN — in that order, with the right header for each. Public projects need no token at all.

Distil a log you already have — fully offline

pipelinemd distill build.log
kubectl logs job/ci-run | pipelinemd distill -

No network, no model, no key. Useful on its own for shrinking a log before pasting it anywhere.

Score it against the corpus

make eval

Runs the distiller and rule engine over 62 labelled traces in corpus/ and reports rule@1, evidence hit rate and exit-code accuracy per failure class. Offline and deterministic. Results and their caveats: docs/evaluation.md.

make gate is the same run with CI's thresholds, and runs on every push — a catalog regression fails the build rather than being noticed later.

Browse the rule catalog

pipelinemd rules                        # all 59, grouped by category
pipelinemd rules --category dependency
pipelinemd rules --search docker
pipelinemd explain npm.eresolve         # one rule in full, patterns included

Inside GitLab CI

Run with no target at all and it diagnoses the pipeline it is running in:

diagnose:
  stage: .post
  image:
    name: ghcr.io/rbalukja15/pipelinemd:0.1
    entrypoint: [""]
  when: on_failure
  script:
    - pipelinemd diagnose --format markdown -o diagnosis.md
  artifacts:
    when: always
    paths: [diagnosis.md]

Nothing is installed when the job runs, so a failure is diagnosed even while PyPI is slow or unreachable. The entrypoint: [""] matters: the image's entrypoint is the CLI, and GitLab needs a shell to run script:.

CI_JOB_TOKEN and CI_PIPELINE_ID are picked up automatically. Set ANTHROPIC_API_KEY as a masked CI variable to enable the diagnosis layer.

Any image with Python 3.11+ works too, at the cost of an install on every failure:

diagnose:
  stage: .post
  image: python:3.12-slim
  when: on_failure
  script:
    - pip install 'pipelinemd[llm]'
    - pipelinemd diagnose --format markdown -o diagnosis.md
  artifacts:
    when: always
    paths: [diagnosis.md]

A ready-made job is in examples/gitlab-ci-diagnose.yml.

Output formats

--format For
terminal (default) Reading it yourself. Colour honours NO_COLOR and non-TTY output.
markdown Pasting into a merge request or issue. Collapsible evidence.
json Other tooling. Versioned via schema_version. One job is one report object; several (--all-jobs) are {"reports": [...], "cost": {...}}, with the run's total cost beside them.

How the distiller works

Each stage is pure, independently testable, and does one thing:

Stage What it removes or adds
ANSI Colour codes, cursor moves, erase sequences, OSC hyperlinks.
Overwrites Replays \r and \b positionally — a progress bar that rewrote one line 500 times collapses to its final frame.
Sections Parses section_start/section_end markers into structured spans with durations, and attributes every line to the section it ran in.
Timestamps Strips the runner's optional per-line RFC3339 prefix.
Redaction Masks credential-shaped substrings before anything leaves the process.
Scoring Weights every line against 27 failure signals and 5 dampeners, so 0 failed, 12 passed does not outrank the real fault.
Windowing Grows context around strong signals, merges overlapping windows, spends a fixed line budget on the best, and always keeps the tail — the runner writes its verdict there.
Collapsing Folds runs of near-identical lines into one entry with a repeat count.

Same trace in, same evidence out — which is what makes it cheap to cache and safe to assert on in tests.

Redaction

pipelinemd can send evidence to an LLM, and you will paste its output into merge requests. GitLab masks known CI variables; anything a build tool prints itself does not get that protection. So before evidence reaches the prompt, the terminal, or the JSON output, these shapes are masked:

GitLab tokens (glpat-, glrt-, …) · GitHub tokens · AWS access key ids · Slack tokens · JWTs · credentials embedded in URLs · Authorization: headers · --password/--token style flags · KEY=value where the key names a secret · PEM private key blocks.

Values that only look secret-shaped (SECRET=false, ***) are left alone. This reduces exposure; it is not a guarantee — treat traces from untrusted pipelines accordingly.

Exit codes

Code Meaning
0 A report was produced.
2 Usage error — bad URL, missing argument, unknown rule.
3 GitLab error — unreachable, unauthorised, not found.
4 Nothing to diagnose — the pipeline has no failed jobs.

A failed Claude call is not fatal: pipelinemd warns on stderr and reports the deterministic findings anyway.

For how the pieces fit together, see docs/architecture.md; for how a version reaches PyPI and the image, docs/releasing.md.

Design notes

  • The core has no dependencies. Not "few" — none. It installs into any runner image without dragging a tree behind it, which matters for a tool whose whole job is to run in someone else's broken build.
  • Rules first, model second. Every rule fires without a network call. The model is asked to do only what rules cannot: decide which of several signals is the cause and which is the consequence.
  • The model never sees a raw trace. It sees distilled, redacted evidence plus what the rules already concluded — which keeps requests small and cheap, and keeps the model's effort on the judgement call.
  • A diagnosis must point at something. Every diagnosis cites line numbers, and every cited number is resolved against the excerpt the model was shown. Cite nothing real and the diagnosis is rejected; cite a mix and the invented numbers are printed alongside it. Confident prose is easy; a claim you can check is the product.
  • Confidence is earned, not asserted. An analysis is only as confident as its weakest part — the rule, the model's own rating — and loses a level for each sign of trouble. It stays high/medium/low: a decimal would claim a calibration nothing here has, and make eval reports how often each level is right instead.
  • The distiller is pure. No clock, no network, no randomness.

Contributing a rule

Rules live in src/pipelinemd/rules/catalog.py. A good one is narrow: anchor patterns to text the tool actually prints, write explanation as why this happens, and make each entry of fixes something someone can do. Add a fixture under tests/fixtures/traces/ and assert the rule fires on it.

Then give it a v1 class in RULE_CLASS in src/pipelinemd/taxonomy.py — the suite fails until you do. Choose from what the failure is, and only use flaky if it is transient whatever the environment; anything that is merely often transient is flaky only when retry history says so.

License

MIT — see LICENSE.

Metadata

Release files for pipelinemd 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pipelinemd 0.1.1
File Size Uploaded
pipelinemd-0.1.1.tar.gz 185.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pipelinemd 0.1.1
File Interpreter ABI Platform
pipelinemd-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 284.2 kB

Release files / pipelinemd-0.1.1.tar.gz

Download URL pipelinemd-0.1.1.tar.gz
Size 185.4 kB
Tags Source
SHA-256 checksum
How to use checksums
5cb552600a7bf5a77c14feedea180699773e00e4011df8c6b8798ce480a90bee
BLAKE2b-256 checksum
How to use checksums
14d3a7a73b3635fa5950daf700b88e509b53bb6bdcb6ba422b150bbc0b992e87
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / pipelinemd-0.1.1-py3-none-any.whl

Download URL pipelinemd-0.1.1-py3-none-any.whl
Size 98.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d96d0d2b774866f5d133ca1407a02ca3f6024ab127baa4f53651d0aef7c56941
BLAKE2b-256 checksum
How to use checksums
6cf12ce9a50e8764c79d5f39fa826d61761d1e82e90846382ac9528125d753a7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page