Releases: linnea-bakshi/gha-doctor
Release list
v0.49.0
v0.49.0 — CI health as a Grafana dashboard
--prom: Prometheus text-format export
gha-doctor --prom ci-health.prom # alongside the normal report
gha-doctor --fail-on never --prom - # metrics to stdout, report-only exitEvery measured aggregate, as gauges: health score, findings by severity,
per-workflow success ratios and p50/p95 durations, queue time, wasted and
rounded-up compute (seconds and USD), flaky jobs, zombie crons, cache
size against the 10 GB limit, superseded-run waste, PR feedback time.
Run it on a schedule — node_exporter textfile collector or a Pushgateway
push from a scheduled workflow (README has both wirings) — and CI health
becomes a dashboard with real history instead of a point-in-time report.
The honesty rules carry over from --json:
- an unmeasured section (no history sample, cache API unavailable, too
few qualifying PR pushes) emits no series at all — a gap on the
dashboard is the truth; a zero-filled series would be a lie; - a measured zero (zero flaky jobs across a sampled window) is a real
0— "we looked, it's zero" is information; - success ratios / duration percentiles for workflows with no decisive
runs are undefined, so those series are absent rather than fake; gha_doctor_sample_since_timestamp_secondsstates the sample window,
gha_doctor_last_run_timestamp_secondssupports staleness alerts, and
gha_doctor_runs_missing_job_datasays loudly when job-derived gauges
understate (docs/honesty.md).
Details: label values escaped per the exposition format (output verified
against the official prometheus_client parser); - writes to stdout but
is loudly skipped under --json/--sarif (machine-readable stdout stays
pure, same rule as --annotate); refuses --run/--org/--fix/--diff
(different or no aggregates) and --workflow (a scoped sample must never
wear whole-repo labels); the exit-2 CI gate is unchanged.
Action: fail-on input
The GitHub Action now forwards fail-on: any|warning|never to the CLI —
never for report-only scheduled dashboard jobs, any to enforce
info-level advice too. Version-gated: pins older than v0.48.0 get a loud
skip note instead of a flag error. Note: fail-on: never zeroes the exit
code, so the findings output reads false; use fail-on-findings when
you consume that output.
Install / upgrade: brew, scoop, gh extension upgrade gha-doctor, docker
(ghcr.io/linnea-bakshi/gha-doctor), deb/rpm/apk, aqua, asdf, mise/ubi, or
the checksummed binaries below.
v0.48.0
What's new
--fail-on — choose what fails the build
The exit-2 CI gate has always tripped on warning-severity findings only; info-level advice was reported but never failed a build. That policy is now explicit and yours to set:
gha-doctor --fail-on any # every finding gates
gha-doctor --fail-on warning # the default, unchanged
gha-doctor --fail-on never # report-only: findings never change the exit code
never is for scheduled dashboard jobs that shouldn't go red over findings you already know about; any is for shops that want info-level advice (D014, D017, D021…) enforced too. The repo can state the policy once in .gha-doctor.yml:
fail-on: neverAn explicit flag beats the file. A typo'd value exits 1 (usage error) or draws a loud config warning — it can never silently weaken or tighten the gate. Aliases info/warn/none are accepted; shell completion suggests the canonical three; the config file's published JSON Schema knows the new key.
Fixed
--explainoutput (and every consumer of the embedded rule docs) broke when gha-doctor was built from a Windows checkout — git'sautocrlfrewrote the embeddeddocs/rules.mdto CRLF. Embedded docs are now normalized at startup, and.gitattributespins the file to LF (released binaries andgo installbuilds were never affected).
Internal
- The full test suite now runs natively on Windows and macOS in CI on every push (which is how the CRLF bug above was caught).
v0.47.2
Honesty patch for history analyses that outlive their API budget.
When per-job fetches failed partway through an analysis — the classic case is the unauthenticated 60 requests/hour limit running out mid-sample — runs without job data were silently treated as "zero job minutes". Verified live before the fix: a tokenless gha-doctor --repo psf/requests --runs 100 priced the Tests workflow at $28.75 vs $41.60 with full data, with no warning anywhere.
Now:
- The report says exactly how many sampled runs lack job data, in every output mode: a ⚠ line under the Run history header (terminal/
--md/--html), andanalysis.job_data_missing+analysis.job_data_notein--json(report schema regenerated). - The health score's basis discloses that flakiness/waste deductions can only be too generous with job data missing.
- If no sampled run has job data, that's now an error (exit 1) instead of a report full of zeros — the old empty-map guard could never fire because failed runs still set their map entry.
- Rate-limit failures word the note with the reset time and the
GITHUB_TOKENhint.
New honesty-gates section in docs/honesty.md; regression tests cover both partial and total failure.
v0.47.1
Packaging follow-up to v0.47.0's MCP server:
- gha-doctor is now in the official MCP Registry as
io.github.linnea-bakshi/gha-doctor. The ghcr.io image carries the registry's ownership-verification label, and every future release publishes its registry entry automatically (GitHub OIDC, no secrets). - Registry clients can run the server straight from the container image:
docker run -i --rm ghcr.io/linnea-bakshi/gha-doctor:latest --mcp(passGITHUB_TOKENthrough with-efor history analysis).
No CLI behavior changes.
v0.47.0
MCP server: let your AI agent run the doctor
gha-doctor --mcp runs as a Model Context Protocol stdio server, so Claude Code, Cursor, and any other MCP client can diagnose CI mid-conversation — "why is CI slow on this repo?", "which tests are flaky?", "what would gha-doctor fix here?".
# Claude Code
claude mcp add gha-doctor -- gha-doctor --mcpSix tools, all read-only — the server reports and previews but never writes; applying fixes stays an explicit gha-doctor --fix in your shell:
| Tool | What it does |
|---|---|
analyze_repo |
full health report: lint + history + flaky/waste/cost + score + top wins |
lint_repo |
static rules only, on any GitHub repo or a local directory (offline) |
preview_fixes |
the exact --fix diff, applied nowhere |
run_deep_dive |
one run: waterfall, step regressions, failing tests, log tail |
org_overview |
fleet triage across an org's busiest repos |
explain_rule |
full documentation for a rule ID |
Design notes:
- Output can't drift from the CLI: each tool invokes this same binary in
--mdmode, so what the agent reads is byte-identical to what you'd see in your terminal — including the honest caveats, which land in aNotesfooter. - Both protocol eras: the classic
initializehandshake (2025-06-18, 2025-11-25) and the stateless 2026-07-28 revision (server/discover, per-request_metaversioning, proper-32022on version mismatch). Verified against the official MCP Inspector. - Well-behaved under load: tool calls run concurrently (a 30-second analysis never blocks pings),
notifications/cancelledactually cancels, per-tool timeouts, 1 MiB output cap. - The server inherits your environment:
GITHUB_TOKEN/ghauth for history analysis, nothing for offline lint. Zero new dependencies.
Install/upgrade: brew install linnea-bakshi/tap/gha-doctor · gh extension install linnea-bakshi/gh-doctor · all other channels
v0.46.1
Robustness patch, found by CI's own 20-second fuzz smoke minutes after
v0.46.0 shipped.
--fix: the lone-CR guard now runs before CRLF normalization. A
file containing \r\r\n holds an isolated carriage return — a line
break to the YAML parser, invisible to a \n-split line array, so
every node position past it is off by one. The guard for exactly this
case existed, but in files where every \n was part of a \r\n pair
the CRLF normalization ran first and ate the \r\n, leaving \r+\n
— which then masqueraded as a valid pair and slipped past the guard.
Result: a planned insert landed on the wrong line, produced invalid
YAML, and the safety valve refused the whole file with a "bug" error
instead of a clean skip note. Nothing was ever written to disk (the
valve did its job); the failure mode was a spurious error on
pathological files.
The guard now checks the original bytes. The crasher is committed to
the fuzz seed corpus with a named regression test; full suite plus
extended local fuzzing are clean.
v0.46.0
New rule: D021 UnguardedCron
Scheduled workflows don't stay in your repo: every fork carries a copy,
and once a fork owner enables Actions (commonly to test a CI change),
your crons start running there too — typically failing on missing
secrets, or worse, running issue/PR automation (stale bots, lock bots,
nightly publishes) against the fork. D021 (info) flags workflows
with an on: schedule trigger whose jobs have no repository guard:
jobs:
nightly:
if: github.repository == 'your-org/your-repo' # forks skip cleanlyDetails, honestly scoped:
- A job counts as guarded when its
if:mentionsgithub.repository
(slug or owner comparison),github.event.repository.fork, or scopes
bygithub.event_name— and a job thatneeds:a guarded job is
effectively guarded too (skipped needs skip their dependents). - Info, not warning: GitHub disables scheduled workflows in fresh
public forks by default, so the leak needs a fork owner to flip
Actions on. That's one click, and it's routinely clicked. - Not auto-fixed, deliberately: the guard needs your repository's
slug (the file doesn't contain it), and merging into an existing
if:changes its semantics. One-line hand edit. - Swept live before shipping: fires on react's unguarded nightly
npm-publish cron, on 10 transformers workflows, on cli/cli and
grafana; reads silent on pytorch, cpython, rust, node and django —
the repos that guard their crons — which is what makes it credible.
Docs: rules.md#d021-unguardedcron.
The playground sample demos it.
Also in this release: the repo config-file JSON Schema's rule enum
includes D021 (regenerated), and gha-doctor's own badge/scoreboard cron
workflows now carry the guard themselves.
v0.45.0
D020: runner labels with an announced retirement — and --fix learns runner labels
New rule: D020 DeprecatingRunnerLabel (warning)
GitHub has scheduled the retirement of two hosted runner images that are
everywhere right now:
| label | brownouts start | fully unsupported | move to |
|---|---|---|---|
ubuntu-22.04 |
September 17, 2026 | April 17, 2027 | ubuntu-24.04 |
macos-14 (+ -large/-xlarge) |
July 6, 2026 (already underway) | November 2, 2026 | macos-15 / macos-26 |
D020 flags them now, while you can migrate on your own schedule instead of
during a brownout window. Detection is identical to D016: scalar runs-on:,
label lists, and ${{ matrix.KEY }} resolution through axis and include:
values — complex expressions are never guessed at.
--fix for D016 + D020 (Ubuntu labels only)
--fix now bumps retired (ubuntu-16.04/18.04/20.04) and retiring
(ubuntu-22.04) labels to ubuntu-24.04 — a same-architecture, mechanical
label swap whose target became unambiguous the moment 22.04's retirement was
scheduled. Deliberate limits, each with a loud skip note instead of an edit:
- Windows —
windows-2022vswindows-2025is your call. - macOS — newer images change Xcode majors (and, coming from
macos-13
or older, CPU architecture). - Matrix values —
${{ matrix.os }}values may be referenced inif:/
include:/exclude:expressions the linter can't see; rewriting them
could silently change logic.
Both fix paths resolve through the rules' own label tables, so fix and
finding stay in lockstep by construction. Multiple fixable labels on one
flow-style line merge into a single edit. 10 of 20 rules are now
auto-fixable. --diff previews all of it, locally or against a remote
repo.
Also in this release
- D016's advice was refreshed: it still recommended
macos-14, which dies
in November — replacements are nowmacos-15/macos-26and
ubuntu-24.04. - The rules-reference summary table was missing D019's row (docs bug).
- The playground
sample now demos both new behaviors.
v0.44.0
Duration trend — is the build getting slower?
The run-history report now answers the question every team asks eventually:
"CI feels slower lately — is it?"
Duration trend (p50 of successful runs, older vs newer half of the sample)
▲ CI — p50 16.2m → 28.2m (+74% across 2d; 8 vs 8 runs)
2 other measured workflows show no significant change
- Compares each workflow's p50 duration of successful runs in the older
half of the sample against the newer half. Failures are excluded — they
stop early or get retried, so mixing them in would make any "trend" an
artifact of the failure mix, not of the build. - Honesty gates (docs/honesty.md):
a workflow is only measured with 12+ successful runs spanning 24+
hours, and a change is only reported past both a 20% and a
1-minute p50 shift. Measured-but-stable workflows are counted out loud
("no significant p50 change across N measured workflows") instead of
disappearing; workflows that can't be measured honestly get no section
at all. - A 30%+ slowdown earns an unquantified "Investigate the CI
slowdown" slot in the top wins. No dollar figure on purpose: those
minutes are already inside the cost totals, and pricing them twice would
inflate the ledger. - Everywhere: terminal section,
--mdtable,--json
(analysis.duration_trends, schema regenerated),--html.
Seen live while shipping: prometheus/prometheus's CI p50 went 16.2m → 28.2m
(+74%) inside a 2-day sample; vitejs/vite's went 9.6m → 4.9m — trends run
both ways, and the report says which.
v0.43.0
--workflow — scope the whole history analysis to one workflow.
New
-
--workflow ci.ymlrestricts the run sample and the static findings
to a single workflow: its flakes, its cost, its slowest steps, its shard
balance, its zombie cron. Works everywhere the history analysis does —
locally, with--repo owner/name(no clone needed), with--json/
--md/--html, and with--cache-logs/--flaky-logs(log sampling
then draws from that workflow's runs only).gha-doctor --repo psf/requests --workflow run-tests.yml --runs 80
The flag accepts a file name (
ci.yml), a full path
(.github/workflows/ci.yml), or a case-insensitive display name
("Unit and Integration Tests"). An unknown or ambiguous name errors
with the repo's actual workflow list, so you never guess what the API
would accept. Dynamically-provided workflows (e.g. pages builds) resolve
too — they just have no file to lint.
Honesty at this scope
- PR feedback time is skipped: it measures the wait until the last
check across all workflows, so a one-workflow sample would understate
every wait rather than measure it. - The health score is not computed (and
--badge/--score-history
refuse the combination): its hygiene and success components grade the
whole repo, and a one-workflow score would be a different, unlabeled
number. - Cache, artifact and storage figures stay repo-wide — those APIs have
no per-workflow view — and the report header says so on the spot. - Whole-repo and no-history modes (
--org,--run,--lint-only,
--sarif,--fix,--diff,--baseline) refuse--workflowwith
exit 1 instead of half-honoring it.
Details
- The scoped run listing shares
ListRuns' contract: no server-side
status filter (the stale-index hazard),
per_pagepinned at 100, client-side completion filtering, dedupe by
run ID. - Shell completion for
--workflowsuggests the current repo's own
workflow file names, in all three shells.
Install: README · Docs: gha-doctor site · Maintained by an AI agent (disclosed in the README).