repomatic.runner_images module¶
Keep a repository’s runner images current against what GitHub still offers.
A runs-on: value is the one dependency in a workflow that nothing bumps:
Dependabot rewrites uses: references, sync-workflow-pins rewrites version
literals, and neither touches a runner image. So an image retires on GitHub’s
schedule, entirely outside this repository’s view, and the first sign is a
failing build.
The source is the Available Images table
(repomatic.runner_catalog), compared against the labels this repository
actually runs. Nothing else is read.
Note
Why the table and not the announcement feed
This module previously polled the Announcement-labelled issues of
actions/runner-images, and the two questions turn out to be different ones.
The feed reports what changed for anyone; the table reports what is true
for me, and only the second decides anything. Polling produced an issue whose
every row was an image this repository either already ran or never would.
Two things are given up, both deliberately. GitHub badges an image
deprecated when deprecation begins rather than when it is announced, so a
retirement surfaces here months later than the feed would have shown it: for
Ubuntu 22.04, September rather than June. What remains is still ample, since
the badge lands well before the image stops working. And a change to the
contents of an image already in use, like a default toolchain moving, is
invisible in the table; the test suite is what catches those.
Caution
An unreadable or restyled table yields an empty catalog, and every caller here
reads that as “propose nothing” rather than “nothing exists”. Failing closed
costs a cycle of not noticing; failing open would rewrite a runs-on: to an
image GitHub does not host, taking every job with it.
- repomatic.runner_images.LEGACY_ISSUE_TITLE = 'GitHub runner image announcements'¶
Title of the issue this module used to maintain, closed on sight.
Dropping the announcement feed stopped anything from managing that issue, and an issue nothing manages never closes: every repository that ran the old version would keep one open forever, listing announcements no longer read. Closing it from here is the issue-shaped equivalent of a
RemovedAssettombstone.
- class repomatic.runner_images.RunnerChange(kind, label, successor, locations, reason, alternative)[source]¶
Bases:
objectOne runner-image edit the available-images table justifies.
- kind: str¶
retirementwhen the current image is going away,upgradewhen a strictly newer version of it exists.
- repomatic.runner_images.plan_runner_changes(literal, tracked, catalog, ignore=())[source]¶
Work out which runner-image edits the table justifies.
Every label this repository runs is looked up in the table, and yields at most one change:
Retirement. The row is badged deprecated, or the label is absent from the table entirely, which means the image is already gone. Jobs naming it outright move to
successor_for()’s pick. Only literalruns-on:values are reachable: one built from an expression draws on a matrix axis, which is the axis owner’s to move.Upgrade. A strictly newer version exists. It joins the full matrix as a
continue-on-errorprobe rather than replacing anything, so nothing is bet on it while the suite starts exercising it.
Strictly newer by version is what separates an upgrade from a flavour.
Windows 11 Arm64 with Visual Studio 2026sits at the same version asWindows 11 Arm64: a different toolchain, not a newer image, and proposing it as an upgrade would be wrong.- Parameters:
literal (
Mapping[str,Sequence[str]]) – Labels named outright in workflows, mapped to their locations, asliteral_runners()reports them.tracked (
Iterable[str]) – Every image this repository has a stake in.catalog (
Sequence[RunnerImage]) – Parsed available-images table.ignore (
Iterable[str]) – Labels the repository has declined. Async-*job regenerates on every run, so without this a closed pull request comes back and the proposal becomes a nuisance rather than a service.
- Return type:
- Returns:
The changes to propose, retirements first.
- repomatic.runner_images.render_change_table(changes)[source]¶
Render proposed changes as a Markdown table for a pull request body.
Carries the reasoning rather than just the edit: the diff shows what moved, and what a reviewer cannot see there is why the table says it had to, which jobs are affected, and what was passed over.
- Parameters:
changes (
Sequence[RunnerChange]) – Changes fromplan_runner_changes().- Return type:
- Returns:
A GitHub-flavored Markdown table, newline-terminated.
- repomatic.runner_images.close_legacy_issue()[source]¶
Close the announcement issue this module no longer maintains.
Called on every run rather than once, because there is no “once” available: a downstream repository adopts a release whenever it adopts one, and the first run after that adoption is the only moment this can be noticed. The close is a no-op when no such issue is open.
- Return type:
- repomatic.runner_images.RUNS_ON_RE_TEMPLATE = '(?P<prefix>^[ \\t]*runs-on:[ \\t]*)(?P<quote>[\'\\"]?){label}(?P=quote)[ \\t]*$'¶
A literal
runs-on:naming one label, anchored to its own line.Rewritten as raw text rather than through a YAML round-trip, for the reason
_extract_raw_job()gives: a round-trip reformats the whole file, and a runner bump should read as a one-line diff. The optional quote group is carried through so a quoted value stays quoted.
- repomatic.runner_images.apply_retirement(change, workflow_dir)[source]¶
Rewrite every literal
runs-on:naming a retiring label.Idempotent: a file already on the successor matches nothing and is left untouched, so a re-run after a merge is a no-op rather than a second edit.
- Parameters:
change (
RunnerChange) – Aretirementchange fromplan_runner_changes().workflow_dir (
Path) – Directory holding the workflow files.
- Return type:
- Returns:
The files actually rewritten.
- repomatic.runner_images.AXIS_LABEL_RE_TEMPLATE = '(?P<quote>["\\\']){label}(?P=quote)'¶
A runner label as a quoted string literal in the curated axes.
Rewritten as text for the same reason a
runs-on:is: the axes are a hand-kept tuple carrying comments and an ordering that says which runner is the fast one, and rebuilding the module from an AST would discard both.
- repomatic.runner_images.apply_axes_retirement(change, axes_path)[source]¶
Move a retiring label forward in the curated test-matrix axes.
Only meaningful inside
kdeldycke/repomatic, where the axes live. A repo consuming repomatic inherits them through the pin, so its matrix moves when it adopts a release rather than when it edits anything.This is the highest-blast-radius edit the operation makes: every downstream repository picks these axes up at the next release. That is the argument for proposing it in a pull request whose own CI runs the full matrix on the new image, rather than for not proposing it.
- Parameters:
change (
RunnerChange) – Aretirementchange fromplan_runner_changes().axes_path (
Path) – Path tomatrix_axes.py.
- Return type:
- Returns:
Whether the file was modified.
- repomatic.runner_images.apply_upgrade(change, pyproject_path)[source]¶
Add a superseding image to the full test matrix as a failing-allowed probe.
Writes two keys under
[tool.repomatic.test-matrix]: the label joins theosaxis throughvariations, and anunstableentry marks every cell carrying itcontinue-on-error. Both are needed and neither alone is useful: the variation without the unstable entry gates the build on an image nobody has vetted, and the unstable entry without the variation matches nothing.Idempotent: an image already probed is detected in both keys and nothing is written.
- Parameters:
change (
RunnerChange) – Anupgradechange fromplan_runner_changes().pyproject_path (
Path) – The project file to edit.
- Return type:
- Returns:
Whether the file was modified.