mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +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>
110 lines
3.8 KiB
Python
110 lines
3.8 KiB
Python
"""One deploy job's title and artifact name: how they are built and read back."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from typing import TYPE_CHECKING, NamedTuple
|
||
|
||
from utils.github.variant.pools import ARCHITECTURES, DISTROS, FILESYSTEMS, MODES
|
||
from utils.roles.display import VARIANT_SEPARATOR
|
||
from utils.symbol_glossary import to_emoji, to_word
|
||
|
||
if TYPE_CHECKING:
|
||
from collections.abc import Sequence
|
||
|
||
LOCAL_GLYPH = to_emoji("test_host")
|
||
|
||
_AXIS_GLYPHS = (
|
||
"".join(to_emoji(word) for word in ("tor", "clearnet", "priority", "instructions"))
|
||
+ LOCAL_GLYPH
|
||
)
|
||
|
||
|
||
def _alternation(words: Sequence[str]) -> str:
|
||
"""A regex alternation over the glyphs of *words*."""
|
||
return "|".join(re.escape(to_emoji(word)) for word in words)
|
||
|
||
|
||
LABEL_RE = re.compile(
|
||
r"^.*(?P<mode>" + _alternation(MODES) + r")️?"
|
||
r"(?P<tor>" + re.escape(to_emoji("tor")) + r")?"
|
||
r"(?:" + re.escape(to_emoji("clearnet")) + r"|" + re.escape(LOCAL_GLYPH) + r")?️?"
|
||
r"(?P<distro>" + _alternation(DISTROS) + r")?️?"
|
||
r"(?P<filesystem>" + _alternation(FILESYSTEMS) + r")?️?"
|
||
r"(?P<architecture>" + _alternation(ARCHITECTURES) + r")?️?"
|
||
r"[" + re.escape(_AXIS_GLYPHS) + r"️\s]*"
|
||
r"(?P<name>.+?)"
|
||
r"(?:" + re.escape(VARIANT_SEPARATOR) + r"(?P<variant>[0-9,]+))?"
|
||
r"[" + re.escape(_AXIS_GLYPHS) + r"️\s]*$"
|
||
)
|
||
|
||
|
||
class Label(NamedTuple):
|
||
"""One deploy job title, taken apart."""
|
||
|
||
mode: str
|
||
name: str
|
||
variant: str
|
||
tor: bool
|
||
distro: str = ""
|
||
filesystem: str = ""
|
||
architecture: str = ""
|
||
|
||
|
||
def parse_label(name: str) -> Label | None:
|
||
"""Take a deploy job title apart.
|
||
|
||
The inverse of what :func:`assign` builds, kept next to it so the two
|
||
cannot drift: consumers that hand-rolled their own regex over raw role
|
||
ids silently matched nothing once job titles carried display names, and
|
||
every failure went unreported.
|
||
|
||
Args:
|
||
name: the job title, with or without a reusable-workflow caller path
|
||
in front of it.
|
||
|
||
Returns:
|
||
``None`` when the title carries no deploy row. ``name`` is the display
|
||
name, returned unresolved -- callers decode it through
|
||
``utils.roles.display``, which is what knows the role tree. ``tor``,
|
||
``distro`` and ``filesystem`` matter because a priority role runs the
|
||
same mode and variant several times over, and only the glyphs tell
|
||
those jobs apart -- a retrigger built from the title alone would
|
||
otherwise replay a different combination than the one that failed.
|
||
"""
|
||
match = LABEL_RE.match(name.strip())
|
||
if match is None:
|
||
return None
|
||
return Label(
|
||
to_word(match.group("mode")),
|
||
match.group("name").strip(),
|
||
match.group("variant") or "",
|
||
match.group("tor") is not None,
|
||
to_word(match.group("distro") or ""),
|
||
to_word(match.group("filesystem") or ""),
|
||
to_word(match.group("architecture") or ""),
|
||
)
|
||
|
||
|
||
def artifact_slug(
|
||
mode: str,
|
||
app: str,
|
||
variant: str,
|
||
tor: bool,
|
||
distro: str = "",
|
||
filesystem: str = "",
|
||
architecture: str = "",
|
||
) -> str:
|
||
"""What identifies one deploy job's artifacts.
|
||
|
||
Built here rather than as a workflow expression so the matrix entry and
|
||
every consumer read the same string: a priority role runs the same mode
|
||
and variant twice, once behind the onion and once not, and two jobs
|
||
uploading under one name is an artifact conflict, not an overwrite. The
|
||
distro and filesystem are in it for the same reason -- a selection may
|
||
name one row on two distros (``role#0%debian role#0%fedora``), and those
|
||
are two deploys of one mode, variant and onion state.
|
||
"""
|
||
shards = (variant, "tor" if tor else "", distro, filesystem, architecture)
|
||
return f"{mode}-{app}" + "".join(f"-{shard}" for shard in shards if shard)
|