# 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
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Final
TEST_RUNNERS_FULL = (
"ubuntu-26.04-arm",
"ubuntu-26.04",
"macos-26",
"macos-26-intel",
"windows-11-arm",
"windows-2025",
)
"""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).
```{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.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 = (
"ubuntu-26.04-arm",
"macos-26",
"windows-2025",
)
"""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.
```
"""
TEST_PYTHON_FULL = (
"3.10",
"3.14",
"3.15",
)
"""Python versions tested across every runner in the full matrix.
Spans the supported range: the floor (`3.10`), the latest stable release
(`3.14`), and the in-development version (`3.15`, flagged `continue-on-error`
via {data}`UNSTABLE_PYTHON_VERSIONS`). Intermediate releases (3.11, 3.12, 3.13)
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.10",
"3.14",
)
"""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.15"})
"""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.15-dev` rather than a bare `py3.15` 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.15t` reads
`3.15t-dev`.
The spelling is borrowed, not invented. pyenv ships version definitions named
`3.15-dev` and `3.15t-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.15` parses as a version
request ("No interpreter found for Python 3.15"), while `uv python find
3.15-dev` falls through to the executable-name branch ("No interpreter found
for executable name `3.15-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.14t": "ubuntu-26.04-arm"}
"""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]
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