Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ci-audit

See where your CI time actually goes — for any GitHub repo, in one command.

npx ci-audit vercel/next.js
  ci-audit  vercel/next.js
  last 200 runs · 11.6 hours · Mon 27 Jul · push and PR triggers

  ⏱  28m 41s median wait per commit  ·  45m 22s at p95
     348 CI-hours across 3884 jobs
     43 commits · 200 runs · 4.7 workflow runs per push

  💸 $1,406 billed in this window — 3284 larger-runner jobs, billed even on public repos

  SLOWEST JOB FAMILIES          shards    total      p50    share
  ────────────────────────────────────────────────────────────────
  test prod                         10  57h 32m  12m 18s      17%
  test dev                          10  43h 36m   9m 05s      13%
  test turbopack production          7  40h 05m  11m 51s      12%
  top 8 = 73% of CI time · 90 more families = 27% (161 job variants total)

  🎯 critical path (inferred from timing · 42% of commits)
     build (1m 53s) → Stats (10m 19s)  =  12m 12s
     → 43% of the 28m 41s median wait is these 2 jobs in sequence
     → Stats is on the path in 67% of commits — a minute saved there is a minute saved everywhere

  ♻️  5 superseded runs ran to completion on commits a newer push had already replaced
     0.8 CI-hours (0% of total, $1.54) — a concurrency block with cancel-in-progress reclaims it

  INSIDE  test unit windows   (p50 per step)
  ────────────────────────────────────────────────────────────────
  ▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░  Run node run… --type unit  3m 49s   53%
  ▓▓▓▓░░░░░░░░░░░░░░░░░░  Run pnpm install            1m 14s   17%
  ▓▓▓░░░░░░░░░░░░░░░░░░░  Cache pnpm store            1m 00s   14%
                          + 38 steps under 1%
                          44 steps in one job — worth a look on its own

  MOST FAILING                      runs  failed   rate  burned      $
  ────────────────────────────────────────────────────────────────
  Rspack inte…evelopment)           16      15    94%  2h 32m    $10
  5 families fail more than half the time, burning 4h 52m of runner time ($20)

Four things nothing else gives you:

  • The critical path, reconstructed from timing. Your slowest job is 11m but your median wait is 30m — the difference is needs: edges running jobs in sequence. ci-audit infers those edges from job start and end times (a job starting as another finishes is gated on it) and prints the chain, so you get build → test → publish = 13m 50s, 46% of your median wait without fetching or parsing a single line of workflow YAML.
  • Job families, not shards. test (1/10) through test (10/10) are one logical job. Split, each looks like 1% of your CI and nothing looks worth fixing. Grouped, the real bottleneck usually turns out to be 15–25% of total time.
  • Per-step timing. Which job is slow is a starting point. Which line of your workflow YAML is slow is a ticket.
  • Wait, not fleet hours. Aggregate CI-hours is a platform-team metric. ci-audit leads with the median wait from push to green, measured per commit across every workflow the push fired.

Plus two findings that are usually free money: jobs that fail more often than they pass (with what their failures cost in runner time and dollars), and runs that kept going after a newer push had already replaced them — which a concurrency block reclaims in four lines of YAML.

Usage

ci-audit <owner/repo> [options]

--days <n>       cover the last n days (spreads the sample evenly across them)
--runs <n>       runs to analyze (default 200, max 1000)
--sample         quick pass, stays under the 60 req/hr unauthenticated limit
--branch <name>  only runs on this branch
--workflow <f>   only this workflow (file name, e.g. ci.yml)
--events <list>  only these triggers (default: push, pull_request, pull_request_target,
                 merge_group; "all" keeps cron and bot triggers too)
--ungrouped      don't collapse matrix shards into job families
--inside <job>   which job to break down per step (default: slowest visible one)
--json           machine-readable output
--markdown       markdown output
--token <t>      GitHub token

--days 30 --runs 300 is usually what you want: a month of history, sampled evenly across it, for the cost of 300 requests. Plain --runs 300 on a busy repo may only cover a few hours.

Token

You need one — the Actions API allows 60 req/hr unauthenticated and one audit is ~300 requests. In order of preference:

  1. Nothing to do if you've run gh auth login — the token is auto-detected.
  2. GITHUB_TOKEN / GH_TOKEN in the environment.
  3. --token <t>.

No token at all still works via --sample (50 runs), but percentiles get noisy.

What it can't see

Being explicit, because a CI report you can't trust is worse than none:

  • Inside composite actions. If one step is a composite action or one large script, the API reports it as a single step. ci-audit detects this, says so, and breaks down the slowest job it can see into instead of printing a 98% bar and seven 0% rows.

  • Self-hosted runners. Excluded from cost entirely and labelled. Their time is still counted.

  • Cache hit rate. Not exposed by any API — only derivable by parsing multi-MB step logs. Not attempted.

  • Billed vs wall-clock time. completed_at − started_at excludes some runner overhead, so it won't exactly match your invoice. Step timings are share of wall-clock, never billed minutes.

  • Flakes hidden by retry actions. nick-fields/retry retries inside one attempt, so the run never looks rerun. Only ~3% of runs are ever rerun, so ci-audit adds a second signal — the same commit passing in one run and failing in another — plus a plain failure-rate table that always has data. An empty flaky table means "no evidence", not "no flakes".

  • Dollar amounts on public repos. Standard hosted runners are free for public repos, so the real number is $0. Larger runners are billed on public repos, and that measured figure is the headline; the ~$X/mo if private number is hypothetical and marked with ~.

  • True needs: edges. The critical path is inferred from timing, not read from your YAML. A job that happens to start as another finishes will be treated as depending on it. The chain is right often enough to act on and is labelled with the share of commits it matched — treat it as a strong hint, not a graph.

  • CI that isn't in Actions. Only triggers a developer blocks on are sampled, because a repo with a cron firing every few minutes can fill all 1000 runs the API will page through and leave no room for the real CI. If every job in the window finishes in under a minute, ci-audit says so outright instead of presenting label bots as your test suite.

Two deliberate exclusions from the wait calculation, both verified against real repos: reruns (a rerun clicked two days later would report as a 49-hour wait) and workflows fired more than six hours after the push (nightly cron reuses the commit's sha).

Development

npm install
npm test
node src/cli.ts owner/repo   # Node 24+, runs TypeScript directly
npm run build

MIT.

About

See where your CI time actually goes — for any GitHub repo, in one command.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages