See how many hours flaky GitHub Actions jobs cost your pull requests

Every Monday: the jobs that failed and then passed on the same commit, the PRs they held up, the hours waited and the runner minutes burned.

Runs
Mondays at 9:00
Posts
Stat tile

From your apps to your board

How the skill works: your agent reads runs that needed a retry from GitHub Actions; tie each flake to its prs from GitHub; repository from What you tell it, mondays at 9:00, and gives you hours prs waited on reruns and the report in the chat.

Reads

  • GitHub ActionsRuns that needed a retry
  • GitHubTie each flake to its PRs
  • What you tell itRepository
reads
Your agentClaude Code, Codex, Cursor or GeminiMondays at 9:00
posts

You get

  • Hours PRs waited on rerunsA stat tile on your board, with its trend
  • The report in the chatEvery run, with or without Briefkin

What you get

  • Hours PRs waited on reruns this week, against previous weeks.
  • Runner minutes spent on failed attempts and the number of PRs held up.
  • Per job: flakes, PRs, hours waited and runner minutes, worst first.
  • One line on what to do first, for example "Quarantine e2e-checkout: 14 flakes, 6.4 hours".

The card on the right is a sample. Yours shows your own numbers and updates in place every run.

What lands on your board

acme-webHours PRs waited on rerunsLast 7 days9.1h+49% vs previous period, worsevs last report
Runner minutes290+31% vs previous period, worse
PRs held up12+33% vs previous period, worse
Flaky reruns21+40% vs previous period, worse
An example with made-up numbers. Your agent posts your real ones.

What it does

For developers and DevOps leads who know CI is flaky but cannot say how much it costs. GitHub Actions records every attempt of every run; the pull request page only shows the final green check. The hour between a red e2e job and its passing rerun disappears.

The agent finds runs that failed and passed on retry with no code change, ties them to the PRs on that commit, and adds up how long those PRs waited and how many runner minutes the failed attempts used. The job at the top of the list is the one to quarantine or fix this week.

How it works

  1. 1Find runs that needed a retry

    Call GET /repos/{owner}/{repo}/actions/runs?event=pull_request&created=>=<7 days ago>&per_page=100 and keep runs with run_attempt above 1 and conclusion: success. Those passed on a later attempt of the same run, so the same head_sha.

  2. 2Read each attempt and its jobs

    For each attempt, GET /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number} for its conclusion and updated_at, and .../attempts/{attempt_number}/jobs for job names, conclusions, started_at and completed_at. A flake is a job that failed in one attempt and succeeded in the next on the same SHA.

  3. 3Tie each flake to its PRs

    Take the run's pull_requests list; when it is empty (forks), call GET /repos/{owner}/{repo}/commits/{head_sha}/pulls. Flakes on SHAs with no PR are counted in runner minutes but not in waiting time.

  4. 4

    4Add up the tax

    Waiting per flake is the passing attempt's updated_at minus the failed attempt's updated_at, capped at 24 hours so a weekend does not dominate. Runner minutes are each failed job's duration rounded up to the whole minute, as GitHub counts it. Group by job name: flakes, distinct PRs, hours waited, runner minutes.

  5. 5

    5Post the trend

    Read last week's card with GET https://briefkin.dev/v1/cards/insight-github-actions-flaky-ci-tax to extend the history, then post total hours waited as the headline and the job table when the card opens, under the same item_key.

  6. Post it to the board

    With BRIEFKIN_TOKEN set, the agent sends the result as one card that updates in place every run. Without it, you get the report in the chat.

Install and run it

Pick your agent, where the data comes from and how to schedule it; the commands below follow.

