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
1Find runs that needed a retry
Call
GET /repos/{owner}/{repo}/actions/runs?event=pull_request&created=>=<7 days ago>&per_page=100and keep runs withrun_attemptabove 1 andconclusion: success. Those passed on a later attempt of the same run, so the samehead_sha.2Read each attempt and its jobs
For each attempt,
GET /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}for itsconclusionandupdated_at, and.../attempts/{attempt_number}/jobsfor job names, conclusions,started_atandcompleted_at. A flake is a job that failed in one attempt and succeeded in the next on the same SHA.3Tie each flake to its PRs
Take the run's
pull_requestslist; when it is empty (forks), callGET /repos/{owner}/{repo}/commits/{head_sha}/pulls. Flakes on SHAs with no PR are counted in runner minutes but not in waiting time.- 4
4Add up the tax
Waiting per flake is the passing attempt's
updated_atminus the failed attempt'supdated_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
5Post the trend
Read last week's card with
GET https://briefkin.dev/v1/cards/insight-github-actions-flaky-ci-taxto extend the history, then post total hours waited as the headline and the job table when the card opens, under the sameitem_key. Post it to the board
With
BRIEFKIN_TOKENset, 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.
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.
shell
curl -fsSL https://briefkin.dev/skills/github-actions-flaky-ci-tax/install | shRun it
Ask Claude Code:
prompt
Run the github-actions-flaky-ci-tax skillOr paste the prompt instead of installing:
prompt
Using 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.Put it on a schedule
Mondays at 9:00, from cron on any machine where Claude Code is signed in:
crontab
0 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.Send it to your board
Connect Briefkin once with
curl -fsSL https://briefkin.dev/install | shand 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
ghsigned in, or a fine-grained token inGITHUB_TOKENwith 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
- GitHub MCP Server with the
actionstoolset, hosted athttps://api.githubcopilot.com/mcp/x/actions; append/readonlyfor read tools only. It lists runs and jobs and fetches job logs. - The
ghCLI.gh run list --branch main --status failure --json databaseId,workflowName,conclusion,createdAtfinds red runs, andgh run view <run-id> --log-failedprints only the failing steps' output. SetGH_TOKENfor unattended runs. - 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
- GitHub MCP Server, GitHub's official server, hosted at
https://api.githubcopilot.com/mcp/. Sign in with OAuth or a personal access token; add/readonlyto the URL, or use a toolset URL such ashttps://api.githubcopilot.com/mcp/x/pull_requests/readonly, to expose only read tools. It also runs locally (source). - The
ghCLI. Rungh auth loginonce, or setGH_TOKENfor scheduled runs.gh pr list --search "review-requested:@me" --json number,title,url,createdAtandgh api repos/{owner}/{repo}/dependabot/alertscover most reports. - 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.