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-14 is the Arm64 image while its x64 twin is macos-14-large, and macos-26-intel breaks 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-26 or macos-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-large beside macos-26-intel, and a -latest$ test keeps the very label lint-repo warns 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-26 against macos-26-xlarge, macos-26-intel against macos-26-large). The sized ones are the paid larger runners, so they are never the default. Note that -intel is 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.architecture keeps 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-large is x64 while macos-15-xlarge is Arm64. -intel names the x64 half of a generation (SIZED_SUFFIXES says why it is not a size), and -arm covers the Linux (ubuntu-26.04-arm) and Windows (windows-11-arm) Arm images alike.

Order is for reading only: -large cannot match a label ending in -xlarge, because the two differ in the character before large.

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_SUFFIXES settles it first.

class repomatic.runner_catalog.RunnerImage(display_name, architecture, labels, preview, deprecated)[source]¶

Bases: object

One 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 family and version both read it rather than the labels.

architecture: str¶

x64 or arm64, as the table’s own column spells it.

labels: tuple[str, ...]¶

Every runs-on: label reaching this image, -latest aliases removed.

preview: bool¶

Whether the row is badged as a public preview.

deprecated: bool¶

Whether the row is badged as deprecated.

property family: str¶

Leading word of the display name: Ubuntu, macOS or Windows.

Used to keep a successor search inside one operating system, which the labels alone cannot express (macos-26-intel and macos-26 share a family that no common label prefix captures).

property version: tuple[int, ...]¶

Numeric version read out of the display name, for ordering.

Ubuntu 26.04 Arm64 sorts above Ubuntu 24.04, and a name carrying no number at all (Ubuntu Slim) sorts below every numbered sibling rather than raising.

property preferred_label: str¶

The one label to write into a runs-on: for this image.

Prefers a plain label over a sized variant, so a row offering macos-26 beside macos-26-xlarge yields the ordinary hosted runner.

repomatic.runner_catalog.parse_catalog(readme)[source]¶

Read the Available Images table out of a readme.

Parameters:

readme (str) – Full Markdown source of the actions/runner-images readme.

Return type:

list[RunnerImage]

Returns:

One RunnerImage per 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 gh rather 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:

list[RunnerImage]

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:

dict[str, RunnerImage]

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:
Return type:

list[RunnerImage]

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:

RunnerImage | None

Returns:

The best replacement, or None when 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:
Return type:

RunnerImage | None

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 2026 sits at the same version as Windows 11 Arm64 and is a different toolchain rather than a newer image, so it is not an upgrade and is not reported as one.

Parameters:
Return type:

RunnerImage | None

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 above APPLE_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.py pins 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) – A runs-on: label, like ubuntu-26.04-arm or macos-15.

Return type:

Architecture

Returns:

The Extra Platforms architecture, matching how repomatic.release.binary names one.