Changing one word in a workflow file feels harmless. runs-on: ubuntu-24.04 becomes runs-on: ubuntu-26.04, and you've swapped the operating system under every build step you own.
I haven't run a production pipeline on the new image yet, so this isn't a benchmark. What I did: read the contents lists GitHub publishes for both images (image versions 20260927.149.1 for 26.04 and 20261004.327.1 for 24.04), diffed the parts that matter to a Node.js project, and wrote down the order I'd test them in. The versions below are quoted from those lists, and image contents change weekly, so recheck before you rely on a number.
Is ubuntu-26.04 Ready to Use for CI?
It exists and you can target it. The labels are ubuntu-26.04 for x64 and ubuntu-26.04-arm for Arm64. GitHub announced the image as a public preview on June 11, 2026, with the warning that preview images may have longer queue times at peak hours and different tools than earlier images.
Is it still in preview? The runner-images README table now lists 26.04 as a plain row, without the preview badge that its Xcode 27 row carries. I didn't find a general-availability announcement in what I read, though, so I'd check the repository before pointing a required status check at it. And ubuntu-latest? Still Ubuntu 24.04 in that README. Per its own rules, GitHub says it will announce a move of the -latest label with lead time.
Ubuntu 26.04 LTS itself came out on April 23, 2026, with standard support to April 2031. Plenty of runway. The question is only when you switch, not whether.
What Differs Between the Two Images?
Here's the table I'd tape to the monitor. All values come from the two image readmes named above.
| Item | ubuntu-24.04 | ubuntu-26.04 |
|---|---|---|
Default node on PATH | 22.23.3 | 24.21.0 |
| Cached Node versions | 22.23.3, 24.21.0 | 22.23.3, 24.21.0 |
| Python | 3.12.3 | 3.14.4 |
| OpenSSL (system) | 3.0.13 | 3.5.5 |
| Kernel | 6.17.0 (azure) | 7.0.0 (azure) |
| Bash | 5.2.21 | 5.3.9 |
| Docker client | 28.0.4 | 29.4.2 |
| Docker Compose | 2.38.2 | 5.1.3 |
| Podman | 4.9.3 | 5.7.0 |
| g++ | 13.2.0 | 15.2.0 |
| Yarn (classic) | 1.22.22 | 1.22.22 |
Two rows deserve a second look. The cached Node versions are identical, so anything that calls actions/setup-node with an explicit version behaves the same on both. The default node on PATH is not identical, and that's the trap.
Why Does the Default Node Version Matter So Much?
Because plenty of workflows never install Node at all. They call npm ci and rely on whatever the image ships. On 24.04 that's Node 22. On 26.04 it's Node 24. You haven't touched your package.json, yet your CI now runs a different major version, with a different V8, different require(esm) behavior and different deprecations. If a build goes red after the label change, this is the first suspect.
Never let a CI image decide your Node version for you: the line that says which runtime your tests ran on belongs in your repository, not in someone else's runner changelog.
The fix is one step, and it costs about a second when the version is cached:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: node --version && npm --version && python3 --version && openssl version
- run: npm ci
- run: npm test
The node --version line looks pointless. It isn't. When the same job is green on one image and red on another, that single log line is usually the whole diagnosis, and it saves you from scrolling through installation noise. I would add it to every workflow.
If your project is still on Node 20, the Node.js 20 to 24 migration guide covers what breaks at the language level, and our Node.js 24.19.0 notes cover one of the releases in the 24.x line these images carry.
What Breaks When Python and the Compiler Move?
Pure JavaScript projects mostly won't notice. Projects with native addons might.
Node's own binaries bundle their own OpenSSL, so the system OpenSSL jump from 3.0 to 3.5 matters mainly to native modules linked against the system library and to anything shelling out to openssl or curl. The part more likely to bite is node-gyp. It uses Python and the system C++ compiler to build addons that don't ship prebuilt binaries, and the 26.04 image moves Python from 3.12.3 to 3.14.4 and ships g++ 15.2. An older addon that compiled quietly on a 2024-era toolchain can fail on a 2026 one. I haven't hit that on this image, so I can't name a package. I just know which step to look at: npm ci or npm rebuild, in the logs, before any test runs.
Docker is the other place to look. Compose went from 2.38.2 to 5.1.3 across the two images, a jump in major version number, so a workflow that parses docker compose output or relies on an older flag deserves a dry run.
How Do You Test the Switch Without Risking the Main Branch?
Add the new OS as an extra matrix leg that is allowed to fail, then promote it once it's green for a week.
jobs:
test:
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04]
node: [22, 24]
experimental: [false]
include:
- os: ubuntu-26.04
node: 24
experimental: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: node --version && python3 --version
- run: npm ci
- run: npm test
fail-fast: false keeps one red leg from cancelling the others, and continue-on-error stops the experimental leg from blocking merges. Two weeks of green on the extra leg is a decent bar. Then flip experimental to false and drop the old OS when you're ready.
Watch out for one thing: a required status check is matched by job name. Renaming or reshaping the matrix changes the names, and a required check that never reports leaves a pull request stuck. Change the branch protection rule in the same sitting as the workflow.
The same approach works for the type-check job in our TypeScript 7 CI guide. It's a short job, so the experimental leg costs almost nothing.
Should You Move Now?
My view: add the matrix leg this week, switch the primary label later. There's no deadline pushing you. Ubuntu 24.04 stays supported for years, and ubuntu-latest hasn't moved. But the default-Node change is silent, and the cheapest day to discover it is a day when nothing else is broken.
Ready to try it? Pin Node, print the versions, add one experimental leg. Then read the logs of that first run before anything else. If it comes back green on the first try, you've lost ten minutes. If it doesn't, you've found out on your terms.