What in your CI is already dead, and what dies on a published date.
py -m ci_eol.cli .Exit 0 when nothing inside the window is dying, 1 when something is, 2 when the question could not be asked. Python standard library only, no dependencies, no API key, no network call — the tables ship with the tool.
A workflow keeps running right up to the day it does not. The runner image is retired, the language version stops getting security fixes, the action's major version is switched off — each on a date that was published months earlier, and none of which produces a warning in your repository first.
The scale is not a guess. GitHub code search, 21 September 2026:
| What a workflow still names | Files on GitHub |
|---|---|
actions/checkout@v3 |
1,335,296 |
node-version: 18 (EOL 30 April 2025) |
516,096 |
ubuntu-22.04 (left support 17 September 2026) |
230,400 |
python-version: "3.9" (EOL 31 October 2025) |
165,632 |
actions/upload-artifact@v3 (deprecated 30 November 2024) |
130,048 |
ubuntu-20.04 (retired 15 April 2025) |
117,504 |
Dependabot raises a pull request when a version moves. It does not tell you that the thing you have stops working on the eleventh of a month. That is a different question, and it is the one someone asks the morning their build goes red: what of mine is dying, when exactly, and what replaces it.
Against my own repositories on 21 September 2026 — which is the only honest way to show a tool like this:
$ py -m ci_eol.cli ../repo-secret-scanner
ci-eol - tables copied 2026-09-21
runner images https://endoflife.date/github-actions-runner-images
language EOL https://endoflife.date
action notices https://github.blog/changelog/2024-04-16-deprecation-notice-v3-of-the-artifact-actions/
2026-10-31 in 40 days
language version Python 3.10 1 occurrence(s)
upstream support runs to 2026-10-31
.github/workflows/ci.yml:18 python-version: ["3.10", "3.12"]
1 occurrence(s) are past their date or die within 90 days.Exit code 1. Across 31 repositories it found Python 3.10 in 17 of them, forty days from the end of upstream support, and Node.js 20 in one, already 144 days past it. Including this repository's own CI, which is the point: a check its author exempts himself from is advice, not a check.
The identifiers overlap, and a report with a false alarm in it is read once and then ignored:
ubuntu-22.04is a prefix ofubuntu-22.04-arm64, and they retire on different terms. Matches are bounded on both sides and ordered longest-first.@v3is a prefix of@v3.1.0. A full tag is read by its major.- A SHA pin says nothing about the major, so it is not guessed at.
${{ matrix.python-version }}carries no version; the numbers live where the matrix is declared, andpython-version: ["3.10", "3.12"]yields both.
The test suite asserts that the current major of every action in the table is never reported, and that every live runner image stays out of the report.
Two different facts, kept apart:
- A published date.
actions/upload-artifact@v3was deprecated on 30 November 2024,ubuntu-20.04retired on 15 April 2025. These carry a date and they set the exit code. - Majors behind.
actions/checkout@v3is four majors behind v7. That is worth knowing and it is not a death sentence, so it is printed in its own section and never fails a build.
py -m ci_eol.cli . # this repository
py -m ci_eol.cli .github/workflows/ci.yml # one file
py -m ci_eol.cli . --within 30 # fail only on the next 30 days
py -m ci_eol.cli . --json report.json # machine-readable
py -m ci_eol.cli . --exclude '.github/workflows/experimental.yml'
py -m ci_eol.cli --list # print the tables
py -m ci_eol.cli --check-source # ask the feeds whether the dates movedEvery command above works offline, which is the point: a check that needs the
network fails in an air-gapped build. --check-source is the exception and is
opt-in for that reason.
$ py -m ci_eol.cli --check-source
ci-eol - comparing the tables copied 2026-09-21 against the feeds
github-actions-runner-images
every date in the table is the date the feed gives, and the feed knows no dated cycle this table lacks
...
Action deprecations are not checked: theirs is a blog post rather than a feed.
The tables still match every feed.Exit 0 when the feeds agree, 1 when a date moved or a dated cycle is missing, 2 when a feed could not be read. Because endoflife.date publishes JSON, the comparison is date against date — a moved date is reported with both values rather than guessed at from prose. Action deprecations are deliberately left out: theirs is a blog post, and a second parser for prose would be another thing to keep right.
Seven of the tests feed the checker by hand instead of over the network, because a run on the day the tables were copied proves only that it reports agreement. Two of those seven exist because the first version compared in one direction only: it asked whether the table agrees with the feed, never whether the table holds a row the feed has never heard of.
- The tables are a dated snapshot, not a feed. Copied on 21 September 2026
from the three sources named in every report. Past sixty days the tool says
out loud that they are old, and
--check-sourceanswers whether they have actually drifted. A silently stale table would be worse than no tool. - Only what the workflow names. A version installed by a shell script, a Dockerfile, or an action's own default is invisible here. "No findings" means nothing named in the file is dying, not that nothing is.
- Action deprecations are only the ones GitHub dated. The artifact actions have a published notice; most actions never get one, so they appear under "majors behind" instead of being invented a date.
- Upstream EOL is not your EOL. A language past upstream support still runs. What it stops getting is security fixes, which is a decision for you and not something this can make.
- GitHub Actions only. GitLab CI, CircleCI and Jenkins name the same things in a different shape; the matcher would need their syntax, and adding it is adding a reader, not rewriting the tables.
MIT, © Dmytro Galko. See LICENSE.