Every developer knows the ritual. A GitHub Actions run goes red. You click through to the failing job. You scroll past 256 lines of runner setup and caching noise to finally find the one line that actually mattered. Then you open the commit, the PR, the history — by hand, one browser tab at a time. For every single failure.
A failed run is not a stack trace in isolation. It's a fact about your repo: which error, which commit caused it, what the PR reviews said, and whether we've seen this exact failure before. The log gives you the first; everything else you reassemble from memory.
I got tired of doing that by hand, so I wrote ci-context: one command that takes a failed run ID and prints a single-page report with six sections — what failed, what commit did it, what the PR said, and how often we've seen this before.
uv run ci-context gh run 30432597129 --repo TBNLZLDYD/ci-context
This isn't AI. That's the whole point.
gh run view --log-failedgh run view --log-failed is genuinely useful, but it hands you a wall of text. The root cause is somewhere in there, guaranteed, but so is a hundred lines of runner bootstrap. Errors come out in the order the shell printed them, not in the order they matter. And it only ever shows you the log — the commit, PR, and history context are separate queries you make yourself.
ci-context assembles everything relevant around the failure into one report:
log and deduplicated.
and changed files with add/delete counts.
reviews, and comments.
across the last N runs, with a failure-rate trend.
gh commands to investigate or rerun.Real output for a real failed run:
╭─ CI Failure Report ──────────── TBNLZLDYD/ci-context ─╮
│ Run #30432597129 · CI │
│ Conclusion: failure │
╰───────────────────────────────────────────────────────╯
Extracted Errors (2 found)
[high] Ruff lint error - E501 Line too long (116 > 100)
File: src/ci_context/github/exceptions.py:54
Step: test (3.12)
[medium] GHA exit code - Process completed with exit code 1
Commit Context
ef1b4e7 - docs: fix outdated documentation across CLAUDE.md, README, and source code
Author: TBNLZLDYD
src/ci_context/github/exceptions.py +1 -1
History Pattern (29 runs analyzed)
[exact] 7bf49b3c8fe94c39 Occurred 6 times First: 2026-07-20 · Last: 2026-07-23
Failure rate: 17% overall · 0% recent · trend: decreasing
Quick Actions
gh run rerun 30432597129 --failed
gh repo view TBNLZLDYD/ci-context --commit ef1b4e7
See what happened there? The root cause is the first thing you read, not the last thing you scroll to. The recurring fingerprint is flagged [exact] with a count. There are actions you can run without opening a browser.
When every tool is bolting on an LLM, the interesting differentiation is sometimes to not. ci-context is deliberately:
no temperature, no drift.
API.
No third-party service sees them.
You can read every pattern in patterns.py.
Regex over AI isn't a downgrade here; it's a feature. The language of CI failures is repetitive and well-bounded (tracebacks, npm ERR!, Error:, FAILED), so a deterministic extractor is both fast and explainable.
##[section] and::group::/::endgroup:: markers, and timestamp prefixes. Preserve line numbers.
(Python, Node, Go, Java, Shell). Each pattern has a start, a message, a location (file:line), and an end condition.
To ask "have we seen this before?", an error needs an identity stable enough to survive minor variation. ci-context fingerprints each error by normalising away the volatile bits — numbers → <NUM>, paths → <ROOT>/, commit SHAs → <SHA> — then lowercasing and taking a hash.
It's the same idea as eslint's rules reusing a stable rule ID, or crash reporters grouping by stack-frame signature. Once you have a fingerprint, the matcher can compare it across the last N runs: exact → [EXACT], Levenshtein similar → [SIMILAR], otherwise [NEW].
~/.cache/ci-context/history.db) stores fingerprints and runmetadata so a warm history scan doesn't re-fetch runs it's already fingerprinted.
job-log downloads PyGithub doesn't support.
--token → config file → gh auth token.No magic from environment variables.
mypy, ruff clean, 348 tests.It only speaks to failed runs, and it reads GitHub Actions specifically — there's no GitLab / CircleCI support yet. The value is proportional to how much Actions you run and how often you debug it. For a solo maintainer or a small team running CI every push, that's a lot of saved tab-switching.
pip install ci-context
ci-context gh run <your-failed-run-id> --repo owner/repo
or build from source — the README covers both. There are real, verified examples in EXAMPLES.md so you can see exactly what you'll get before installing anything.
If you've ever spent ten minutes reconstructing a failure's context by hand, that's the gap it fills. I'm genuinely curious whether the six-section shape matches your mental model — issues and feedback welcome. What do you wish a CI failure report showed you?