The ubuntu-latest label moves to Ubuntu 26.04 and breaks workflows that pin nothing
On 17 September 2026 GitHub announced that the ubuntu-latest label is moving from Ubuntu 24.04 to 26.04, rolling out gradually between 19 October and 19 November 2026. The migration ships a JDK jump from 17 to 25, major-version bumps for Docker Compose and Helm, and the removal of a dozen preinstalled tools: workflows that don’t pin their runtime will break.
17 September 2026. GitHub announces that the ubuntu-latest label is leaving Ubuntu 24.04 for Ubuntu 26.04. 19 October to 19 November 2026. The cutover happens gradually, machine by machine, across a one-month window. JDK 17 → 25. The default Java runtime jumps two generations, and Docker Compose, Helm and CMake each cross a major version. Why it matters: a workflow that writes runs-on: ubuntu-latest and trusts “whatever Ubuntu GitHub hands me” will, this time, break — without anyone touching a line of the repo.
What makes this migration different
GitHub ships a new runner image on basically every Ubuntu LTS, and most of the time nobody notices because ubuntu-latest quietly carries you along. What makes this edition special is the size of the internal jumps. This is not a patch-level refresh, it is a major-version cutover combined with tool removals, and the rollout window is gradual: for a month, two concurrent runs of the same pipeline can be on 24.04 and 26.04 with no change on your end.
The change table, checked against the official actions/runner-images manifests, gives the scale of the jump:
| Component | Ubuntu 24.04 (current) | Ubuntu 26.04 (incoming) |
|---|---|---|
| Java (system default) | 17.0.20 | 25.0.4 |
| Python (system) | 3.12.3 | 3.14.4 |
| Node.js (system) | 22.23.2 | 24.20.0 |
| Ruby (system) | 3.2.3 | 3.3.8 |
| PHP | 8.3.6 | 8.5.4 |
| Docker Client/Server | 28.0.4 | 29.4.2 |
| Docker Compose | 2.38.2 | 5.1.3 |
| Helm | 3.21.4 | 4.2.4 |
| CMake | 3.31.6 | 4.4.3 |
| PostgreSQL | 16.15 | 18.6 |
| MySQL | 8.0.46 | 8.4.11 |
| Podman | 4.9.3 | 5.7.0 |
The JDK jump from 17 to 25 is the one that will hit the most people first, because a large share of Maven and Gradle builds never pin a Java version explicitly and just trust whatever JAVA_HOME points at. Docker Compose and Helm also cross a major boundary, which usually means CLI flag and file-schema changes — not a simple version-string bump.
The tools that vanish, and the packages that rename
The sneakiest part is not the upgrade, it is the disappearance. These tools are present on ubuntu-24.04 and simply absent from ubuntu-26.04: a job that depends on them will fail with command not found, with no prior warning:
- Java 8 — only 11, 17, 21 and 25 ship now
- Miniconda — the
CONDAvariable is present but empty - Swift, Julia, Pulumi, Fastlane, Mercurial, Newman, Parcel, Lerna, MediaInfo, Sphinx
Two apt package names also change: p7zip-full and p7zip-rar become 7zip and 7zip-rar, and dnsutils becomes bind9-dnsutils. A step that runs an explicit apt-get install -y p7zip-full or dnsutils breaks on 26.04, even though the underlying functionality still exists under a new name.
The trap is the silence. An image that drops Java 8 does not warn you: the build fails at compile time, weeks after the announcement, on a runner you did not choose to change.
Self-hosted runners are outside this migration
One clarification before anyone panics: this migration only touches GitHub-hosted runners. A self-hosted runner registers with its own labels — self-hosted, Linux, your pool name — and ubuntu-latest is either absent there or defined by you. GitHub will not update the OS of a machine you administer: if your self-hosted runner runs 24.04, it stays on 24.04 until you re-provision it.
The confusion comes from one special case: teams that mirror GitHub’s official images onto their own runners, through mirrored images or agents installed on Ubuntu VMs. Those move when you move them, not when GitHub flips its label. The point of vigilance is therefore the tool versions: if your self-hosted runner carries a JDK 17 and your build relies on it, GitHub’s migration will neither save nor break you — your image is what decides.
Finding the exposed workflows before 19 October
The audit starts with one simple question: how many of your repos write ubuntu-latest — or, worse, specify nothing at all? GitHub code search answers in a single query, and a local script sweeps an organisation:
# Count uses of ubuntu-latest in a local clone of an organisation:
grep -Rl "runs-on: ubuntu-latest" --include="*.yml" --include="*.yaml" . 2>/dev/null | wc -l
# Also find workflows that specify NO runs-on at all (implicit default):
grep -RL "runs-on:" --include="*.yml" --include="*.yaml" . 2>/dev/null The second case is the more dangerous one: a workflow with no runs-on: inherits an implicit default that will also point at 26.04 eventually. The resulting list is your test perimeter between now and 19 October, no more and no less.
Two options, and one underlying good practice
Between now and 19 October you have exactly two responsible options. Doing nothing and hoping is not one of them, since the rollout is gradual and non-deterministic from your side.
Option A — pin the current image if you are not ready. Just swap the label for the explicit version:
jobs:
build:
runs-on: ubuntu-24.04 # instead of ubuntu-latest
steps:
- uses: actions/checkout@v4 Option B — test the new image now with ubuntu-26.04, ideally in a matrix so you compare both during the transition:
jobs:
build:
strategy:
matrix:
os: [ubuntu-24.04, ubuntu-26.04]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4 But pinning the runner only treats the symptom. The underlying good practice — the one that survives every migration — is to pin the runtimes themselves through the dedicated actions, actions/setup-java, actions/setup-python, actions/setup-node, rather than trusting the system default. A build that declares java-version: 21 explicitly does not move, whether the runner label points at 24.04 or 26.04.
The Java 8 nuance: setup-java keeps working
One point deserves stating plainly, because it prevents a false alarm: the removal of Java 8 only affects the image’s system default. If your builds obtain Java 8 through actions/setup-java with java-version: 8, they keep working on 26.04 — the action downloads its own JDK, independent of what the image preinstalls. The breakage only hits workflows that invoke the system java and assume it points at version 8.
The same logic applies to Miniconda: teams that used it through the preinstalled CONDA variable will need to install their environment explicitly, but those already going through conda-incubator/setup-miniconda will see nothing. The boundary is not “the tool disappears” — it is “the preinstalled tool disappears”, and anything provisioned through a setup action stays intact. That is one more reason to pin runtimes with setup-* rather than depending on the image’s contents: the layer that shields you from migrations is the one you control explicitly.
Verdict
If your workflows lean on the system default — JAVA_HOME, python, node with no setup action — you have a deadline: 19 October, the first day of the cutover window. Pin ubuntu-24.04 until you stabilize, then test ubuntu-26.04 in a matrix before the label flips on its own. If you depend on a removed tool — Java 8, Miniconda, Pulumi, Fastlane, Newman — pinning ubuntu-24.04 is only a stay of execution: the removal is permanent, so migrate the tooling or install it explicitly inside the job. If you already pinned your runtimes via setup-*, you are the exception that sleeps through it: the migration happens underneath you with no visible effect. The rule fits in one line: a latest label is a moving cursor, and anything you do not pin will eventually get moved by someone else.