From fc77ef834f6c613bcba278d41051f61d9ee78da5 Mon Sep 17 00:00:00 2001 From: Booyaka101 Date: Sun, 20 Sep 2026 10:13:44 +0800 Subject: [PATCH 01/44] 1.4.0: the month ubuntu-latest is two operating systems GitHub is re-pointing ubuntu-latest from Ubuntu 24.04 to 26.04 between 2026-10-19 and 2026-11-19 (actions/runner-images#14748). For that month the label means either image depending on which runner a job lands on, and nothing in the lock file can tell you which one you got. MIGRATIONS in src/labels.mjs holds the announced windows, keyed by floating label, separate from DEADLINES because a deadline retires a concrete label and offers destinations you pick between, while a migration is a floating label being re-pointed on dates you do not. One entry today; windows-latest and macos-latest are a data addition. - plan --from ubuntu-latest resolves the pair and diffs the two images, kernel and systemd included, instead of refusing the label. - guard --fail-on-migration classifies the window against the ImageOS the runner exported: pending, moved-early, not-yet-migrated, ambiguous, migrated, settled, stale, unexpected. Off by default. fail-on, fail-on-retirement and fail-on-deprecation are untouched. - Drift that the announced move explains says so, from the table, with no flag and no extra request. - --as-of moves every countdown, so the month can be rehearsed now. - All four output formats carry it: text, annotations on the runs-on: line, step summary, --json. 319 tests pass, and the same 319 pass with fetch replaced by a throw. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 32 + CHANGELOG.md | 85 +++ README.md | 141 ++++- action.yml | 8 +- package.json | 4 +- src/cli.mjs | 230 ++++++-- src/detect.mjs | 37 +- src/http.mjs | 2 +- src/index.mjs | 16 + src/labels.mjs | 171 +++++- src/manifest.mjs | 64 +- src/migration.mjs | 105 ++++ src/report.mjs | 172 +++++- test/cli.test.mjs | 45 +- .../Ubuntu2404-Readme-20260907.300.1.md | 333 +++++++++++ .../Ubuntu2604-Readme-20260907.131.1.md | 316 ++++++++++ test/fixtures/workflows-migration/latest.yml | 18 + test/helpers.mjs | 19 +- test/migration.test.mjs | 558 ++++++++++++++++++ 19 files changed, 2261 insertions(+), 95 deletions(-) create mode 100644 src/migration.mjs create mode 100644 test/fixtures/Ubuntu2404-Readme-20260907.300.1.md create mode 100644 test/fixtures/Ubuntu2604-Readme-20260907.131.1.md create mode 100644 test/fixtures/workflows-migration/latest.yml create mode 100644 test/migration.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18efae7..9f79336 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,6 +134,38 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} + migration: + # Deliberately floating. Every other job here pins its image; this one is the + # lane's subject, and reading ImageOS off a runner that GitHub is in the + # middle of re-pointing is the half no test can reach. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: '22' + + - name: plan the announced ubuntu-latest move against the live manifests + run: node src/cli.mjs plan --from ubuntu-latest + env: + GITHUB_TOKEN: ${{ github.token }} + + # The exit code is deliberately not asserted: it is 1 while the window is + # ahead of this runner and 0 once the move has reached it, so pinning it + # would turn the rollout itself into a red build. What must hold on every + # run is that the lane resolved the label and named its source. + - name: guard --fail-on-migration on a real floating runner + run: | + set -uo pipefail + out="$(node src/cli.mjs guard --tools node --fail-on-migration 30 --no-update-lock 2>&1)" + code=$? + printf '%s\n' "$out" + echo "exit $code" + grep -q 'ubuntu-latest' <<<"$out" || { echo 'the migration lane said nothing'; exit 1; } + grep -q 'actions/runner-images#14748' <<<"$out" || { echo 'no source named'; exit 1; } + env: + GITHUB_TOKEN: ${{ github.token }} + runners: runs-on: ubuntu-24.04 steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index ecb700c..639ca51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,91 @@ All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.4.0] — 2026-09-20 + +### Added + +- **The `ubuntu-latest` migration lane.** GitHub's changelog of 2026-09-17: *"The + ubuntu-latest label will migrate from Ubuntu 24.04 to Ubuntu 26.04. This + migration will roll out gradually between October 19 and November 19, 2026."* + ([actions/runner-images#14748](https://github.com/actions/runner-images/issues/14748)). + + For that month `ubuntu-latest` is two different operating systems depending on + which runner the job lands on, and no workflow file changes. The kernel goes + `6.17.0-1022-azure` -> `7.0.0-1012-azure` and systemd `255.4-1ubuntu8.17` -> + `259.5-0ubuntu3.4`, while Docker Buildx, the AWS and Azure CLIs, Rust and Java + 17 are the same on both images. That is exactly the kind of change this tool + exists to show you before it lands. + + `MIGRATIONS` in `src/labels.mjs` records the announced moves: the floating + label, the two concrete labels it moves between, the window and the source + issue. It sits beside the retirement `DEADLINES` table rather than inside it, + and shares no keys with it: a retirement is an end date for one image, a + migration is a dated window between two live ones. Today it has one entry. + `windows-latest` and `macos-latest` are a data addition when GitHub announces + them. + +- **`plan --from `** no longer refuses. Where `MIGRATIONS` names + both ends, `plan` resolves them and diffs the two concrete images, so the + output is the kernel and systemd deltas rather than a bare warning: + + ``` + $ runner-drift plan --from ubuntu-latest + ubuntu-latest moves from ubuntu-24.04 to ubuntu-26.04. The rollout starts 2026-10-19 (18 days) and finishes 2026-11-19 (49 days). + announced 2026-09-17; source actions/runner-images#14748 https://github.com/actions/runner-images/issues/14748 + ubuntu-24.04 -> ubuntu-26.04 (images 20260907.300.1 -> 20260907.131.1) + + OS 24.04.5 LTS -> 26.04.1 LTS MAJOR + Kernel 6.17.0-1022-azure -> 7.0.0-1012-azure MAJOR + Systemd 255.4-1ubuntu8.17 -> 259.5-0ubuntu3.4 MAJOR + ``` + + A floating `--to`, or a floating `--from` with no announced migration, is + refused exactly as before. `plan` also gained the three image-header rows (OS, + kernel, systemd) for every comparison, not just this one, since they were being + parsed already and dropped. + +- **`guard --fail-on-migration `**, and the matching `fail-on-migration` + action input. Off by default, like `fail-on-retirement`: without it `guard` + does not fetch the two manifests at all. With it, each floating `runs-on:` line + is annotated according to where today sits in the window and which `ImageOS` + the runner exported: pending, this runner has not moved yet, the migration has + reached this runner, done, or, past the window on the old image, an anomaly. + The anomaly fails whatever the threshold, the same rule a label already past + its retirement date follows. + + When the observed image explains the tool drift `guard` just found, the report + says so, rather than leaving a page of major bumps looking unexplained. That + line needs no input: a lock recorded on one side of an announced move and a + runner on the other is a table lookup, not a check to opt into. + + ``` + Explained by the scheduled ubuntu-latest migration ubuntu-24.04 -> ubuntu-26.04 (2026-10-19 to 2026-11-19); the tool versions below moved with the image. + ``` + + `--fail-on`, `--fail-on-retirement` and `--fail-on-deprecation` are untouched, + and so are all four output formats' existing contents: the migration is a new + annotation group, a new step-summary table (Label, Phase, Move, Window, This + runner, Source) and a new `migration` key in `--json`. + +- **`--as-of `** on every command. It moves the clock every countdown is + measured from and nothing else, so `plan --from ubuntu-latest --as-of + 2026-10-19` answers what the report will say on the first day of the rollout + without pretending the run happened then. It is also what makes the migration + tests deterministic. + +- `init` refusing a floating label now points at the migration when there is one, + instead of only saying "pass a concrete label". + +### Changed + +- `resolveManifestVersions` moved from `src/cli.mjs` to `src/manifest.mjs`, where + the rest of the manifest reading lives, and is re-exported from `cli.mjs` so + the public surface is unchanged. The migration lane needed it and importing it + from the CLI would have made a cycle. + +[1.4.0]: https://github.com/Booyaka101/runner-drift/releases/tag/v1.4.0 + ## [1.3.0] — 2026-09-13 ### Added diff --git a/README.md b/README.md index 77974b6..ea9d709 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ rolling: you have 30 days from each `actions/runner` release to install it, or [the Actions service stops queueing jobs to your runner](https://docs.github.com/en/actions/reference/runners/self-hosted-runners). `runner-drift runners` reads the dates straight from GitHub's API and names the runners that are about to go quiet. See -[Self-hosted agent versions](#5-runner-drift-runners--self-hosted-agent-versions). +[Self-hosted agent versions](#6-runner-drift-runners--self-hosted-agent-versions). Since 1.3.0 it watches a third clock, and this one is nearly out. GitHub [removes Node 20 from the hosted runner images on 2026-09-23](https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/), @@ -56,7 +56,16 @@ looks like a pin, and it is `node20`. So is `actions/upload-artifact@v4`. `runner-drift actions` resolves every `uses:` to the runtime the action really declares, follows composites and reusable workflows into whatever they call, and names the step that breaks rather than the line you wrote. See -[which `uses:` survive](#6-runner-drift-actions--which-uses-survive-the-node-20-removal). +[which `uses:` survive](#7-runner-drift-actions--which-uses-survive-the-node-20-removal). + +Since 1.4.0 it also covers the case where the label does not change but the image +does. GitHub is moving `ubuntu-latest` from Ubuntu 24.04 to Ubuntu 26.04, rolling +out [between 2026-10-19 and 2026-11-19](https://github.com/actions/runner-images/issues/14748). +For that month `ubuntu-latest` is two different operating systems depending on +which runner your job lands on, and nothing in your workflow file changes. `plan` +resolves the floating label to the two concrete images GitHub named and diffs +them, so you get the kernel and systemd deltas before the rollout reaches you. +See [when a floating label moves](#5-when-a-floating-label-moves-under-you). - No account, no hosted service, no paid tier. Two endpoints only: `raw.githubusercontent.com` and `api.github.com`. @@ -153,8 +162,13 @@ not move, so it is not in the table. runner-drift plan --from ubuntu-22.04 --to ubuntu-24.04 runner-drift plan --from macos-14 --to macos-15 --tools python,node,dotnet runner-drift plan --from ubuntu-22.04 --to ubuntu-26.04 --json +runner-drift plan --from ubuntu-latest # resolved from the MIGRATIONS table ``` +`--to` is required except for a floating label with an announced migration, where +GitHub has already named both ends. See +[when a floating label moves](#5-when-a-floating-label-moves-under-you). + ### 4. Fail before the brownout Deprecated images get scheduled brownouts before removal: `macos-14` jobs fail @@ -191,11 +205,93 @@ When only a brownout falls inside the threshold the annotation is a `::warning` dates your builds break. The step summary gets a table (Label, Where, Next brownout, Fully unsupported, Migrate to, Source), `--json` gets a `retirement` block, and a label already past its date always fails, whatever the threshold. -`ubuntu-latest` and friends float past retirements, so they are never flagged; -neither is `self-hosted`. `runs-on: ${{ matrix.os }}` is resolved from the +`ubuntu-latest` and friends float past retirements, so this lane never flags +them; they get their own lane, [below](#5-when-a-floating-label-moves-under-you). +`self-hosted` is never flagged at all. `runs-on: ${{ matrix.os }}` is resolved from the matrix values in the same file. -### 5. `runner-drift runners` — self-hosted agent versions +### 5. When a floating label moves under you + +`runs-on: ubuntu-latest` is not a pin, and once a year that matters. GitHub +announced on 2026-09-17 that the label +[migrates from Ubuntu 24.04 to Ubuntu 26.04 between 2026-10-19 and 2026-11-19](https://github.com/actions/runner-images/issues/14748). +During the rollout the label means whichever image your job happens to land on, +so a build can pass and fail on the same commit with nothing to diff. + +`src/labels.mjs` carries the announced moves in a `MIGRATIONS` table, keyed by +floating label, holding the two concrete labels, the window and the source issue. +That is enough for `plan` to stop refusing the floating label and diff the two +real images instead: + +``` +$ npx runner-drift plan --from ubuntu-latest +ubuntu-latest moves from ubuntu-24.04 to ubuntu-26.04. The rollout starts 2026-10-19 (18 days) and finishes 2026-11-19 (49 days). +announced 2026-09-17; source actions/runner-images#14748 https://github.com/actions/runner-images/issues/14748 +ubuntu-24.04 -> ubuntu-26.04 (images 20260907.300.1 -> 20260907.131.1) +ubuntu-24.04 has no announced deprecation deadline in runner-drift's table. + +OS 24.04.5 LTS -> 26.04.1 LTS MAJOR +Kernel 6.17.0-1022-azure -> 7.0.0-1012-azure MAJOR +Systemd 255.4-1ubuntu8.17 -> 259.5-0ubuntu3.4 MAJOR + +CMake 3.31.6 -> 4.4.3 MAJOR +Node.js 22.23.2 -> 24.20.0 MAJOR +Python 3.12.3 -> 3.14.4 MINOR + +3 of 3 detected tool(s) change +``` + +(Real output against the live manifests, run with `--as-of 2026-10-01` for the +countdown and the fixture workflow in `test/fixtures/workflows-migration`.) + +In CI, `guard --fail-on-migration ` scans the `runs-on:` lines for floating +labels with an announced move and fails while the window is still ahead of you: + +```yaml + runner-migration: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: Booyaka101/runner-drift@v1 + with: + fail-on-migration: 30 # or: npx runner-drift guard --fail-on-migration 30 +``` + +It is opt-in, like `fail-on-retirement`: without the input, `guard` does not fetch +the two manifests at all. What it reports depends on where today sits in the +window, and on the `ImageOS` the runner exported: + +| Phase | `ImageOS` | Reported as | +| --- | --- | --- | +| Before the window | — | `::notice`, the window and the countdown | +| In the window | the old image | `::warning`, this runner has not moved yet and the diff is still ahead of you | +| In the window | the new image | `::notice`, the migration has reached this runner | +| In the window | absent | `::warning`, which of the two this job got cannot be told | +| After the window | the new image | `::notice`, the move is done | +| After the window | the old image | `::error`, an anomaly rather than drift | + +The last row is the one worth having. A runner still serving Ubuntu 24.04 in +December, when the label is supposed to mean 26.04 everywhere, is not a version +bump you should record in the lock file. + +One part of this needs no input at all. When `guard` finds real tool drift and the +jump from the lock's image to the runner's image is exactly an announced move, it +says so, because that is a table lookup rather than a check you have to opt into: + +``` +ubuntu-26.04 image 20260907.300.1 -> 20260907.131.1 +Explained by the scheduled ubuntu-latest migration ubuntu-24.04 -> ubuntu-26.04 (2026-10-19 to 2026-11-19); the tool versions below moved with the image. + CMake 3.31.6 -> 4.4.3 MAJOR [20260907.131.1] https://github.com/actions/runner-images/commit/90ebb6f508835349239c3823c663eba7af98f23d +``` + +Exit codes and the `--fail-on` thresholds are untouched by any of this. An +anomaly fails whatever the threshold, the same rule `--fail-on-retirement` uses +for a label already past its date. `--json` gains a `migration` block, and the +step summary gains a table (Label, Phase, Move, Window, This runner, Source). +Adding `windows-latest` or `macos-latest` later is a data change in `MIGRATIONS`, +nothing else. + +### 6. `runner-drift runners` — self-hosted agent versions Two clocks run on a self-hosted runner. The image one does not apply, since you built the machine. The **agent** one does. GitHub requires each new @@ -353,7 +449,7 @@ listing and reports that runner's own dates in the summary table and `--json`. Without a token, or without the permission, it prints the same `::notice` it printed in 1.1.0 and exits 0. That is the common case, not an error path. -### 6. `runner-drift actions` — which `uses:` survive the Node 20 removal +### 7. `runner-drift actions` — which `uses:` survive the Node 20 removal GitHub switched the hosted runners' default action runtime to Node 24 on 2026-06-16 and [removes Node 20 from the images on 2026-09-23](https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/). @@ -451,9 +547,10 @@ summary table, and sets a `will-fail-count` output. | `--lock-file ` | `init`, `guard` | `runner-lock.json` | Lock file location | | `--tools ` | all | detected | Override detection. Aliases (`python`, `npx`, `clang++`, `g++`, `javac`, …) resolve to manifest names; anything else is matched against the manifest case-insensitively, so `--tools Terraform,Kotlin` works | | `--label