Quickstart · Explain a failure · GitHub Action · For coding agents · The study · Contributing
When a CI run goes red and then turns green after someone presses re-run, nothing in the code changed. Whatever failed was a red herring: a flaky test, a network blip, a package registry hiccup, a runner that vanished.
redherring reads the GitHub Actions history your repo already has, finds every one of those, opens the failed logs, and names the flaky tests (20+ test-runner formats) or the infrastructure cause. When a build fails, redherring why tells you (or your coding agent) which failures are known red herrings and which look real.
No server, no signup, nothing to set up in CI first. If your repo has Actions history, the first run already has answers.
Real output for a public repo, 28 September 2026. The "AI provider" cause is an AI code-review job failing with "Selected model is at capacity".
Quickstart
With uv (or pipx install redherring / pip install redherring; Python 3.11+):
uvx redherring scan OWNER/REPO # what flakes here, and what it costs
uvx redherring why https://github.com/OWNER/REPO/actions/runs/ID # is this failure mine?
It needs a GitHub token to read Actions logs. If you use the GitHub CLI, it borrows gh auth token automatically; otherwise set GH_TOKEN. Any token works for public repositories; private ones need actions: read.
How common is this?
We ran it on 122 popular repos (1.25 million workflow runs over two weeks). For most repos it's small: about 2% of red runs. For a quarter of them it's 7% or more, and in some it's a daily tax. Across all 122, 690 runner-hours went to failed jobs that a plain re-run fixed.
Method, every repo's numbers, what causes them, and how accurate the classification is: read the study →
redherring scan: what flakes in this repo?
redherring scan # the repo of the current checkout
redherring scan vitejs/vite --days 14
redherring scan owner/repo --workflow ci.yml --format md > FLAKY.md
You get:
- Flaky tests: each failed and then passed on a re-run of the same commit, with how often, on how many commits, on which OS (plenty of tests only flake on Windows), and when last seen.
- Flaky jobs with no test named, grouped by cause:
network,package registry,rate limited,runner lost,runner environment,job timeout,out of memory,disk full,service startup,test worker crash,GitHub service,AI provider,many tests at once(an environment problem, not ten flaky tests), orunknown. - What it cost: runner time burned by the failed jobs, and how long commits sat red waiting for someone to press re-run.
--format json gives everything, including example job links, for your own dashboards.
redherring why: is this failure mine?
redherring why https://github.com/owner/repo/actions/runs/123456789
redherring why 123456789 --repo owner/repo --format json
For every failure in the run, one verdict:
| verdict | meaning |
|---|---|
| RED HERRING (known flaky) | This test failed and then passed on a re-run of the same commit at least twice in the last N days. |
| PROBABLY FLAKY | It did that once. Re-run once to check. |
| RED HERRING (infrastructure) | The log shows a network, registry, runner, or service problem, not a test failure. |
| NOT THIS CHANGE | The latest finished run on the default branch fails this same test. |
| LOOKS REAL | Never seen flaking and no infrastructure cause, or it already failed on an earlier attempt of this same commit: a re-run isn't fixing it. Probably your change. |
| UNCLEAR / POLICY CHECK | Can't tell from history, or a PR policy check (labels, linked issue) that passes once the PR is updated. |
Exit codes make it scriptable: 0 all red herrings (or nothing failed), 1 something looks real, 3 unclear, 2 error.
redherring why "$RUN" -q && gh run rerun "$RUN" --failed
For coding agents
Agents lose a lot of time "fixing" failures that were never theirs, and sometimes weaken a perfectly good test to make a flake go green. Add this to your AGENTS.md or CLAUDE.md:
## CI failures
Before changing code because CI failed, run `redherring why <run-url> --format json`.
- `recommendation: "rerun"`: every failure is a flake, an infrastructure problem, or already failing
on main. Re-run the failed jobs once (`gh run rerun <id> --failed`); don't edit code or tests for it.
If it fails again, run `redherring why` on the new attempt: a test that fails twice on the same
commit comes back as "looks real".
- `recommendation: "investigate"`: fix only the findings with `"red_herring": false`.
- Never skip, loosen, or delete a test just because it is listed as flaky.
Or install the ready-made skill: copy skills/redherring/ into your agent's skills folder (for Claude Code, .claude/skills/redherring/). More setups are in examples/.
GitHub Action
Explain every failed CI run, in its job summary and as a comment on the pull request:
# .github/workflows/redherring.yml
name: redherring
on:
workflow_run:
workflows: [CI] # the name of your test workflow
types: [completed]
permissions:
actions: read
contents: read
pull-requests: write # only needed for comment: true
jobs:
why:
if: github.event.workflow_run.conclusion == 'failure'
runs-on: ubuntu-latest
steps:
- uses: ixklo/redherring@v0.2.1
with:
comment: true
The comment is posted once per PR and workflow, then edited in place on later failures, never duplicated.
Or post a weekly flaky-test report:
on:
schedule: [{ cron: "0 7 * * 1" }]
permissions:
actions: read
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: ixklo/redherring@v0.2.1
with:
command: scan
days: "30"
Inputs: command (why or scan), run-id, days (default 14), workflow, ledger, comment, cache (default on: keeps downloaded history between runs, because the Actions token allows about 1,000 API requests an hour), fail-on-real, github-token. Output: recommendation. From the command line, the same comment is redherring why <run> --comment.
Keep your flake history: the ledger
From 1 October 2026 GitHub deletes workflow runs once they pass your log-retention period (90 days by default). Past that, redherring has nothing to read. A ledger keeps the evidence:
redherring scan --ledger .github/flake-ledger.json # merges new evidence into the file
redherring why "$RUN" --ledger .github/flake-ledger.json # uses it as extra history
The file is small JSON (one entry per failed job: test names, cause, commit, OS, link). Keep it anywhere durable. You can commit it, or carry it between scheduled runs with actions/cache:
steps:
- uses: actions/cache@v6
with:
path: flake-ledger.json
key: redherring-ledger-${{ github.run_id }}
restore-keys: redherring-ledger-
- uses: ixklo/redherring@v0.2.1
with:
command: scan
ledger: flake-ledger.json
Entries older than a year are dropped (--keep-days).
How it decides
redherring is deliberately strict about what it calls flaky. Only one kind of evidence counts: the same commit failed and then passed. That is what a re-run is. It does not guess from "failed on a PR, passed on main" or "this test fails a lot", because those are often real failures. A tool that cries wolf is worse than none.
Then, per failed job:
- Tests. The log is cleaned (timestamps, colour codes) and read by runner-specific parsers. Test ids keep only stable parts (file, suite, name; never line numbers or timings) so the same test lines up across commits and OSes.
- Infrastructure. If no test is named, the end of the log is matched against specific signatures (
ECONNRESET,Could not transfer artifact,The runner has received a shutdown signal,model is at capacity, ...). - Not flakiness at all. Roll-up jobs ("all required jobs passed") and PR policy checks are set aside and not counted.
Supported test output:
| ecosystem | runners |
|---|---|
| Python | pytest (incl. xdist, pytest-rerunfailures), unittest |
| JavaScript / TypeScript | Jest, Vitest (incl. workspaces), Playwright (incl. its own retries), Mocha, node:test (spec and TAP), Bun, TAP (QUnit/testem) |
| Go | go test, gotestsum, test timeouts |
| Rust | cargo test, cargo-nextest (incl. retries), failed tests: lists (Deno and other file-based runners) |
| Ruby | RSpec, Minitest |
| JVM | Maven Surefire, Gradle (incl. verbose test logging) |
| others | PHPUnit, .NET (dotnet test), CTest, XCTest, ExUnit |
Missing yours? A parser is one function and a test with a real log excerpt. See CONTRIBUTING.md.
Limits, honestly
- History has an expiry date. Job logs are kept for 90 days by default, and from 1 October 2026 GitHub deletes the workflow runs themselves once they pass the repo's log retention period (90 days by default, and at most 90 days for public repos).
--daysbeyond that finds nothing; use a ledger to keep what was found. - Only re-runs count. Repos that never press re-run, or retry inside the job without reporting it, show fewer flakes than they have. Playwright, nextest and pytest-rerunfailures retries are recognised when they appear in a failed job's log.
- The first scan of a big repo costs API requests. A busy monorepo can take a few thousand for 30 days (GitHub allows 5,000 an hour). redherring waits out rate limits on its own, and later scans are cheap: it remembers what each log said, and asks GitHub "has this page changed?" before re-downloading. GitHub answers unchanged pages with a free 304. On astral-sh/uv, a repeat 7-day scan took 4 requests instead of 169.
- Some jobs stay "unknown". In a study of 122 popular repos, nearly half of the red-herring jobs (46%) named neither a test nor a known infrastructure cause. Some logs genuinely say nothing ("exit 1"); others use output formats redherring doesn't parse yet.
whytreats those as "unclear", never as safe to re-run. Parser contributions fix this one format at a time. - GitHub Actions only, for now.
Privacy
redherring runs on your machine (or in your own Actions runner) and talks only to the GitHub API. It caches what it downloads in your user cache folder (%LOCALAPPDATA%\redherring, ~/Library/Caches/redherring, or ~/.cache/redherring; override with REDHERRING_CACHE). The cache holds job lists, what each log said, and API pages with their ETags, plus compressed copies of the logs so improved parsers can re-read them without downloading again. Logs are pruned after 120 days (GitHub deletes its own after 90). Set REDHERRING_KEEP_LOGS=0 to keep only what the logs said; the GitHub Action does this. Nothing is sent anywhere else.
License
MIT
Metadata
Release files for redherring 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| redherring-0.2.1.tar.gz | 72.7 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| redherring-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 120.8 kB
Release files / redherring-0.2.1.tar.gz
| Download URL | redherring-0.2.1.tar.gz |
|---|---|
| Size | 72.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0d1a0eb9a9f8ef524d6f41eb70c0a8fc96efe66e88dbc7fa06c8ad526ffaaf86
|
|
BLAKE2b-256 checksum How to use checksums |
1f11afd1b27157458ebdf245aa49df84dd3dee8ea4b6a60e7d57445301348b28
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.20 {"installer":{"name":"uv","version":"0.12.20","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / redherring-0.2.1-py3-none-any.whl
| Download URL | redherring-0.2.1-py3-none-any.whl |
|---|---|
| Size | 48.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
14563cd1373cde1a3dc086380b9c87865f07d9414c4bbe3cfd062d346134ab6e
|
|
BLAKE2b-256 checksum How to use checksums |
053835e49ff1f425d90022479a76ce5ced5e43755d68fa50c482d66af5a2d010
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.20 {"installer":{"name":"uv","version":"0.12.20","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for redherring 0.2.1
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.