Skip to main content

redherring: find the CI failures that weren't your fault

PyPI Python CI License: MIT GitHub Marketplace

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.

redherring scan on astral-sh/uv: 2,129 runs in 14 days, 280 went red, 94 of those (34%) turned green on a plain re-run; a table of named flaky tests, several Windows-only; flaky jobs grouped by cause

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.

Bar chart: share of red CI runs that were red herrings for the 20 highest of 69 repos with 50 or more red runs. astral-sh/uv 34%, microsoft/vscode 23%, forem/forem 20%, ollama/ollama 18%, bitcoin/bitcoin 17%, vercel/next.js 15%, mastodon/mastodon 15%. The median of the 69 is 3%.

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), or unknown.
  • 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

redherring why on a failed uv PR run: one test is a known flake (failed then passed on re-runs twice in 14 days), the other looks real

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.1.2
        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.1.2
        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.1.2
        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:

  1. 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.
  2. 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, ...).
  3. 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). --days beyond 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.
  • Big repos cost API requests. A busy monorepo can take a few thousand requests for 30 days. redherring waits out rate limits on its own, and caches everything immutable (attempt job lists, logs) so later runs only fetch what's new.
  • 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. why treats 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. Downloaded logs are cached, compressed, in your user cache folder (%LOCALAPPDATA%\redherring, ~/Library/Caches/redherring, or ~/.cache/redherring; override with REDHERRING_CACHE). Nothing is sent anywhere else.

License

MIT

Metadata

Release files for redherring 0.1.2

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

Source distribution (sdist)

Source distribution for redherring 0.1.2
File Size Uploaded Provenance
redherring-0.1.2.tar.gz 65.0 kB Without provenance Details

Built distribution (wheel)

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

Total release size: 109.5 kB

Release files / redherring-0.1.2.tar.gz

Download URL redherring-0.1.2.tar.gz
Size 65.0 kB
Tags Source
SHA-256 checksum
How to use checksums
6291b8c5014303d1ea2de5a8a40d292f74d68b2992dd3629a92fb25be02b93c9
BLAKE2b-256 checksum
How to use checksums
7d8627ae417af863f973a804eefed06219d51334c603efd48d6badcae499c389
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","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.1.2-py3-none-any.whl

Download URL redherring-0.1.2-py3-none-any.whl
Size 44.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e8f75ca4bff80e109f3bd5c63657b782e7bffc2d9c40a364181c7ef6fe5380f5
BLAKE2b-256 checksum
How to use checksums
80a44e48a8dcfa713540b144f0524e0b10d4d3dbb08b4b5ecc000d072696bb96
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","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.1.2

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.

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