# Copyright Kevin Deldycke <[email protected]> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""Test matrix constants for CI workflows.
Defines the GitHub-hosted runner images and Python versions used to build
test matrices. Separating these from
{mod}`repomatic.metadata.core` makes the CI matrix configuration self-contained
and easier to update when runner images or Python releases change.
"""
from __future__ import annotations
from enum import StrEnum
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Final
LINUX_ARM64_RUNNER: Final = "ubuntu-26.04-arm"
"""GitHub-hosted image of the ARM Linux slot."""
LINUX_X64_RUNNER: Final = "ubuntu-26.04"
"""GitHub-hosted image of the x86 Linux slot."""
MACOS_ARM64_RUNNER: Final = "macos-26"
"""GitHub-hosted image of the Apple-silicon macOS slot."""
MACOS_X64_RUNNER: Final = "macos-26-intel"
"""GitHub-hosted image of the Intel macOS slot."""
WINDOWS_ARM64_RUNNER: Final = "windows-11-arm"
"""GitHub-hosted image of the ARM Windows slot."""
WINDOWS_X64_RUNNER: Final = "windows-2025"
"""GitHub-hosted image of the x86 Windows slot."""
TEST_RUNNERS_FULL = (
LINUX_ARM64_RUNNER,
LINUX_X64_RUNNER,
MACOS_ARM64_RUNNER,
MACOS_X64_RUNNER,
WINDOWS_ARM64_RUNNER,
WINDOWS_X64_RUNNER,
)
"""GitHub-hosted runners for the full test matrix.
Two variants per platform (one per architecture). See
[available images](https://github.com/actions/runner-images#available-images).
Each label is spelled once, in the slot constants above, which the test axes
and the Nuitka build fleet
({data}`~repomatic.release.binary.NUITKA_BUILD_TARGETS`) both read.
`sync-runner-images` moves a retiring label by rewriting its one literal, so
both follow.
```{note} Preview images are adopted on measurement, not on GitHub's label
An image counts as stable here once it has been validated against this suite,
not once a vendor relabels it. GitHub's *preview* label gates whether an image
can sit behind the `-latest` aliases, and this project never uses those aliases:
a floating alias re-points with no commit to review, and
{func}`~repomatic.lint_repo.workflows.check_runner_images` rejects one outright. The
Ubuntu 26.04 pair was adopted that way while still in preview, before GitHub
declared it generally available on 2026-09-17. Measured over consecutive runs
before the swap, `ubuntu-26.04-arm` beat `ubuntu-24.04-arm` by 16% on Python
3.10 and 28% on 3.14, tied on 3.15, and failed nothing.
```
```{note} Architecture speed is not uniform across platforms
When reducing to one runner per OS, choose by measured speed, not architecture
(see {doc}`/test-matrix`). Tendencies from `repomatic`'s own full test suite:
ARM Linux runs two to three times as fast as the lean x86 `ubuntu-slim` that
preceded `ubuntu-26.04` on this axis; Apple-silicon `macos-26` beats
`macos-26-intel` by ~2x; the two Windows images tie on compute (`windows-2025`
is the PR pick). Per-job wall-clock folds in setup and upload, so isolate the
test steps before blaming the image. These figures drift as images are
re-provisioned, so re-confirm against your own job timings.
```
"""
TEST_RUNNERS_PR = (
LINUX_ARM64_RUNNER,
MACOS_ARM64_RUNNER,
WINDOWS_X64_RUNNER,
)
"""Reduced runner set for pull request test matrices.
One runner per platform: ARM Linux (`ubuntu-26.04-arm`) and Apple-silicon macOS
(`macos-26`) are the fastest of their platform on the test workload, plus x86
Windows (`windows-2025`, where the two Windows images tie on compute). x86 Linux
stays covered by the full matrix ({data}`TEST_RUNNERS_FULL`).
```{note} Why ARM Linux for the PR slot
The suite runs `pytest --numprocesses=auto`, so it scales with cores and favors
ARM, by two to three times over the x86 image, for quicker PR feedback. See
{doc}`/test-matrix` for the measurements.
```
"""
KNOWN_RUNNERS: Final[frozenset[str]] = frozenset(TEST_RUNNERS_FULL) | frozenset(
TEST_RUNNERS_PR
)
"""Every runner image this project has deliberately chosen.
The closest thing to a curated list of images a project should be running on,
and the one place carrying measured guidance on their relative speed and cost.
A job naming something outside it has been picked without that guidance.
The test axes are the whole list: every job runs on an image the suite is also
validated against, so "where is the suite exercised" and "what may a job run on"
are one question. That is deliberate, since each extra image is one more to
track, pin and migrate. A job needing something else is a decision to make
explicitly, by widening the axes rather than by naming an image here.
"""
TEST_PYTHON_FULL = (
"3.11",
"3.15",
"3.16",
)
"""Python versions tested across every runner in the full matrix.
Spans the supported range: the floor (`3.11`), the latest stable release
(`3.15`), and the in-development version (`3.16`, flagged `continue-on-error`
via {data}`UNSTABLE_PYTHON_VERSIONS`). Intermediate releases (3.12 to 3.14)
are skipped to reduce CI load. Released build *flavors* (free-threaded) are not
full-spread; they get a single-runner smoke test instead, see
{data}`SINGLE_RUNNER_PYTHON_VERSIONS`.
"""
TEST_PYTHON_PR = (
"3.11",
"3.15",
)
"""Reduced Python version set for pull request test matrices.
Just the floor and the latest stable release, for fast PR feedback. The
in-development version and released build flavors (free-threaded) are left to
the full matrix.
"""
UNSTABLE_PYTHON_VERSIONS: Final[frozenset[str]] = frozenset({"3.16"})
"""Python versions still in development.
Jobs using these versions run with `continue-on-error` in CI. Contrast with
{data}`SINGLE_RUNNER_PYTHON_VERSIONS`, which are released and run stable.
"""
PRERELEASE_LABEL_SUFFIX: Final[str] = "-dev"
"""Suffix marking an unreleased Python in a CI job name.
Appended to each {data}`UNSTABLE_PYTHON_VERSIONS` member to form the
`python-label` matrix key, so a `continue-on-error` cell states *why* it may
fail: `⁉️ ubuntu-26.04 / py3.16-dev` rather than a bare `py3.16` indistinguishable
from a released one. Being a plain suffix append, it composes with the
free-threaded flavor the way both tools below spell it: `3.16t` reads
`3.16t-dev`.
The spelling is borrowed, not invented. pyenv ships version definitions named
`3.16-dev` and `3.16t-dev` that build from the CPython branch tip, and
[`actions/setup-python`](https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md)
documents an `x.y-dev` syntax resolving to "the latest patch version of Python,
alpha, beta and rc (release candidate) releases included". Anyone reading a
GitHub Actions job name has met it in one of the two.
```{warning} A label, never a uv request
uv does not implement the syntax. `uv python find 3.16` parses as a version
request ("No interpreter found for Python 3.16"), while `uv python find
3.16-dev` falls through to the executable-name branch ("No interpreter found
for executable name `3.16-dev`"). The workflow hands `python-version` straight
to `uv venv --python`, so the axis value stays the bare version and this suffix
reaches the job `name:` alone. Writing it into a
`[tool.repomatic.test-matrix]` directive matches no cell.
```
"""
SINGLE_RUNNER_PYTHON_VERSIONS: Final[dict[str, str]] = {"3.15t": LINUX_ARM64_RUNNER}
"""Released Python build flavors smoke-tested on a single runner, mapped to it.
A free-threaded build (the `t` suffix, made officially supported in 3.14 by
[PEP 779](https://peps.python.org/pep-0779/)) runs the same released interpreter
as its base version, just without the GIL. The base version already gets the
full cross-platform spread ({data}`TEST_PYTHON_FULL`), so the library logic is
covered everywhere; the flavor only needs one runner to catch a
free-threading-specific break. These run *stable* (expected to pass), unlike the
unreleased {data}`UNSTABLE_PYTHON_VERSIONS`. The runner is `ubuntu-26.04-arm`,
the default single-runner pick: the fastest measured on compute-bound parallel
work and the cheapest tier, and free-threading targets server workloads where
Linux/ARM is the norm (see {doc}`/test-matrix`).
"""
[docs]
class MatrixName(StrEnum):
"""The two test matrices every project gets, and the axes each one starts from."""
FULL = "full"
"""The matrix of pushes to the default branch and of scheduled runs."""
PR = "pr"
"""The reduced matrix of pull requests, for fast feedback."""
@property
def runners(self) -> tuple[str, ...]:
"""The runner images this matrix seeds its `os` axis with, in order."""
return TEST_RUNNERS_FULL if self is MatrixName.FULL else TEST_RUNNERS_PR
@property
def python_versions(self) -> tuple[str, ...]:
"""The Python versions this matrix seeds its `python-version` axis with."""
return TEST_PYTHON_FULL if self is MatrixName.FULL else TEST_PYTHON_PR
[docs]
def python_version_sort_key(version: str) -> tuple[tuple[int, ...], int]:
"""Sort key ordering `python-version` axis values by release.
Compares on the numeric release components, then places a build flavor (the
free-threaded `t` suffix of {data}`SINGLE_RUNNER_PYTHON_VERSIONS`) directly
after its base version rather than after every later release: `3.14` sorts
before `3.14t`, which sorts before `3.15`. Non-numeric components are
dropped, so an axis value like `pypy3.10` falls back to the digits it
carries.
:param version: A `python-version` axis value, like `3.14` or `3.14t`.
:return: A key tuple suitable for {func}`sorted`.
"""
flavor = int(version.endswith("t"))
base = version[:-1] if flavor else version
parts = tuple(int(chunk) for chunk in base.split(".") if chunk.isdigit())
return parts, flavor