Node 20 EOL Reached GitHub Actions on September 23. Your node20 Actions Kept Passing.
GitHub removed Node 20 from its runners, and the runner answers by quietly running every node20 action on Node 24. The build stays green, so the build status can no longer tell you whether you migrated.
Node 20 reached end of life on 30 April 2026, according to the Node.js release
schedule. GitHub Actions kept it on its runners for almost five more months,
then on 23 September posted the final notice: Node 20 is gone, JavaScript
actions run on Node 24, and the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION
opt-out no longer works.
Read that as a removal and you would expect workflows calling node20 actions to
start failing. They did not. A Surge PR Preview run in antvis/AVA, started at
02:38 UTC on 24 September, finished green while calling actions/checkout@v4,
actions/setup-node@v4 and afc163/surge-preview@v1, all three of which
declare node20. The only trace is a warning annotation at the end of the job.
That makes two GitHub Actions deadlines in a week that meant something other than their date. It is a pattern worth naming.
The pattern: the date on the notice is not the day your pipeline changes
A deprecation notice gives a date, and teams plan against the date. What reaches your build is whatever the platform's code does, and that code can move before the date, after it, or absorb the change entirely.
Last week's instance was pull_request_target. GitHub set 2 November to enforce
a default rule disabling it in public repositories, while the change that
breaks builds, checkout refusing fork code in those workflows, reached every
floating actions/checkout major tag on 20 July. We
wrote that one up. Today's
instance runs the other way: the removal date passed, and for most workflows
nothing visible happened.
What the runner does with node20 after the Node 20 EOL
The answer is in the runner, not the changelog. In HandlerFactory.cs in
actions/runner, an action declaring node12 or node16 is first rewritten to
node20. A node20 declaration then goes through
NodeUtil.DetermineActionsNodeVersion, and when the server sets the
require-Node-24 flag, that function returns Node 24 before it reads either
environment variable. The opt-out is not refused with an error. It is never
consulted.
The action's metadata still says node20. The runner adds the action to a list
of upgraded actions and, when the job finishes, emits one warning naming them
and saying they are being forced to run on Node.js 24. That warning, on a job
that passed, is the entire user-visible footprint.
The docs have not caught up. GitHub's metadata syntax reference, fetched today,
still tells action authors to use node20 for Node.js v20.
The one hard failure in that code path is platform-specific. On Linux ARM32, where Node 24 has no official support, a separate kill-switch flag makes the runner return no Node version at all, and the step fails with a message that ARM32 runners are no longer supported. The 23 September notice also says Node 24 is incompatible with macOS 13.4 and earlier. Both are self-hosted concerns.
How much still declares node20
GitHub code search, run today, finds 9,168 action.yml files containing both
using and node20, against 6,288 for node24. A sample of twenty node20
hits were all real runs.using declarations, one of them github/combine-prs in
GitHub's own organisation. Those are file counts on indexed public default
branches, not repository counts, and private actions are invisible to the
search.
On GitHub-hosted runners, every one of those actions now runs on a Node major its author did not declare. Whether that matters depends on the action's code, which is the one thing no notice can tell you.
Why teams keep walking into it
The notice moves. The original September 2025 deprecation post carries three editor's notes: the migration moved to June 2026, then to 16 June, then the removal was set for 23 September. A team that planned against the first date planned against a moving target.
The build stops being a signal. Rewriting node20 to Node 24 keeps existing
workflows green, and that is precisely why a green build no longer tells you
whether you migrated.
It is not unique to Node. GitHub's 22 September SSH notice removes the SHA-1
ssh-rsa signature type and the diffie-hellman-group-exchange-sha256 key
exchange, and says most clients that support RSA with SHA-2 will pick it
automatically, so most connections need no change. The breaks land on old
libraries: its minimum-version table includes OpenSSH 7.2p1, libssh2 1.11.0
and PuTTY 0.82. Its schedule lists brownouts on 4 November and 9 December, then
prints the final removal as 13 January 2026, a date earlier than the notice
itself. Plan on the brownouts, because the final date cannot be right as
printed.
The argument in why enterprise AI doesn't ship is that systems break at the integration seams nobody owns. A pipeline assembled from third-party actions is that seam, and a removal that works by substitution hides it until an action's code meets a runtime it was never run against.
The cheapest way out
Stop reading the date and read the runtime. Four checks:
-
List what is being forced. For any run after 23 September, this prints the runner's warning, naming each node20 action:
gh run view RUN_ID -R OWNER/REPO --json jobs --jq '.jobs[].databaseId' \ | while read j; do gh api repos/OWNER/REPO/check-runs/$j/annotations --jq '.[].message'; done \ | grep 'forced to run on Node.js 24'No output means no job in that run called a node20 action.
-
Bump the majors. The latest releases of actions/checkout (v7.0.1) and actions/setup-node (v7.0.0) declare
node24; theirv4tags still declarenode20. If you publish an action, setruns.using: node24and cut a release. -
Retire the edge runners now. Self-hosted runners on Linux ARM32 or macOS 13.4 and earlier are out of support today, not on a later date.
-
Check SSH before 4 November. Run
ssh -vT git@github.comand read thekex: algorithmline, then check every CI library that speaks SSH to GitHub against the notice's minimum-version table.
The claim here is falsifiable: a GitHub-hosted job whose only problem is a
node20 declaration keeps passing after 23 September. A hosted job failing
with a Node 20 unavailable error would show it wrong. So far the runs point the
other way: antvis/AVA and openaps/AndroidAPSdocs both show successful runs after
the removal, each carrying the forced-upgrade warning.
Sources
- Node 20 is no longer available in GitHub Actions - GitHub Changelog
- Deprecation of Node 20 on GitHub Actions runners - GitHub Changelog
- HandlerFactory.cs - actions/runner
- NodeUtil.cs - actions/runner
- JobExtension.cs - actions/runner
- Metadata syntax reference - GitHub Docs
- Node.js release schedule - nodejs/Release
- antvis/AVA Surge PR Preview run, 24 September 2026
- openaps/AndroidAPSdocs Deploy run, 23 September 2026
- GitHub code search - node20 in action.yml files
- Security improvements for SSH - GitHub Changelog
- Workflow execution protections in GitHub Actions generally available - GitHub Changelog
- Safer pull_request_target defaults for GitHub Actions checkout - GitHub Changelog
Related reading
- Dev Tools
Your GitHub Copilot Settings Change Meaning on October 22. Unconfigured Will Mean On.
By Petru Popa · Read - Dev Tools
Copilot Custom Agents Pinned to Retiring Models: Aim at the LTS, Not the Suggested Alternative
By Petru Popa · Read - Dev Tools
Copilot's New Agent Observability Counts MCP Connections, Not Tool Calls
By Petru Popa · Read
Turn this into a plan for your team.
One week, fixed fee: a working session with your team, a prioritized use-case backlog, and an ROI model for the opportunities worth chasing.