Agent
Data
Schedule
  1. Install the skill

    One command. It puts the SKILL.md in ~/.claude/skills and in ~/.agents/skills, which Codex, Cursor, Gemini CLI, Copilot CLI and OpenCode read too.

    shellcurl -fsSL https://briefkin.dev/skills/github-actions-flaky-ci-tax/install | sh
  2. Run it

    Ask Claude Code:

    promptRun the github-actions-flaky-ci-tax skill

    Or paste the prompt instead of installing:

    promptUsing the GitHub API for <owner/repo>, list pull_request workflow runs from the last 7 days with run_attempt above 1 that ended in success. For each, read every attempt and its jobs; a flake is a job that failed in one attempt and passed in the next on the same head SHA. Find the PRs on that SHA (the run's pull_requests, or commits/{sha}/pulls). For each flake, the waiting time is the passing attempt's updated_at minus the failed attempt's updated_at, capped at 24 hours; runner minutes are the failed job's duration rounded up to the minute. Give me total hours PRs waited, runner minutes and distinct PRs held up, then a table per job (flakes, PRs, hours, runner minutes), worst first. End with the one job to fix or quarantine first.
  3. Put it on a schedule

    Mondays at 9:00, from cron on any machine where Claude Code is signed in:

    crontab0 9 * * 1 claude -p "Run the github-actions-flaky-ci-tax skill"

    Add it with crontab -e. A laptop asleep at that time runs it when it wakes if you use launchd or Task Scheduler instead; GitHub Actions works for the API route.

  4. Send it to your board

    Connect Briefkin once with curl -fsSL https://briefkin.dev/install | sh and every run lands on your board as a stat tile. How setup works

View the SKILL.mdRaw file
SKILL.md---
name: github-actions-flaky-ci-tax
description: "Every Monday: the jobs that failed and then passed on the same commit, the PRs they held up, the hours waited and the runner minutes burned. Use when the user asks for this, or when it runs on a schedule."
---

# Flaky CI tax: GitHub Actions reruns and the PRs they held up

Every Monday: the jobs that failed and then passed on the same commit, the PRs they held up, the hours waited and the runner minutes burned.

For developers and DevOps leads who know CI is flaky but cannot say how much it costs. GitHub Actions records every attempt of every run; the pull request page only shows the final green check. The hour between a red e2e job and its passing rerun disappears.

The agent finds runs that failed and passed on retry with no code change, ties them to the PRs on that commit, and adds up how long those PRs waited and how many runner minutes the failed attempts used. The job at the top of the list is the one to quarantine or fix this week.

## Two ways to get the data

Use the API or MCP route when you have an MCP server, a CLI or an API token for the app. Otherwise, if you can drive a browser the user is signed in to (Claude in Chrome, a Playwright or Chrome DevTools MCP server), use the browser route. Say which one you used.

## Route A: API or MCP server

### What you need

- `gh` signed in, or a fine-grained token in `GITHUB_TOKEN` with Actions: Read, Pull requests: Read and Metadata: Read on the repositories.
- CI triggered on `pull_request`, so runs carry the PR and its head SHA; reruns done with "Re-run jobs" rather than an empty commit, which changes the SHA and hides the flake.

If something is missing, say what and stop; do not invent numbers.

### Steps

1. **Find runs that needed a retry** (GitHub Actions): Call `GET /repos/{owner}/{repo}/actions/runs?event=pull_request&created=>=<7 days ago>&per_page=100` and keep runs with `run_attempt` above 1 and `conclusion: success`. Those passed on a later attempt of the same run, so the same `head_sha`.
2. **Read each attempt and its jobs** (GitHub Actions): For each attempt, `GET /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}` for its `conclusion` and `updated_at`, and `.../attempts/{attempt_number}/jobs` for job names, conclusions, `started_at` and `completed_at`. A flake is a job that failed in one attempt and succeeded in the next on the same SHA.
3. **Tie each flake to its PRs** (GitHub): Take the run's `pull_requests` list; when it is empty (forks), call `GET /repos/{owner}/{repo}/commits/{head_sha}/pulls`. Flakes on SHAs with no PR are counted in runner minutes but not in waiting time.
4. **Add up the tax**: Waiting per flake is the passing attempt's `updated_at` minus the failed attempt's `updated_at`, capped at 24 hours so a weekend does not dominate. Runner minutes are each failed job's duration rounded up to the whole minute, as GitHub counts it. Group by job name: flakes, distinct PRs, hours waited, runner minutes.
5. **Post the trend**: Read last week's card with `GET https://briefkin.dev/v1/cards/insight-github-actions-flaky-ci-tax` to extend the history, then post total hours waited as the headline and the job table when the card opens, under the same `item_key`.

## Route B: the browser

### What you need

- Signed in to GitHub with read access to the repository and its Actions runs.

If something is missing, say what and stop; do not invent numbers.

### Steps

Start at https://github.com/. The user is signed in already; never type passwords or codes, and ask if a sign-in page appears.

1. **Open the repository's Actions tab**: Go to the repository, click **Actions** and filter the run list by event `pull_request`. Look at the last 7 days of runs.
2. **Find runs that passed on a retry**: Open green runs; a run that was re-run shows an attempt picker at the top of its summary page. Switch to the earlier attempt and note which job failed and when it finished, then the time the passing attempt finished.
3. **Note the PR and the job time**: The run summary links the pull request. Open the failed job in the earlier attempt to read its duration, and round it up to the whole minute.
4. **Add it up**: Per job name, count the flakes, the distinct PRs, the hours between the failed and passing attempt (capped at 24 hours each), and the runner minutes. Sort by hours waited.

Note: The Actions list has no filter for rerun runs, so the agent has to open each green run, which is slow past a few dozen runs a week. For busier repositories the API route is the one to use; the browser route works as a spot check on one workflow.

## The report

- **Hours PRs waited on reruns** this week, against previous weeks.
- **Runner minutes** spent on failed attempts and the number of PRs held up.
- **Per job**: flakes, PRs, hours waited and runner minutes, worst first.
- One line on what to do first, for example "Quarantine e2e-checkout: 14 flakes, 6.4 hours".

## Post it to Briefkin

When `BRIEFKIN_TOKEN` is set, or `~/.config/briefkin/env` exists (the installer from https://briefkin.dev/docs writes it), also send the result to the user's Briefkin board as one card. Keep the `item_key` the same on every run so the card updates in place, use a new `event_id` per run, and put today's real numbers in place of the example ones. If the request fails, say so once and carry on.

```bash
[ -n "${BRIEFKIN_TOKEN:-}" ] || . ~/.config/briefkin/env
curl -sS -X POST "${BRIEFKIN_URL:-https://briefkin.dev}/v1/events" \
  -H "authorization: Bearer $BRIEFKIN_TOKEN" \
  -H "content-type: application/json" \
  -d @- <<'JSON'
{
  "v": 1,
  "event_id": "github-actions-flaky-ci-tax/<date of this run, e.g. 2026-10-16>",
  "item_key": "github-actions-flaky-ci-tax",
  "project": "acme-web",
  "source": "Flaky CI tax: GitHub Actions reruns and the PRs they held up skill",
  "kind": "metric",
  "title": "Flaky e2e-checkout cost 6.4 hours of PR waiting",
  "summary": "Flaky **e2e-checkout** cost **6.4 hours** of waiting and **212 runner minutes** across **9 PRs**.",
  "metric": {
    "label": "Hours PRs waited on reruns",
    "value": 9.1,
    "unit": "h",
    "period_label": "Last 7 days",
    "better": "lower",
    "period_start": "<start of the period, ISO 8601>",
    "period_end": "<end of the period, ISO 8601>"
  },
  "glance": [
    {
      "type": "Grid",
      "props": {
        "columns": 3
      },
      "children": [
        {
          "type": "Stat",
          "props": {
            "label": "Runner minutes",
            "value": 290,
            "delta": 31,
            "better": "lower"
          }
        },
        {
          "type": "Stat",
          "props": {
            "label": "PRs held up",
            "value": 12,
            "delta": 33,
            "better": "lower"
          }
        },
        {
          "type": "Stat",
          "props": {
            "label": "Flaky reruns",
            "value": 21,
            "delta": 40,
            "better": "lower"
          }
        }
      ]
    }
  ],
  "widget": [
    {
      "type": "Table",
      "props": {
        "columns": [
          "Job",
          "Flakes",
          "PRs",
          "Hours waited",
          "Runner min"
        ],
        "align": [
          "left",
          "right",
          "right",
          "right",
          "right"
        ],
        "rows": [
          [
            "e2e-checkout",
            14,
            9,
            6.4,
            212
          ],
          [
            "integration-db",
            5,
            4,
            1.9,
            58
          ],
          [
            "visual-regression",
            2,
            2,
            0.8,
            20
          ],
          [
            "All jobs",
            21,
            12,
            9.1,
            290
          ]
        ]
      }
    },
    {
      "type": "Callout",
      "props": {
        "text": "Quarantine or fix e2e-checkout first: 11 of its 14 failures were in the same step, Pay with test card. PRs count once in the total when two jobs held them up.",
        "tone": "warn"
      }
    }
  ],
  "size": "md"
}
JSON
```

Widgets come from the Briefkin catalog (https://briefkin.dev/v1/catalog). Without a token, just give the user the report.

Set `period_start` and `period_end` to the window the number covers: the tile names it from them ("Last 7 days", "Today", "This month"). For a count right now (open items, a balance), send `"period_label": ""` so no period shows; for another name, send it as `period_label` (at most 24 characters).

## Run it on a schedule

Suggested: Mondays at 9:00 (`0 9 * * 1`). From cron, with the agent signed in on that machine:

```bash
0 9 * * 1 claude -p "Run the github-actions-flaky-ci-tax skill"
# Codex: codex exec, Gemini CLI: gemini -p, Copilot CLI: copilot -p, OpenCode: opencode run
```

Or use the agent's own scheduler (Claude Code routines, Codex automations, Claude or ChatGPT scheduled tasks): https://briefkin.dev/skills/github-actions-flaky-ci-tax#schedule

Source: https://briefkin.dev/skills/github-actions-flaky-ci-tax

What your agent needs

  • gh signed in, or a fine-grained token in GITHUB_TOKEN with Actions: Read, Pull requests: Read and Metadata: Read on the repositories.

  • CI triggered on pull_request, so runs carry the PR and its head SHA; reruns done with "Re-run jobs" rather than an empty commit, which changes the SHA and hides the flake.

Connecting GitHub Actions

  1. GitHub MCP Server with the actions toolset, hosted at https://api.githubcopilot.com/mcp/x/actions; append /readonly for read tools only. It lists runs and jobs and fetches job logs.
  2. The gh CLI. gh run list --branch main --status failure --json databaseId,workflowName,conclusion,createdAt finds red runs, and gh run view <run-id> --log-failed prints only the failing steps' output. Set GH_TOKEN for unattended runs.
  3. REST API with a fine-grained token in GITHUB_TOKEN, scoped to the repositories you watch, with Actions: Read and Metadata: Read.

Actions: Read lets the agent list runs and download logs but not rerun, cancel or trigger workflows; grant write only if you want it to retry flaky jobs.

Connecting GitHub

  1. GitHub MCP Server, GitHub's official server, hosted at https://api.githubcopilot.com/mcp/. Sign in with OAuth or a personal access token; add /readonly to the URL, or use a toolset URL such as https://api.githubcopilot.com/mcp/x/pull_requests/readonly, to expose only read tools. It also runs locally (source).
  2. The gh CLI. Run gh auth login once, or set GH_TOKEN for scheduled runs. gh pr list --search "review-requested:@me" --json number,title,url,createdAt and gh api repos/{owner}/{repo}/dependabot/alerts cover most reports.
  3. REST and GraphQL with a fine-grained token. Store it in GITHUB_TOKEN, limit it to the repositories in the report, and grant Read on Metadata, Contents, Pull requests and Issues, plus Dependabot alerts if you report on them.

With every permission on a fine-grained token set to Read, the agent can't push, merge, comment or close anything.

Questions

Does it catch a flake when someone pushes an empty commit to retrigger CI?

No. An empty commit has a new SHA, so it looks like a new run, not a retry. The skill only counts reruns of the same run on the same commit, which is the clean signal. If your team retriggers by pushing, the tax is higher than the card says.

Are the runner minutes what GitHub bills?

They follow GitHub's rule of rounding each job up to the minute, but they are raw minutes. Windows and macOS runners count at a multiple of Linux minutes against included minutes, larger runners are billed per minute at their own rate, and public repositories and self-hosted runners are not billed at all.

Do I need Briefkin to use the Flaky CI tax: GitHub Actions reruns and the PRs they held up skill?

No. The skill runs with your own agent and gives you the report in the chat. With a Briefkin token set, each run also posts it to your board as a stat tile, so you can glance at it instead of asking again.

Which agents can run it?

Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot CLI, OpenCode, Windsurf and Amp all read SKILL.md files. The install command puts it in ~/.claude/skills and ~/.agents/skills, where they look. ChatGPT, the Claude app and Grok take the prompt instead. Pick your agent under "Install and run it" and the commands change to match.

Can it work without an API or MCP server, from my browser?

Yes. Switch the data source to "Your browser" and the steps change to the GitHub Actions and GitHub pages themselves: the agent opens github.com in a browser you are signed in to and reads the numbers off the page. Claude Code, Codex, Cursor, Claude app and ChatGPT can drive a browser out of the box; Gemini CLI, Copilot CLI and OpenCode need a Playwright or Chrome DevTools MCP server first. It is slower than the API and needs you signed in, so it suits a report you run by hand or from a scheduler on your own computer.

Watch hours prs waited on reruns on your board, run after run

Skills are free to use with any agent. Briefkin is where their results land: one board for every report, number and question your agents send, with a ping when something needs you.

Free during the open beta. No card.