Your CI is pinned to actions/checkout@v5 and node@22. It is not pinned to the
compiler. GitHub rebuilds the hosted runner images roughly weekly and you cannot
select an older one — the feature request was rejected
("there's no technical feasibility for implementation yet"), and GitHub staff have
said plainly that it's impossible to specify an older runner image in a workflow.
So when Clang, Python or CMake moves underneath you, the first sign is a red build
with no diff to blame.
runner-drift locks the tool versions your workflows actually use, diffs them on
every image bump, and names the runner-images commit that shipped the change.
It also answers the question every ubuntu-22.04 user has right now — GitHub is
deprecating that image from 2026-09-17, fully unsupported 2027-04-17, with four
brownouts starting 2027-03-23:
what actually breaks if I move to ubuntu-24.04?
$ npx runner-drift plan --from ubuntu-22.04 --to ubuntu-24.04
ubuntu-22.04 -> ubuntu-24.04 (images 20260720.234.2 -> 20260720.247.2)
ubuntu-22.04 is fully unsupported on 2027-04-17; brownouts begin 2027-03-23 (source: actions/runner-images#14254)
255 days left (230 until the first brownout) — deprecation began 2026-09-17; see https://github.com/actions/runner-images/issues/14254
brownout windows (14:00-00:00 UTC): 2027-03-23, 2027-03-30, 2027-04-06, 2027-04-13
announced migration targets: ubuntu-24.04, ubuntu-26.04, ubuntu-latest
Clang 13.0.1,14.0.0,15.0.7 -> 16.0.6,17.0.6,18.1.3 REMOVED: 13.0.1, 14.0.0, 15.0.7 / ADDED: 16.0.6, 17.0.6, 18.1.3
Python 3.10.12 -> 3.12.3 MINOR
2 of 3 detected tool(s) change; 1 unchanged (not shown)
That is real output against the live manifests. Note what is not there: CMake. It is 3.31.6 on both images, so it is suppressed — the report is only the rows that affect you, picked by scanning your own workflows for the tools your steps invoke.
- No account, no API key, no hosted service. Two endpoints only:
raw.githubusercontent.comandapi.github.com(unauthenticated;GITHUB_TOKENis used purely for the rate limit if it happens to be set). - Zero runtime dependencies. Node 22+, ESM.
npx runner-drift --help # no install
npm i -D runner-drift # or as a dev dependency
npm i -g runner-drift # or globally$ runner-drift init
Scanned 2 workflow file(s) in .github/workflows
Runner label: ubuntu-22.04 (image 20260720.234.2, 22.04.5 LTS)
Locked 3 tool(s): CMake, Clang, Python
Wrote runner-lock.json
Heads up: ubuntu-22.04 is fully unsupported on 2027-04-17 (https://github.com/actions/runner-images/issues/14254)
Preview the move: runner-drift plan --from ubuntu-22.04 --to ubuntu-24.04
Next: add the guard step to your workflow (see the README) and commit runner-lock.json.Commit runner-lock.json.
Add the action to any job (it lives at the root of this repo, so it also works straight from the Marketplace):
- uses: Booyaka101/runner-drift@v1
with:
fail-on: major # omit to report only and never fail the jobOr call the CLI directly:
- run: npx runner-drift guard --fail-on major
env:
GITHUB_TOKEN: ${{ github.token }}The first run records the baseline and exits 0:
baseline recorded — ubuntu-22.04 image 20260623.199.1
Terraform: 1.15.6 (from manifest)
Kotlin: 2.4.0-release-281 (from manifest)
CMake: 3.31.6 (from manifest)
Wrote runner-lock.json. Commit it so the next image bump can be diffed.
A later run, after GitHub has rolled four new images:
::warning title=runner-drift: Terraform patch::Terraform drifted on ubuntu-22.04: 1.15.6 -> 1.15.8 (PATCH) — shipped by 20260714.228.1 https://github.com/actions/runner-images/commit/f3d0fbf668c2d437a5a5a03e75206801e22e5e62
::warning title=runner-drift: Kotlin patch::Kotlin drifted on ubuntu-22.04: 2.4.0-release-281 -> 2.4.10-release-377 (PATCH) — shipped by 20260720.234.2 https://github.com/actions/runner-images/commit/3b7fa9c1aa1efb5fc0ba4b443dcfa69f47f53434
ubuntu-22.04 image 20260623.199.1 -> 20260720.234.2
Terraform 1.15.6 -> 1.15.8 PATCH [20260714.228.1] https://github.com/actions/runner-images/commit/f3d0fbf668c2d437a5a5a03e75206801e22e5e62
Kotlin 2.4.0-release-281 -> 2.4.10-release-377 PATCH [20260720.234.2] https://github.com/actions/runner-images/commit/3b7fa9c1aa1efb5fc0ba4b443dcfa69f47f53434
…and the same thing as a table in the job summary:
ubuntu-22.04 image 20260623.199.1 → 20260720.234.2
| Tool | Locked | Now | Change | Shipped by |
|---|---|---|---|---|
Terraform |
1.15.6 | 1.15.8 | 🟡 PATCH | 20260714.228.1 |
Kotlin |
2.4.0-release-281 | 2.4.10-release-377 | 🟡 PATCH | 20260720.234.2 |
Four image versions shipped between the lock and the run, and each tool is pinned
to the specific one that changed it — not just "the newest image". CMake did
not move, so it is not in the table.
runner-drift plan --from ubuntu-22.04 --to ubuntu-24.04
runner-drift plan --from macos-14 --to macos-15 --tools python,node,dotnet
runner-drift plan --from ubuntu-22.04 --to ubuntu-26.04 --json| Flag | Applies to | Default | Meaning |
|---|---|---|---|
--workflows <path> |
all | .github/workflows |
Workflow directory or a single workflow file |
--lock-file <path> |
init, guard |
runner-lock.json |
Lock file location |
--tools <a,b,c> |
all | detected | Override detection. Aliases (python, npx, clang++, g++, javac, …) resolve to manifest names; anything else is matched against the manifest case-insensitively, so --tools Terraform,Kotlin works |
--label <label> |
init |
detected | Explicit runner label |
--from / --to |
plan |
— | Runner labels to compare (required) |
--fail-on <level> |
guard |
never fail | major, minor or any |
--json |
all | off | Machine-readable output |
--no-summary |
guard |
on | Skip the $GITHUB_STEP_SUMMARY write |
--no-update-lock |
guard |
on | Report drift but leave the lock file untouched |
Exit codes: 0 success (including "drift found" without --fail-on), 1 drift at
or above the --fail-on threshold, 2 usage / configuration error.
| Input | Default | Meaning |
|---|---|---|
fail-on |
'' |
major, minor, any; empty means report only |
tools |
'' |
Comma-separated override |
lock-file |
runner-lock.json |
Lock file path |
workflows |
.github/workflows |
Scanned when there is no lock yet |
version |
1.0.1 |
npm version of runner-drift to run |
github-token |
${{ github.token }} |
Rate limit only |
{
"schemaVersion": 1,
"label": "ubuntu-22.04",
"imageOS": "ubuntu22",
"imageVersion": "20260623.199.1",
"tools": {
"Python": { "versions": ["3.10.12"], "source": "probe", "command": "python3 --version" },
"Clang": { "versions": ["13.0.1", "14.0.0", "15.0.7"], "source": "manifest" }
},
"updatedAt": "2026-08-05T02:52:15.070Z"
}source records how the version was observed. guard probes the tool directly
(python3 --version, clang --version, java -version, …) when it can, because a
manifest says what the image was built with while a probe says what your job
will actually execute. Tools with no probe recipe fall back to the manifest for
that exact image version, and the source is recorded so a source change is never
mistaken for a version change.
ubuntu-22.04, ubuntu-24.04, ubuntu-26.04 (+ -arm), windows-2022,
windows-2025, macos-14, macos-15, macos-26 (+ -arm64).
Deadline data currently covers ubuntu-22.04 (+ arm) and macos-14 — the two
images with an announced retirement date. Every other label diffs fine, it just
has no countdown.
- Floating labels are refused, on purpose.
ubuntu-latest/macos-latestare re-pointed by GitHub without notice, soplanwill not guess what they mean — pass the concrete label.guarddoes not need to guess: it reads the real label from the runner'sImageOSenv var at run time. - Deadlines are a hardcoded table, transcribed from #14254 and #13518 and printed with their source URL. There is no machine-readable feed to consume; if GitHub moves a date, the table needs a release.
- Detection is a targeted line scan, not a full YAML parse (the package has zero
dependencies). It handles inline, flow-sequence and block-sequence
runs-on:, and resolvesruns-on: ${{ matrix.os }}by harvesting label-shaped values from the same file. If it misses something,--toolsand--labeloverride it completely. - Self-hosted runners are a clean skip, not a feature. No
ImageVersion, nothing to compare;guardprints a::noticeand exits 0. - Azure DevOps is out of scope, even though the same images and the same deprecation apply there.
- No auto-fix.
runner-drifttells you exactly what moved and who moved it; the migration is yours. - Multi-version probes report one version.
clang --versionreports the default clang, while the manifest lists all three. That is why thesourcefield exists — compare like with like.
git clone https://github.com/Booyaka101/runner-drift
cd runner-drift
node --test # 101 tests, fully offline against real downloaded manifest fixturesTests run against four real manifest snapshots in test/fixtures/
(Ubuntu2204 at two different image versions, Ubuntu2404, macos-15), so the
golden plan output is deterministic while the live path re-fetches.
- npm — https://www.npmjs.com/package/runner-drift
- GitHub Marketplace — https://github.com/marketplace/actions/runner-drift
- Write-up — You can't pin a GitHub Actions runner image, but you can find out exactly what changed
MIT — see LICENSE.