Catch GitHub Actions breakages before your CI does.
Your workflow file is valid YAML, actionlint is happy, and CI still goes red on Monday, because something outside your repo changed:
ubuntu-latestis moving to Ubuntu 26.04. There are no Python 3.8/3.9 builds for it, sosetup-pythonfails with "The version '3.9' with architecture 'x64' was not found".macos-latestis Apple Silicon. Python 3.7 and Node 14 have no arm64 builds there.windows-11-armhas no Python builds before 3.11.- macOS 14 runners were retired on 2026-11-02. Ubuntu 20.04 and Windows 2019 were retired earlier.
- Node 20 reached end-of-life, and GitHub removed it from hosted runners in September 2026.
actions/checkout@v4and many other actions still declareusing: node20. python-version: 3.10without quotes is the number3.1. On Linux the job fails. On Windows it can quietly install Python 3.1.4.
ci-preflight expands your build matrix and checks every job against the live lists of
runner images, Python builds, Node.js builds and the runtime each action declares. It tells you
what will break and how to fix it.
npx ci-preflight # checks .github/workflows, .github/actions and action.ymlNo config is needed. The tool sends no code anywhere: it only downloads public version lists,
caches them for 6 hours, and reads the action.yml of the actions you use.
# .github/workflows/preflight.yml
name: Preflight
on:
pull_request:
schedule:
- cron: '0 6 * * 1' # every Monday: catch runner changes before they bite
jobs:
preflight:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: robin221221/ci-preflight@v0Findings show up as inline annotations on the PR and in the job summary. The step fails on
errors. Set strict: true to also fail on warnings.
| Rule | Severity | Example |
|---|---|---|
python-unavailable |
error | python-version: '3.9' on ubuntu-26.04, 3.7 on macos-latest, 3.10 on windows-11-arm |
python-unavailable |
warning | the version works on today's ubuntu-latest but has no build for the image it is migrating to |
node-unavailable |
error | node-version: 14 on macos-latest (arm64), 18 on windows-11-arm |
runner-retired |
error | runs-on: ubuntu-20.04, macos-13, macos-14, windows-2019 |
runner-deprecated |
warning | runs-on: ubuntu-22.04 (retirement scheduled for 2027-04-17) |
runner-unknown |
error | runs-on: ubuntu-24.4 (a typo, so the job waits for a runner forever) |
yaml-float |
error | python-version: 3.10, go-version: 1.20, node-version: 18.10 (YAML drops the trailing zero) |
action-node-runtime |
warning | actions/cache@v4 runs on node20. Suggests the newest major on node24 |
workflow-parse |
error | the file is not valid YAML |
A few details:
- Matrices are expanded like GitHub does it: the cartesian product first, then
exclude(partial matches), theninclude(extends matching combinations or adds new ones). Only the failing combinations are reported. - Expressions
${{ matrix.* }}and${{ env.* }}(workflow, job and stepenv) are resolved. Anything else, such asinputs.*orfromJSON(...), is skipped, not guessed. -latestlabels are checked against today's image and the newer GA image the label is migrating to, so you get a warning before the switch.setup-pythonmatching follows the action's own rules:3.12,3.12.4,3.x,>=3.10 <3.13,3.13t(free-threaded),3.14-dev,allow-prereleases,architecture, and multi-line lists. PyPy and GraalPy are skipped.setup-nodematching uses the same file keys the action downloads (osx-arm64-tar,win-arm64-zip, …).lts/<codename>is validated.lts/*,latestandnodealways resolve, so they are not checked.- Not checked: jobs that run in a
container:(the tools come from the image), self-hosted runners, larger runners with custom labels, and matrices computed at runtime. Labels are still checked when possible.
| What | Source |
|---|---|
| Runner labels, OS versions, architectures, deprecation badges | actions/runner-images README |
| CPython builds per OS version and arch | actions/python-versions manifest, the same file setup-python uses |
| Node.js builds | nodejs.org/dist/index.json |
| Action runtimes | each action's action.yml at the ref you use, plus its latest release |
Responses are cached in ~/.cache/ci-preflight (or $XDG_CACHE_HOME) for 6 hours. Use
--offline to run from the cache only. Set GITHUB_TOKEN or GH_TOKEN to avoid API rate limits
when the tool looks up latest releases.
ci-preflight [options] [paths...]
-f, --format <fmt> text | github | json (default: text; inside GitHub
Actions, annotations are printed as well)
--strict exit with code 1 on warnings, not only errors
--ignore <rules> comma-separated rule ids to skip (repeatable)
--offline use cached data only (no network)
--no-actions do not fetch action metadata (skips action-node-runtime)
--cwd <dir> repository root (default: current directory)
--verbose show what was skipped (dynamic matrices, ...)
--rules list rule ids
Exit codes: 0 means no errors, 1 means problems were found, 2 means a usage error.
Put a comment on the same line or the line above:
runs-on: ubuntu-22.04 # ci-preflight-ignore
python-version: '3.8' # ci-preflight-ignore: python-unavailable| Input | Default | Description |
|---|---|---|
path |
(auto) | files or directories, comma- or newline-separated |
strict |
false |
fail on warnings too |
ignore |
comma-separated rule ids | |
summary |
true |
write a Markdown report to the job summary |
token |
${{ github.token }} |
used to look up latest action releases |
Outputs: errors, warnings (counts).
- actionlint checks that your workflow is correct: syntax, expressions, shellcheck. ci-preflight checks that it will still run against the world as it is today. Use both.
- Dependabot / actions-up bump action versions. They don't know that Python 3.9 has no Ubuntu 26.04 build, and they don't look at your matrix.
- runner-drift tracks the tools preinstalled on
runner images. ci-preflight checks the versions you ask
setup-*actions to install.
On a sample of 36 popular Python and JavaScript repositories (November 2026), it found 19 that
use actions on end-of-life Node runtimes, 7 still on the deprecated ubuntu-22.04, 3 testing
Python versions that the next ubuntu-latest won't have, and 2 with jobs on retired macOS 14 runners.
setup-go,setup-java,setup-ruby,setup-dotnet,setup-uvversion checks- tools used in
run:steps that a runner image no longer ships - checking the steps of reusable workflows and composite actions you call
- end-of-life notices (for example, a Python version past its EOL date)
Ideas and bug reports are welcome. Please open an issue.
npm ci
npm run check # typecheck + tests + build
node src/cli.ts # run from source (Node 22.18+ strips types natively)Tests run offline against trimmed snapshots of the real data in test/fixtures/data.
dist/ is committed because the GitHub Action runs from it. CI verifies it is up to date.
