If your CI logs lit up with Node.js 20 actions are deprecated warnings sometime in the last few weeks, you’re not alone and you’re not imagining it. GitHub flipped the switch in early June 2026: JavaScript actions now run on Node 24 by default, whether or not the action was written for it. The warnings are the polite version. The hard version arrives September 16, 2026, when Node 20 gets pulled off the runners entirely and anything still pinned to it stops executing.
So this is a deadline, not a suggestion. The good news is that for most repos the fix is a 20-minute chore — bump a handful of action versions and the noise goes away. The annoying part is the edge cases: a custom action you maintain, a third-party action whose author went quiet, or a job that actually breaks under Node 24 because of how the newer runtime handles fetch or native modules. Let’s sort out which bucket you’re in.
The timeline that’s driving all of this
GitHub announced the Node 20 deprecation in a September 2025 changelog, and the rollout has three dates worth pinning to your calendar:
- March 2026 — Node 24 became the default runtime version that new and updated actions target. This is when the deprecation warnings started showing up for actions still declaring
node20. - Early June 2026 — the forced flip. JavaScript actions that declare
node20in their metadata now get executed on Node 24 regardless. GitHub did this so your pipelines keep running on a supported, patched runtime even if the action author hasn’t updated yet. - September 16, 2026 — Node 20 is removed from the runner image. After this, the compatibility shim is gone. Actions that can’t run on Node 24 will fail, not warn.
That middle step trips people up. Once the forced default landed, a lot of teams assumed they were fine because nothing broke — the jobs still passed. But “runs on Node 24 via a shim” and “declares Node 24 in its metadata” are different states, and only the second one makes the warning disappear. More on that in a second, because it’s the single most common source of confusion right now.
One sharp edge for self-hosted and macOS users: Node 24 dropped support for macOS 13.4 and older, and it has no official ARM32 build. If you run self-hosted runners on a Raspberry Pi or an old Mac mini, that’s a real wall, not a warning. Plan for new hardware or a different runner.
Why the warning won’t go away even after you “updated”
Here’s the thing nobody explains clearly. An action’s runtime is declared in its action.yml under runs.using — it says node20 or node24. The deprecation warning is keyed off that metadata field, not off which Node version actually executed the code.
So you can hit three different states:
- You’re on an old action version that declares
node20. You get a warning, and (post-June) it runs on Node 24 anyway via the shim. - You bump to a version where the author changed
runs.usingtonode24. Warning gone. - You set
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true. The action runs on Node 24, but the warning stays, because the metadata still saysnode20.
That third one catches people who reach for the env var thinking it’s a fix. It’s not a fix for the warnings — it just opts you into the new runtime early. The only thing that silences the log spam is the action author updating their metadata and you pulling that version. If you’ve bumped everything and a warning persists, it’s coming from a transitive action you didn’t pin directly — a setup-* step inside a composite action, usually. Track it down by reading the full annotation, which names the offending action.
Fix path 1: you just consume actions
This is 90% of repos. You don’t write actions, you use them. The fix is to bump the major versions of every action in your workflows to one built for Node 24. The big first-party ones to update:
actions/checkout→ v5 or v6actions/setup-node→ v5 or v6actions/cache→ latest majoractions/upload-artifact/actions/download-artifact→ v4+ (older majors were already deprecated separately)actions/github-script→ latestpnpm/action-setup,actions/setup-python, and any othersetup-*you rely on
As of mid-2026 the first-party actions ship v6 as the current major, with v5 also Node 24-capable. Either is fine — pick one and be consistent. If you pin actions to commit SHAs (which you should, for supply-chain safety), grab the SHA of the latest release tag rather than a floating major.
Don’t blindly run a find-and-replace to @v6 across the board, though. Major version bumps occasionally carry behavior changes unrelated to Node — checkout v5/v6 tightened some defaults, and upload-artifact v4 changed how artifacts merge across matrix jobs. Read the release notes for the two or three actions you depend on most. For the long tail of minor actions, a bump-and-test in a branch is usually safe.
If an action you depend on hasn’t been updated and the maintainer is unresponsive, you have a few options: fork it and bump the metadata yourself, find a maintained alternative, or — as a stopgap only — keep it limping with the escape hatch below. None of these are great, which is why auditing early matters.
Fix path 2: you maintain an action
If you publish a JavaScript action, this part is on you, and it’s genuinely small. Open your action.yml (or action.yaml) and change the runtime declaration:
runs:
using: 'node24' # was 'node20'
main: 'dist/index.js'Then rebuild your bundle on Node 24 and commit the regenerated dist/. Most actions use @vercel/ncc to compile everything into a single file:
nvm install 24
nvm use 24
npm ci
npm run build # usually: ncc build src/index.js -o dist
git add dist action.yml
git commit -m "Run on Node 24"The reason you rebuild rather than just editing the YAML: your committed dist/ was produced against Node 20’s bundled dependencies and assumptions. Regenerating on 24 catches any toolchain-level drift before your users hit it. After you tag a new release, your consumers’ warnings disappear the moment they bump to it — you’re fixing the problem for everyone downstream.
While you’re in there, update your own CI to test against Node 24, and consider a matrix that covers both 22 and 24 so you don’t accidentally ship something that only works on one. If you use a setup-node step in the action’s own workflow, bump that too.
The escape hatches (and why they expire)
GitHub gave us two environment variables for the transition. Use them deliberately, because both are temporary by design.
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true — this keeps a node20 action running on Node 20 after the forced default, overriding the shim. It’s the “I need this action and it genuinely can’t run on 24 yet” lever. The catch: it only works until September 16, when Node 20 leaves the runner. After that the variable points at a runtime that doesn’t exist, and the job fails. Treat it as a countdown timer, not a solution.
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true — the opposite lever. It forces node20-declared actions onto Node 24 now, before the global default would. Useful for testing whether your whole pipeline survives Node 24 ahead of the deadline. As noted, it doesn’t silence warnings — but it’s the cleanest way to flush out runtime incompatibilities on your schedule instead of GitHub’s.
You can set either as a workflow-level env:, a job-level one, or as a machine-level variable on self-hosted runners. For a controlled test, scope it to one workflow:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: trueMy advice: use FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 for a week on a test branch to surface breakage, fix what breaks, then bump action versions properly and remove the variable. Reserve ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION for the one stubborn action you can’t replace before September, and put a ticket on the calendar to deal with it.
Find every action still on Node 20
Before you can fix anything you need an inventory. The warnings in your logs are one source, but they only show actions that actually ran on the last execution — conditional jobs and rarely-triggered workflows hide. Grep the workflow files directly:
# Every action reference across all workflows
grep -rhoE 'uses:\s*\S+' .github/workflows/ \
| sed -E 's/uses:\s*//' \
| sort -uThat gives you the full list of owner/action@ref strings to check. For each one, glance at its repo and confirm the version you’re pinned to declares node24. If you have a lot of repos, the GitHub API can enumerate workflows org-wide, but for a single repo the grep is faster than any tooling.
A slightly fancier version flags pinned majors that are suspiciously old:
grep -rnE 'uses:.*@v[0-9]+' .github/workflows/ \
| grep -iE 'checkout@v[1-4]|setup-node@v[1-4]|cache@v[1-3]|artifact@v[1-3]'Anything that matches is almost certainly still on Node 20. Bump those first — they’re the highest-traffic actions and the ones most likely to actually break.
What actually breaks on Node 24
For the vast majority of workflows, nothing breaks — Node 24 runs your node20 actions fine and the migration is purely cosmetic. But if your pipeline compiles code, talks to flaky TLS endpoints, or runs your own scripts via actions/github-script or inline node, test before September. The failure modes worth knowing:
Native addons. Node 24 bumped its ABI, so any package with compiled bindings (bcrypt, node-sass/sharp in some setups, database drivers) needs a rebuild against 24. Pre-built binaries from node-pre-gyp are usually fine, but anything compiled in CI needs the toolchain refreshed. On Windows specifically, Node 24 dropped MSVC in favor of ClangCL — if your CI compiles native addons on Windows runners, that pipeline needs attention.
fetch and Undici. Node 24 ships a newer Undici, which backs the global fetch. The server-side fetch implementation keeps getting stricter about malformed headers, redirect handling, and connection reuse. If you have scripts hitting internal APIs that were sloppy about HTTP semantics, they may start throwing where they used to limp along. This is the kind of thing that passes locally and fails in CI, so test against 24 explicitly.
require() of ES modules. This is now stable in Node 24, which is mostly good news — but a CJS file that require()s an ESM module with top-level await throws a hard ERR_REQUIRE_ASYNC_MODULE. If a dependency quietly shipped a TLA somewhere, you’ll find out now.
TLS and crypto. Node 24 tightened defaults around legacy TLS and deprecated some old crypto APIs. Integrations with older internal services or legacy certificate setups are the usual victims.
The clean way to de-risk all of this is a matrix that runs your test suite on both Node 22 and 24 before you commit to 24 everywhere:
strategy:
matrix:
node: [22, 24]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- run: npm ci && npm testNote that this node-version is the runtime your code runs on — separate from the Node version the action machinery uses. Bumping setup-node’s major fixes the deprecation warning; setting node-version: 24 is how you verify your actual application survives. Keep the two ideas distinct or you’ll chase the wrong problem.
Where to start tomorrow morning
If you do one thing today, run the grep audit and bump actions/checkout, actions/setup-node, actions/cache, and the artifact actions to their current majors. That clears the warnings for most repos and removes the September risk for the actions most likely to break. Then spin up a throwaway branch with FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true and watch what falls over — better to find the one cranky native addon now than at 2am on September 16.
While you’re tuning workflows, it’s a decent moment to glance at what all this CI is costing you — runner minutes add up fast once you start running 22-and-24 matrices everywhere. If that’s on your mind, our breakdown of GitHub Actions pricing in 2026 covers where the minutes actually go.
Sources: GitHub Changelog — Deprecation of Node 20 on GitHub Actions runners, actions/setup-node releases, Node.js 20 to 24 migration notes