Skip to content

About

Catch GitHub Actions breakages before CI does: Python/Node versions missing on your runners, retired runner labels, YAML 3.10 -> 3.1, actions stuck on Node 20.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

ci-preflight ✈️

Catch GitHub Actions breakages before your CI does.

CI npm License: MIT

Your workflow file is valid YAML, actionlint is happy, and CI still goes red on Monday, because something outside your repo changed:

  • ubuntu-latest is moving to Ubuntu 26.04. There are no Python 3.8/3.9 builds for it, so setup-python fails with "The version '3.9' with architecture 'x64' was not found".
  • macos-latest is Apple Silicon. Python 3.7 and Node 14 have no arm64 builds there.
  • windows-11-arm has 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@v4 and many other actions still declare using: node20.
  • python-version: 3.10 without quotes is the number 3.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.

ci-preflight output

Quick start

npx ci-preflight            # checks .github/workflows, .github/actions and action.yml

No 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.

As a GitHub Action

# .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@v0

Findings 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.

What it checks

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), then include (extends matching combinations or adds new ones). Only the failing combinations are reported.
  • Expressions ${{ matrix.* }} and ${{ env.* }} (workflow, job and step env) are resolved. Anything else, such as inputs.* or fromJSON(...), is skipped, not guessed.
  • -latest labels 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-python matching 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-node matching uses the same file keys the action downloads (osx-arm64-tar, win-arm64-zip, …). lts/<codename> is validated. lts/*, latest and node always 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.

Data sources

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.

CLI

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.

Silencing a finding

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

Action inputs and outputs

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).

How is this different from…

  • 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.

Roadmap

  • setup-go, setup-java, setup-ruby, setup-dotnet, setup-uv version 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.

Development

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.

License

MIT

About

Catch GitHub Actions breakages before CI does: Python/Node versions missing on your runners, retired runner labels, YAML 3.10 -> 3.1, actions stuck on Node 20.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages