repomatic.runner_catalog module¶
Which runner images exist, what they are called, and which are on the way out.
actions/runner-images publishes an Available Images table in its readme with
one row per image, carrying the display name, the architecture, the runs-on:
labels that reach it, and inline preview / deprecated badges. That table is
the canonical dictionary for two questions nothing else answers cleanly:
Which generation a label belongs to. Deriving one from the other by pattern fails on macOS, where the generations disagree:
macos-14is the Arm64 image while its x64 twin ismacos-14-large, andmacos-26-intelbreaks the pattern again. The display name states the family and the version that the labels only imply, so a successor search stays inside one operating system and orders its generations correctly.Which images are current. The badges mark preview and deprecated images, which is what makes a retirement visible before a build starts failing.
This table is the only source read. repomatic.runner_images carries why
it replaced the announcement feed, and what that trade costs.
Caution
Every parse here fails closed. A restyled table yields no rows, which makes
the catalog unavailable rather than wrong, and every caller treats an
unavailable catalog as “propose nothing”. A wrong label would rewrite a
runs-on: to something GitHub does not host, which fails every job in the
repository; a missing one costs a cycle of not noticing.
- repomatic.runner_catalog.CATALOG_REPO = 'actions/runner-images'¶
Repository whose readme carries the Available Images table.
- repomatic.runner_catalog.TABLE_HEADER_RE = re.compile('^\\|\\s*Image\\s*\\|\\s*Architecture\\s*\\|\\s*YAML Label\\s*\\|', re.MULTILINE)¶
The table’s header row, matched by column name rather than by position.
Anchoring on the names is what survives a column being added or reordered: the row is located by what it says, and the cells below it are read by the index this match establishes rather than by a hard-coded one.
- repomatic.runner_catalog.BADGE_RE = re.compile('!\\[(?P<state>preview|deprecated)\\]')¶
A status badge, identified by its alt text rather than its image URL.
The URL carries a colour and a style that GitHub restyles freely; the alt text is the word a reader sees and has stayed put across restyles.
- repomatic.runner_catalog.LABEL_RE = re.compile('`([a-z][a-z0-9.\\-]*)`')¶
A
runs-on:label, backticked inside the YAML Label cell.The cell separates alternatives in prose (”
macos-latest,macos-26ormacos-26-xlarge”), so the backticks are what delimit a label rather than the punctuation around them.
- repomatic.runner_catalog.LATEST_TOKEN = 'latest'¶
Hyphen-separated part marking a floating alias, dropped on sight.
Tested per part rather than as a suffix, because the alias is not always trailing: the x64 macOS row offers
macos-latest-largebesidemacos-26-intel, and a-latest$test keeps the very labellint-reporejects. GitHub repoints these with no commit to review, so filtering here means no caller can propose one by accident.
- repomatic.runner_catalog.SIZED_SUFFIXES = ('-large', '-xlarge')¶
macOS size variants, deprioritized when picking one label from a row.
A row often lists an ordinary hosted label beside sized ones (
macos-26againstmacos-26-xlarge,macos-26-intelagainstmacos-26-large). The sized ones are the paid larger runners, so they are never the default. Note that-intelis not a size variant: it is the x64 half of a macOS generation, and the label this project runs.
- class repomatic.runner_catalog.RunnerImage(display_name, architecture, labels, preview, deprecated)[source]¶
Bases:
objectOne row of the Available Images table.
- display_name: str¶
Name as the table writes it, badges and endpoint markup stripped.
The only place the operating system and the generation are spelled out, so
familyandversionboth read it rather than the labels.
- property family: str¶
Leading word of the display name:
Ubuntu,macOSorWindows.Used to keep a successor search inside one operating system, which the labels alone cannot express (
macos-26-intelandmacos-26share a family that no common label prefix captures).
- repomatic.runner_catalog.parse_catalog(readme)[source]¶
Read the Available Images table out of a readme.
- Parameters:
readme (
str) – Full Markdown source of theactions/runner-imagesreadme.- Return type:
- Returns:
One
RunnerImageper table row, empty when the table cannot be located or yields no usable row.
- repomatic.runner_catalog.fetch_catalog(repo='actions/runner-images')[source]¶
Download and parse the Available Images table.
Read through
ghrather than a bare HTTP GET: the authenticated path carries a rate limit a CI job will not exhaust, where the anonymous one shares 60 requests an hour with every other job on the runner.- Parameters:
repo (
str) – Repository whose readme to read.- Return type:
- Returns:
The catalog, empty when the readme could not be read or parsed. Callers treat an empty catalog as “propose nothing” rather than as “nothing exists”.
- repomatic.runner_catalog.by_label(catalog)[source]¶
Index a catalog by every label reaching each image.
- Return type:
- repomatic.runner_catalog.live_siblings(current, catalog)[source]¶
Every image that could host a job currently on current.
Same operating system and architecture, not itself, and not on its way out. Version is deliberately not filtered: a dying image whose family offers only a same-version sibling still has somewhere to go, and going there beats staying on a deadline.
- Parameters:
current (
RunnerImage) – The image being moved off.catalog (
Sequence[RunnerImage]) – Parsed catalog.
- Return type:
- Returns:
Candidates, unordered.
- repomatic.runner_catalog.successor_for(label, catalog)[source]¶
The image a workflow on label should move to when its own is retiring.
Prefers a released image over a preview, then the highest version. The ordering matters more than it looks: a retirement is a forced move, and landing it on something GitHub is still rolling out trades a known deadline for an unknown one. So a released successor always wins, however old.
A preview is still returned when the family offers nothing else, because the alternative is proposing nothing and leaving the job on an image with an end date.
newer_preview_than()surfaces the preview separately when a released successor was chosen, so a reviewer sees the fresher option without it being taken on their behalf.- Parameters:
label (
str) – Label whose image is retiring, or has vanished.catalog (
Sequence[RunnerImage]) – Parsed catalog.
- Return type:
- Returns:
The best replacement, or
Nonewhen the family offers none.
- repomatic.runner_catalog.newer_preview_than(chosen, current, catalog)[source]¶
A preview image newer than the one
successor_for()settled on.Reported rather than adopted. Whether a fresher preview beats a released image is a capacity-and-risk judgement the pull request exists to host, so naming the alternative in the body is the useful half; picking it is not.
- Parameters:
chosen (
RunnerImage) – Whatsuccessor_forreturned.current (
RunnerImage) – The image being moved off.catalog (
Sequence[RunnerImage]) – Parsed catalog.
- Return type:
- Returns:
The newest preview above chosen, or
None.
- repomatic.runner_catalog.newer_version_than(label, catalog)[source]¶
A genuinely newer version of the image behind label, if one exists.
Strictly newer by version, which is what separates an upgrade from a flavour.
Windows 11 Arm64 with Visual Studio 2026sits at the same version asWindows 11 Arm64and is a different toolchain rather than a newer image, so it is not an upgrade and is not reported as one.- Parameters:
label (
str) – Label currently in use.catalog (
Sequence[RunnerImage]) – Parsed catalog.
- Return type:
- Returns:
The newest strictly-higher version available, or
None.