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-repowarns about. 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.
- repomatic.runner_catalog.CATALOG_ARCHITECTURES: Final[dict[str, Architecture]] = {'arm64': Architecture(id='aarch64', name='ARM64 (AArch64)'), 'x64': Architecture(id='x86_64', name='x86-64 (AMD64)')}¶
The Architecture column’s own spelling, mapped to extra-platforms.
RunnerImage.architecturekeeps the table’s wording, so a comparison against anything else in this project has to come through here.
- repomatic.runner_catalog.ARCH_SUFFIXES: Final[tuple[tuple[str, Architecture], ...]] = (('-arm', Architecture(id='aarch64', name='ARM64 (AArch64)')), ('-xlarge', Architecture(id='aarch64', name='ARM64 (AArch64)')), ('-large', Architecture(id='x86_64', name='x86-64 (AMD64)')), ('-intel', Architecture(id='x86_64', name='x86-64 (AMD64)')))¶
Label suffixes that settle an architecture on their own.
The macOS size suffixes are why this cannot be one rule: they invert what the names suggest, since
macos-15-largeis x64 whilemacos-15-xlargeis Arm64.-intelnames the x64 half of a generation (SIZED_SUFFIXESsays why it is not a size), and-armcovers the Linux (ubuntu-26.04-arm) and Windows (windows-11-arm) Arm images alike.Order is for reading only:
-largecannot match a label ending in-xlarge, because the two differ in the character beforelarge.
- repomatic.runner_catalog.APPLE_SILICON_MACOS_FLOOR: Final[int] = 14¶
First macOS generation whose bare label is Apple silicon.
GitHub moved the default macOS image to Apple silicon at
macos-14, so a bare label from this generation on is Arm64 and an older one is x64. A suffixed label never reaches this comparison:ARCH_SUFFIXESsettles it first.
- 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.
- repomatic.runner_catalog.runner_architecture(label)[source]¶
Returns the architecture a
runs-on:label runs on, without the catalog.The table above is the authority, but reading it costs a network call, so nothing that runs inside a job can wait on it. This derives the same answer from the label’s own shape, which is fixed enough to state: a suffix from
ARCH_SUFFIXES, else Apple silicon for a macOS generation at or aboveAPPLE_SILICON_MACOS_FLOOR, else x64.Answering from the shape is also what lets it speak for a label this project has never listed, which a fixed table cannot: a downstream repository adds its own images through
[tool.repomatic.test-matrix] variations.os.Caution
Unlike every parse in this module, this one does not fail closed: an unrecognized label reads as x64 rather than as “unknown”, because there is no third answer a caller could act on.
tests/test_runner_catalog.pypins the derivation against the published table, so a new label shape GitHub introduces fails the suite instead of being guessed at silently.- Parameters:
label (
str) – Aruns-on:label, likeubuntu-26.04-armormacos-15.- Return type:
- Returns:
The Extra Platforms architecture, matching how
repomatic.release.binarynames one.