Extract CI metrics and surface test failures from GitHub Actions logs.
Quick Start · Commands · Framework Support · Configuration · Examples
A gh CLI extension. gh bench stats pulls numeric values from CI logs and aggregates them (median, mean, p95). gh bench failures extracts structured test failures using framework-aware parsers.
# Install
gh extension install undont/gh-bench
# Aggregate a metric across recent runs
gh bench stats --workflow ci.yml --pattern 'duration=(?P<ms>[0-9.]+)ms'
# Surface test failures from a specific run
gh bench failures --runs 12345678Extract a numeric value from logs across multiple workflow runs and compute aggregations.
gh bench stats --workflow ci.yml --pattern 'median=(?P<ms>[0-9.]+)ms'
gh bench stats --workflow ci.yml --pattern 'duration=(?P<s>[0-9.]+)s' --agg median,p95,min,max
gh bench stats --runs 111,222,333 --pattern 'score=(?P<val>[0-9.]+)'| Flag | Short | Default | Description |
|---|---|---|---|
--workflow |
-w |
Workflow filename or name | |
--runs |
-r |
Comma-separated list of run IDs | |
--pattern |
-P |
Regex with a named capture group (?P<name>...). Optionally include (?P<label>...) for row context |
|
--preset |
-p |
Use a built-in pattern preset (see --list-presets) |
|
--list-presets |
-L |
List available pattern presets and exit | |
--limit |
-l |
10 |
Max number of runs to fetch |
--branch |
-b |
Filter runs by branch | |
--agg |
-a |
median |
Aggregations: median, mean, p95, min, max (comma-separated) |
--match |
-m |
first |
Which matches to extract per run: first, all |
--step |
-s |
Filter logs to a specific step name (substring match) | |
--concurrency |
-c |
5 |
Concurrent log fetchers |
--format |
-f |
table |
Output format: table, json, csv |
Either --pattern or --preset is required. Either --workflow or --runs is required.
Fetch failed CI runs, identify failing steps, and extract structured errors using framework-aware parsers. Each failure is annotated with the timestamp from the raw CI log, so you can see exactly when it occurred.
By default, only runs from branches with open pull requests are shown. Use --all to include all failed runs, or --branch to target a specific branch.
When a run has been re-run (multiple attempts), only the latest attempt's failures are shown. Steps from re-runs are annotated with the attempt number, e.g. Step: Run tests (attempt 2).
gh bench failures --workflow ci.yml
gh bench failures -w ci.yml --all
gh bench failures -r 12345678
gh bench failures -w ci.yml -b main -l 10
gh bench failures --runs 12345678 --format json
gh bench failures -w ci.yml --group| Flag | Short | Default | Description |
|---|---|---|---|
--workflow |
-w |
Workflow filename or name | |
--runs |
-r |
Comma-separated list of run IDs | |
--limit |
-l |
5 |
Max failed runs to fetch |
--branch |
-b |
Filter by branch (disables open-PR filtering) | |
--all |
-a |
false |
Include all failed runs, not just those with open PRs |
--group |
-g |
false |
Group identical failures across runs |
--exclude-step |
-x |
Exclude steps matching name (case-insensitive substring, repeatable) | |
--concurrency |
-c |
5 |
Concurrent log fetchers |
--format |
-f |
table |
Output format: table, json, csv |
Either --workflow or --runs is required.
The failures command auto-detects the test framework from log output and extracts structured failure information.
| Framework | Detection | Extracted Fields |
|---|---|---|
| .NET (xUnit, NUnit, MSTest) | Failed TestName [duration], error CS*:, summary lines |
Test name, duration, exception, assertion, stack trace location |
| Go | --- FAIL: TestName (duration), FAIL\tpackage |
Test name, duration, error message, file:line |
| Vitest / Jest | ✗ Suite > Test, FAIL *.test.tsx, or tsc diagnostics when running --typecheck |
Test name, error type, expected/actual, file:line:col |
| Fallback | No framework detected | Last 30 non-boilerplate lines, ##[error] messages |
The fallback parser strips GitHub Actions boilerplate (env vars, annotations, cleanup lines, shell script source) and prioritises ##[error] messages.
Create a .gh-bench.yml file in your repository root to set project-level defaults. CLI flags override config values when explicitly set.
# .gh-bench.yml
workflow: ci.yml
failures:
exclude-steps:
- "Check CI status"
- "Summary"| Key | Applies to | Description |
|---|---|---|
workflow |
stats, failures |
Default workflow (replaces --workflow flag) |
failures.exclude-steps |
failures |
Steps to exclude by name (replaces --exclude-step flag) |
The config file is discovered by walking up from the current directory to the nearest .git root.
$ gh bench stats --workflow ci.yml --pattern 'Total time:\s*(?P<seconds>[0-9.]+)s' --agg median,p95
RUN ID TITLE VALUE
23364348137 ACME-1338 Replace law firm ... 84.5
23348219428 ACME-1335 Add Roslyn analys... 91.0
23341291675 ACME-1332 Rename hooks ... 103.2
────────────────────────────────────────────────────────────────────
median: 91.0 p95: 103.2Using --match all with the go-test preset extracts every package duration and labels each row with the package name:
$ gh bench stats --preset go-test --workflow ci.yml --match all --limit 1
RUN ID LABEL VALUE
23402873321 audit 1.057
23402873321 cli 1.073
23402873321 lockfile 1.027
23402873321 scanner 166.842
23402873321 server 1.031
────────────────────────────────────────────────────────────
median: 1.1Labels come from the (?P<label>...) capture group in the pattern. When labels share a common path prefix, the table automatically strips it for readability. Full paths are preserved in --format json and --format csv output.
$ gh bench failures --runs 23341983210
● RUN 23341983210 — ACME-1335 Add Roslyn analyser (2026-03-20T12:03:31Z)
Step: Run integration-platform tests
Framework: dotnet
Failed Tests (1)
✗ Acme.IntegrationTests.TokenManagement.ExchangeCodeForTokens_ReturnsBadRequest [1 s] @ 20/03/26 12:03:45
Shouldly.ShouldAssertException : errors[0].Reason
should start with "Microsoft 365 authentication fai"
but was "An unexpected error occurred..."
at ExchangeCodeForTokensIntegrationTests.cs:line 181$ gh bench failures --runs 23341983210 --json | jq '.[].failures[].test_name'
"Acme.IntegrationTests.TokenManagement.ExchangeCodeForTokens_ReturnsBadRequest"git clone https://github.com/undont/gh-bench
cd gh-bench
make build # Build the binary
make install # Build and install as gh extension
make test # Run tests with race detection + coverage
make check # Run all checks (vet, lint, test)All Makefile targets
| Target | Description |
|---|---|
build |
Build the binary |
install |
Build and install as gh extension |
uninstall |
Remove the gh extension |
clean |
Remove build artefacts |
test |
Run tests with race detection and coverage |
cover |
Show coverage report in browser |
lint |
Run golangci-lint |
fmt |
Format all Go files |
vet |
Run go vet |
check |
Run all checks (vet, lint, test) |
Use FILTER="TestName" to run specific tests: make test FILTER=TestDotnet