mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-09 12:17:13 +00:00
Both architectures were claimed to work and only one was ever exercised. The deploy matrix now carries an architecture the way it carries mode, tor, distro and filesystem, so every run proves both. A job cannot ask for an architecture the way it asks for a distro, because the architecture is the machine rather than something the job installs on it. The matrix therefore carries a runner label and `runs-on` reads it, and an architecture without a label in RUNNERS is a build error rather than a row that lands on the wrong hardware and still reports green. assert_architecture.sh checks the runner it actually got, because a label that stops resolving to arm64 would otherwise go unnoticed forever. Assignment is rotation, not chance: two architectures against five distros and fifteen distro/filesystem positions are coprime enough that every combination comes round. A role narrows the pool through `architectures` in its meta/services.yml; without one it takes whatever the rotation hands it. An empty intersection aborts instead of silently removing the role from the run. Images are built per architecture and pushed under an architecture-suffixed tag, then merged into the plain tag by manifest_all.sh; pushing the plain tag from both builds would let the last one win and send half the rows to the wrong platform. cleanup.sh now protects the `ci-<sha>-<architecture>` children of a protected tag, because the plain tag is an index over them and deleting a child leaves it unpullable on that platform. label.py is split out of axes.py, which the 500-line limit no longer fits. Writing a job title and reading one back belong together: a retrigger that recovers an axis wrongly replays a different deploy than the one that failed, and a drifting artifact name makes the failure reporter download nothing. The leading `.*` in LABEL_RE is greedy on purpose so it anchors on the last mode glyph; a reusable-workflow caller path carries a glyph of its own, and test_selection.py guards the case. Verified: 313 unit tests and 48 subtests across utils/github/variant, utils/roles and cli/meta/ci. Locally, a deploy pinned to arm64 reaches ok=1444 and its image builds log aarch64-linux-gnu paths. Not verified here: actionlint over the seven changed workflows, and a full `make test`. The matrix doubles the row count, so runtime and runner availability change for every CI run from here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
89 lines
2.9 KiB
Python
89 lines
2.9 KiB
Python
"""What the deploy axes may draw from, and how they draw.
|
|
|
|
Separate from :mod:`utils.github.variant.axes` because it answers a narrower
|
|
question: ``axes`` decides which value a given row takes, this module owns the
|
|
value sets themselves, the narrowing a run may apply to them, and the rotation
|
|
that walks them.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from utils.distros import distro_names
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Sequence
|
|
|
|
MODES = ("compose", "swarm", "host")
|
|
|
|
DISTROS = distro_names()
|
|
|
|
FILESYSTEMS = ("zfs", "btrfs", "ext4")
|
|
|
|
ARCHITECTURES = ("amd64", "arm64")
|
|
|
|
RUNNERS = {"amd64": "ubuntu-latest", "arm64": "ubuntu-24.04-arm"}
|
|
"""The runner label each architecture deploys on."""
|
|
|
|
|
|
def resolve_pool(
|
|
raw: str | None, declared: Sequence[str], axis: str
|
|
) -> tuple[str, ...]:
|
|
"""The values one axis may draw from this run.
|
|
|
|
Args:
|
|
raw: the run's input, space- or comma-separated. Empty means the whole
|
|
declared set, the same way ``--modes auto`` opens the mode axis.
|
|
declared: every value the axis knows, in declaration order.
|
|
axis: what to call the axis in the refusal.
|
|
|
|
Raises:
|
|
SystemExit: a token naming no declared value. A typo has to abort the
|
|
run rather than narrow it to a set nobody asked for.
|
|
"""
|
|
tokens = (raw or "").replace(",", " ").split()
|
|
if not tokens:
|
|
return tuple(declared)
|
|
unknown = [token for token in tokens if token not in declared]
|
|
if unknown:
|
|
raise SystemExit(
|
|
f"unknown {axis}(s): {' '.join(unknown)}; expected {', '.join(declared)}"
|
|
)
|
|
return tuple(value for value in declared if value in tokens)
|
|
|
|
|
|
def resolve_distros(raw: str | None) -> tuple[str, ...]:
|
|
"""The distributions this run spreads its rows over; empty means all."""
|
|
return resolve_pool(raw, DISTROS, "distro")
|
|
|
|
|
|
def resolve_filesystems(raw: str | None) -> tuple[str, ...]:
|
|
"""The docker data-root kinds this run spreads its rows over; empty means
|
|
all."""
|
|
return resolve_pool(raw, FILESYSTEMS, "filesystem")
|
|
|
|
|
|
def resolve_architectures(raw: str | None) -> tuple[str, ...]:
|
|
"""The CPU architectures this run spreads its rows over; empty means
|
|
both."""
|
|
return resolve_pool(raw, ARCHITECTURES, "architecture")
|
|
|
|
|
|
def runner_of(architecture: str) -> str:
|
|
"""The runner label *architecture* deploys on.
|
|
|
|
Raises:
|
|
KeyError: the architecture has no label. Falling back to the amd64
|
|
runner would deploy an arm64 row on amd64 hardware and report it
|
|
green under an arm64 job title.
|
|
"""
|
|
return RUNNERS[architecture]
|
|
|
|
|
|
def rotate(pool: Sequence[str], position: int, sweep: int) -> str:
|
|
"""The value a row draws from *pool* this sweep: a plain rotation over the
|
|
row's global position, so consecutive rows spread across the pool and
|
|
consecutive sweeps move every row on to the next value."""
|
|
return pool[(position + sweep) % len(pool)]
|