All articles
Dev ToolsBy Petru Popa · 6 min read

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:

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

  2. Bump the majors. The latest releases of actions/checkout (v7.0.1) and actions/setup-node (v7.0.0) declare node24; their v4 tags still declare node20. If you publish an action, set runs.using: node24 and cut a release.

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

  4. Check SSH before 4 November. Run ssh -vT git@github.com and read the kex: algorithm line, 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.

Ready to start?

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.

Book an AI Opportunity Sprint