Have 30-200 Employees? Make $50k-$500k selling your data for AI Training.

Learn More
Node 20 GitHub Actions Deprecated: How to Migrate Workflows
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
  • Premium Results
  • Publish articles on SitePoint
  • Daily curated jobs
  • Learning Paths
  • Discounts to dev tools
Start Free Trial

7 Day Free Trial. Cancel Anytime.

How to Migrate GitHub Actions from Node 20 to Node 24

  1. Audit existing workflows for actions whose action.yml declares runs.using: node20.
  2. Update third-party action version pins to releases that target node24.
  3. Change the runs.using field in custom action.yml files from node20 to node24.
  4. Scan action dependencies for native modules (.node, node-gyp) that need recompilation.
  5. Verify engines fields in package.json allow Node 24.
  6. Test actions across Ubuntu, Windows, and macOS runners under the Node 24 runtime.
  7. Pin verified action versions using full commit SHAs for supply-chain security.
  8. Monitor GitHub's changelog for the hard removal date of the Node 20 runner runtime.

Any team relying on GitHub Actions for CI/CD will soon face a hard deadline. GitHub has officially deprecated Node 20 on Actions runners, and the migration to Node 24 is not optional. This guide walks through every step of the migration: auditing existing workflows, updating third-party and custom actions, handling native module compatibility, and resolving the errors that surface along the way.

Table of Contents

Why GitHub Actions Is Dropping Node 20

Node 20's active LTS support ends April 2026, after which it receives no security patches. GitHub is deprecating it on Actions runners ahead of that date. Once a Node.js version exits active LTS, it stops receiving security patches, bug fixes, and performance updates. GitHub has consistently aligned the Actions runner environment with actively supported LTS versions. Running JavaScript actions on an unsupported runtime leaves them exposed to unpatched CVEs and unfixed bugs.

GitHub announced the deprecation of Node 20 on Actions runners in a changelog post dated September 19, 2025 (https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/). The rollout follows a phased approach that mirrors previous runtime transitions (Node 12 to Node 16, Node 16 to Node 20): deprecation warnings appear first in workflow run logs, then those warnings escalate to more prominent notices, and eventually the Node 20 runtime is removed from runners entirely, causing any action still targeting it to fail. As of the September 19, 2025 changelog, GitHub has not announced a specific hard removal date. Monitor the linked changelog and subscribe to GitHub blog updates for timeline announcements. Waiting until warnings become hard failures means scrambling under pressure rather than migrating on your own schedule.

Waiting until warnings become hard failures means scrambling under pressure rather than migrating on your own schedule.

What This Deprecation Actually Affects (and What It Doesn't)

Actions Runtime vs. Your Project's Node Version

The single most important distinction in this migration is the difference between the Node.js version that executes a JavaScript action and the Node.js version a workflow installs for project-level tasks.

When a GitHub Action's action.yml declares runs.using: node20, that tells the Actions runner to use its bundled Node 20 binary to execute the action's JavaScript code. This is the runtime being deprecated. Separately, when a workflow step uses actions/setup-node to install Node 20 (or Node 18, or Node 22) for running a project's test suite, linting, or build process, that installation is completely unaffected by this deprecation. A project that needs to test against Node 20 in its CI matrix can continue doing so indefinitely. The setup-node action simply downloads and configures the requested Node.js version on the runner's PATH; it has no relationship to the runtime that executes the action itself.

Composite actions that invoke JavaScript actions inherit the runtime of those child actions; review their runs.using fields as well.

Who Needs to Act

Three groups must take action. Action maintainers who publish JavaScript-based actions need to update the runs.using field in their action.yml metadata from node20 to node24. Update your version pins to pull in releases rebuilt for Node 24 if you consume actions via uses: directives. Teams with custom or private actions, whether stored in internal repositories or embedded within monorepos, must audit and update those actions just as they would any public action.

Step 1: Audit Your Workflows for Node 20 Dependencies

Finding Affected Actions

The first task is identifying which actions in existing workflows still target Node 20. GitHub itself surfaces deprecation warnings directly in workflow run logs. After a workflow completes, expanding the log output for individual steps will reveal warning annotations for any action running on the deprecated Node 20 runtime. These warnings name the specific action and version responsible.

For a more proactive approach, manually inspect the action.yml file of each pinned action version referenced in workflow files. Navigate to the action's repository at the tagged version being used (for example, actions/checkout at v4), open action.yml, and check the runs.using field. If it reads node20, that action will need updating. For workflows with dozens of action references, this manual approach is tedious but thorough.

Using actions/setup-node Correctly

actions/setup-node sets the project's Node version for subsequent workflow steps. It does not influence the runtime used to execute any action. A node-version matrix that includes Node 18, Node 20, and Node 22 for project testing purposes is entirely valid and should remain unchanged. The deprecation applies only to the action execution layer.

# .github/workflows/test.yml
name: Run Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    permissions:
      contents: read  # Explicit least-privilege permissions

    strategy:
      matrix:
        # This matrix controls YOUR PROJECT's Node version for testing.
        # It is NOT affected by the Node 20 action runtime deprecation.
        node-version: ["18", "20", "22", "24"]

    steps:
      # Pin actions to full commit SHAs for supply-chain security.
      # Resolve current SHA: git ls-remote https://github.com/actions/checkout refs/tags/v4
      - uses: actions/checkout@v4  # Replace with SHA pin, e.g. actions/checkout@11bd71901...

      # actions/setup-node installs Node for your project's build/test steps.
      # This is independent of the Node version that executes the actions themselves.
      # Verify setup-node Node 24-compatible version at github.com/actions/setup-node/releases
      - uses: actions/setup-node@v4  # Replace with SHA pin
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'  # Built-in cache; handles cross-platform path automatically

      - run: npm ci --ignore-scripts
      - run: npm test

Step 2: Update Third-Party Action Versions

Checking for Node 24-Compatible Releases

Most widely used actions publish new major or minor versions specifically to adopt updated runtimes. Check the releases page and changelog of each action referenced in your workflows. A release that updates the runs.using field to node24 will typically call this out explicitly in its release notes. For example, the actions/checkout action has historically bumped major versions to coincide with runtime transitions (verify the version-to-runtime mapping at the respective tagged action.yml in the actions/checkout repository).

Verify the latest actions/checkout release at https://github.com/actions/checkout/releases and confirm its action.yml declares runs.using: node24 before pinning to that version. You can check any action's runtime target directly:

# Replace OWNER/REPO and TAG with the action and version you want to verify.
# -f: fail on HTTP error (non-2xx exits non-zero)
# -L: follow redirects
curl -sfL "https://raw.githubusercontent.com/OWNER/REPO/TAG/action.yml" \
  | grep -E '^\s+using:' \
  || echo "ERROR: could not retrieve action.yml or 'using' field not found"

Automate this with Dependabot or Renovate. Configure either tool to monitor GitHub Actions version pins and open pull requests when new versions are available. Dependabot's built-in github-actions ecosystem support detects outdated action versions and proposes updates automatically, reducing the manual tracking burden significantly.

Handling Actions Without Node 24 Support Yet

Not every action maintainer will ship a Node 24-compatible release immediately. For actions that lag behind, pin to the latest known-good version and monitor the repository for updates. Opening an issue on the upstream repository requesting Node 24 support is both helpful and common practice. If the maintainer has stopped updating the action, search GitHub Marketplace for alternatives with recent commits and a runs.using: node24 declaration in their action.yml. Forking remains an option of last resort, as it transfers the ongoing maintenance burden to your team. Be aware that forking transfers full security responsibility, including vulnerability patching, to your team.

# BEFORE: Workflow using older action versions targeting Node 20 runtime
# DO NOT USE — deprecated example shown for comparison only
steps:
  # actions/checkout@v4 uses runs.using: node20
  - uses: actions/checkout@v4

  # actions/cache@v3 uses runs.using: node20
  - uses: actions/cache@v3
    with:
      path: ~/.npm
      key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
# AFTER: Updated to Node 24-compatible action versions with SHA pins
# IMPORTANT: Before pinning, verify each action's runs.using value:
#   curl -sfL https://raw.githubusercontent.com/actions/checkout/TAG/action.yml | grep -E '^\s+using:'
#   curl -sfL https://raw.githubusercontent.com/actions/cache/TAG/action.yml | grep -E '^\s+using:'
# Resolve current SHAs:
#   git ls-remote https://github.com/actions/checkout refs/tags/v4
#   git ls-remote https://github.com/actions/cache refs/tags/v4
steps:
  # Verify the Node 24-compatible checkout version at github.com/actions/checkout/releases before pinning
  - uses: actions/checkout@v4  # Replace with SHA pin, e.g. actions/checkout@11bd71901...

  # Verify the Node 24-compatible cache version at github.com/actions/toolkit/releases before pinning
  - name: Get npm cache directory
    id: npm-cache-dir
    shell: bash
    run: echo "dir=$(npm config get cache)" >> "$GITHUB_OUTPUT"

  - uses: actions/cache@v4  # Replace with SHA pin, e.g. actions/cache@6849a6489...
    with:
      path: ${{ steps.npm-cache-dir.outputs.dir }}
      key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
      restore-keys: |
        ${{ runner.os }}-node-

Step 3: Migrate Custom Action Metadata to Node 24

Updating action.yml: The runs.using Field

The runs.using field in action.yml tells the GitHub Actions runner which Node.js runtime to use when executing the action's entry point. For JavaScript actions, this field directly controls which bundled Node binary runs the code. The migration requires changing this value from node20 to node24.

# BEFORE: action.yml targeting Node 20
name: 'My Custom Action'
description: 'Performs automated deployment checks'
inputs:
  environment:
    description: 'Target deployment environment'
    required: true
runs:
  # This is the single required change for simple JavaScript actions
  using: 'node20'
  main: 'dist/index.js'
# AFTER: action.yml targeting Node 24
name: My Custom Action
description: Performs automated deployment checks
inputs:
  environment:
    description: Target deployment environment
    required: true
runs:
  using: node24          # Unquoted scalar; matches GitHub canonical format
  main: dist/index.js
  # pre: dist/pre.js     # Uncomment if action uses pre: hook; inherits same runtime
  # post: dist/post.js   # Uncomment if action uses post: hook; inherits same runtime

Self-hosted runners: Before publishing, confirm your runner binary supports node24. Check https://github.com/actions/runner/releases for the minimum required version. Self-hosted runners must be updated to a runner version that includes the Node 24 runtime before runs.using: node24 will work. On your runner, verify with:

# On the self-hosted runner machine:
./config.sh --version
# Expected output: 2.NNN.N  (e.g., 2.317.0)
# Cross-reference against https://github.com/actions/runner/releases
# for the minimum version that bundles the Node 24 runtime.

If the action uses only @actions/core and standard Node.js APIs, this field change is the only metadata edit needed. The complexity arises when the action's dependencies include native modules or rely on APIs that changed between Node 20 and Node 24.

Rollback strategy: Before merging, tag a pre-migration release of your action. If Node 24 runtime issues emerge, consumers can pin to the pre-migration tag while you investigate. This is especially critical for widely consumed actions, since publishing a changed action.yml with node24 affects all consumers who track the tag.

Before merging, tag a pre-migration release of your action. If Node 24 runtime issues emerge, consumers can pin to the pre-migration tag while you investigate.

Testing Your Custom Action After the Update

Run the action's test suite on Ubuntu, Windows, and macOS runners before publishing. The nektos/act tool allows running GitHub Actions workflows locally. Before relying on it, verify that your version of act supports the node24 runtime at https://github.com/nektos/act/releases. Create a test workflow that exercises the action's primary code paths and run it against all three runner operating systems. Platform-specific behavior differences are common, particularly around file paths, shell execution, and native module loading. A dedicated test workflow in the action's own repository, triggered on pull requests, catches regressions before they reach consumers.

Step 4: Audit Native Module and Engine Compatibility

Identifying Native Module Issues

Native or compiled Node modules, particularly those using node-gyp or N-API, may break when the runtime jumps from Node 20 to Node 24. These modules compile against the Node.js C++ ABI, and major version bumps can introduce incompatibilities. To identify native dependencies in an action's dependency tree, use a multi-layered approach:

# Locate compiled native bindings (.node and .wasm files)
find node_modules -name '*.node' -o -name '*.wasm' | sort

# Identify packages that invoke node-gyp or node-pre-gyp at install time
npm ls --all 2>/dev/null \
  | grep -Ei "gyp|napi|native|bindings|prebuild" \
  || echo "No obvious native module references found"

# Cross-check package.json files for gypfile declarations
grep -rl '"gypfile": true' node_modules --include='package.json' 2>/dev/null

An empty result across all three commands means no native modules are present. Any dependency that compiles native code during npm install is a candidate for breakage.

Checking engines Fields

Many packages declare supported Node.js versions via the engines field in package.json. A dependency with "engines": { "node": ">=14 <21" } will refuse to install under Node 24 when engine-strict is enabled, or will emit warnings otherwise. Review these constraints across all direct and transitive dependencies. Update or relax engines fields where appropriate, but only after verifying actual compatibility. Do not widen engines ranges to suppress warnings without running the full test suite under Node 24; suppressing the warning without testing can mask runtime failures. Run the full test suite locally under Node 24 before committing any changes.

Common Compatibility Issues

The jump from Node 20 to Node 24 spans multiple intermediate major versions (Node 21, 22, 23, and 24), each carrying V8 engine upgrades, OpenSSL version changes, and the removal of previously deprecated Node.js APIs. Consult the Node.js 22 and Node.js 24 migration guides for removed or changed APIs specific to those versions; legacy url.parse behavior and older Buffer constructor patterns are common candidates for breakage. See https://nodejs.org/en/docs/guides/. Replacing a deprecated API call like url.parse() with new URL() is a one-line fix; waiting for a native dependency like node-sass to release Node 24-compatible bindings may take weeks. Check each dependency's issue tracker for Node 24 status before assuming the fix is on your side.

Migration Checklist and Quick-Reference

Native Module and Engines Compatibility Checklist:

  1. Run find node_modules -name '*.node' -o -name '*.wasm' to locate compiled native bindings in action dependencies.
  2. Run npm ls --all 2>/dev/null | grep -Ei "gyp|napi|native|bindings|prebuild" to identify packages that invoke native compilation at install time.
  3. Check each native module's Node 24 support status in its repository or changelog.
  4. Verify the engines field in package.json permits Node 24.
  5. Run the full test suite under Node 24 locally.
  6. Consult the Node.js 22 and 24 migration guides for removed or changed APIs.
  7. Rebuild any node-gyp bindings with Node 24 (npm rebuild under a Node 24 environment).
  8. Update lockfiles (package-lock.json) after dependency bumps.

Troubleshooting Common Migration Errors

"This action was built using Node 20" Warning

The runner prints this warning when a step executes an action whose action.yml still declares runs.using: node20. During the deprecation period, this is a non-blocking warning. This becomes a hard failure once GitHub removes the Node 20 runtime. Do not treat the warning period as indefinite. The fix is updating the action to a version that targets Node 24, either by bumping the version pin for third-party actions or by changing the runs.using field in custom actions.

GLIBC Version Mismatch on Older Self-Hosted Runners

Node 24 requires glibc 2.28 or later. Verify your runner's version with ldd --version; CentOS 7 (glibc 2.17) is incompatible. Consult Node 24 release notes for the authoritative minimum. Self-hosted runners operating on older Linux distributions (for example, CentOS 7) may lack the required glibc version, producing errors at action startup. Ubuntu 20.04 ships glibc 2.31 and is compatible. The resolution is either upgrading the runner's operating system to a version that ships a compatible glibc, or switching to container-based jobs that use a sufficiently recent base image.

Action Fails Only on Windows/macOS

Platform-specific native module issues are the most common culprit when an action passes on Ubuntu but fails on Windows or macOS. Native modules may compile successfully on one platform and fail on another due to toolchain differences, missing system libraries, or path handling assumptions. Diagnose these by examining the error output for compilation failures or missing bindings, and check the module's issue tracker for known platform-specific Node 24 incompatibilities.

The Node 20 deprecation on GitHub Actions runners targets the action execution runtime, not the Node.js version used for project builds and tests.

Wrapping Up

The Node 20 deprecation on GitHub Actions runners targets the action execution runtime, not the Node.js version used for project builds and tests. Migration requires attention in three areas: updating version pins for third-party actions to Node 24-compatible releases, changing the runs.using field in custom action.yml files from node20 to node24, and auditing native module and engine compatibility across action dependencies. Teams that migrate proactively avoid the disruption of hard failures when GitHub removes the Node 20 runtime entirely.

Authoritative references for tracking timelines and compatibility:

SitePoint TeamSitePoint Team

Sharing our passion for building incredible internet things.

© 2000 – 2026 SitePoint Pty. Ltd.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.