Files
computer-playbook/utils/github/variant/tor.py
T
kevinveenbirkenbachandClaude Opus 5 66bde6af40 feat(ci): spread distro and filesystem per row
A run-wide draw proved one distribution and said nothing about the other four, while reporting green. Both are matrix-row axes now, assigned by the same deterministic rotation mode and tor already use, so one sweep exercises every distribution and every filesystem.

The two read the row's position like an odometer, the distro as the low digit. Turning both on the position directly covers only n of the n x m pairs whenever the pools are the same length, and no sweep unlocks it.

Both are part of a row's identity: glyphs in the job label, columns in the plan table, shards in the artifact name. Both are selectable through %distro and /filesystem, and a retrigger replays the distro off a failed title. It leaves the filesystem out, because a title states the kind the matrix assigned and a deploy may have fallen back from it.

A row's filesystem is a preference unless a human names it, on the run or in a token. Reading the matrix's own narrowing as a demand would fail rows for every transient condition the applying step is built to tolerate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:23:29 +02:00

149 lines
5.4 KiB
Python

"""The onion axis of the CI deploy matrix: what it may do, and to which rows.
Separate from :mod:`utils.github.variant.axes` because it answers a different
question. ``axes`` decides which combination a row takes this sweep; this
module decides which onion states exist for that row at all -- read off the
role's ``meta/services.yml``, off the covered variant, off the deploy mode,
and off the run's own ``tor`` input.
"""
from __future__ import annotations
import os
from typing import TYPE_CHECKING
from utils import PROJECT_ROOT
from utils.cache.yaml import load_yaml_any
from utils.roles.applications.services.registry import (
build_service_registry_from_roles_dir,
)
from utils.roles.mapping import ROLE_FILE_META_SERVICES
if TYPE_CHECKING:
from collections.abc import Mapping, Sequence
from typing import Any
ROLES_DIR = PROJECT_ROOT / "roles"
TOR_MODES = ("auto", "enforced", "exclusive", "disabled")
TOR_DEPLOY_MODES = ("swarm", "compose")
def _tor_flag(config: Mapping[str, Any]) -> Any:
"""The ``services.tor.enabled`` value of one config, or ``None`` if unset."""
services = config.get("services") if isinstance(config, dict) else None
tor = services.get("tor") if isinstance(services, dict) else None
return tor.get("enabled") if isinstance(tor, dict) else None
def _base_tor_capable(app: str) -> bool:
"""Read the tor gate straight from the role's ``meta/services.yml``."""
path = ROLES_DIR / app / ROLE_FILE_META_SERVICES
if not path.exists():
return True
try:
services = load_yaml_any(path) or {}
except Exception: # noqa: BLE001 malformed role meta must not break the matrix
return True
return _tor_flag({"services": services}) is not False
def tor_capable(
app: str,
variant: int | None = None,
variants_per_app: Mapping[str, Sequence[Mapping[str, Any]]] | None = None,
) -> bool:
"""Whether a matrix row may be deployed behind the node onion.
Args:
app: application id.
variant: the row's variant index; ``None`` for a role declaring none.
variants_per_app: rendered variant configs per app; ``None`` falls back
to the role's base ``meta/services.yml``.
Returns:
``False`` when the covered variant pins ``services.tor.enabled`` to a
literal false, ``True`` otherwise. A reactive Jinja flag counts as
capable: it is the role saying "onion when the node is dark".
Consulting the variant matters -- roles pin the gate ``true`` in
variant 0 and ``false`` in the rest, so reading only the base config
claims an onion for rounds that deliberately run without one.
"""
declared = (variants_per_app or {}).get(app) or []
if variant is None or not 0 <= variant < len(declared):
return _base_tor_capable(app)
return _tor_flag(declared[variant]) is not False
def tor_provider() -> str | None:
"""Application id of the role providing the ``tor`` service, ``None`` if
the registry names none. Resolved rather than hardcoded so renaming the
provider role cannot leave the matrix pointing at a dead id."""
entry = build_service_registry_from_roles_dir(ROLES_DIR).get("tor") or {}
role = entry.get("role")
return role if isinstance(role, str) and role else None
def resolve_tor_mode(raw: str | None = None) -> str:
"""Tor axis mode from ``INFINITO_TOR``; unknown or empty means ``auto``.
Args:
raw: explicit value; ``None`` reads the environment.
Returns:
one of :data:`TOR_MODES`.
"""
if raw is None:
raw = os.environ.get("INFINITO_TOR")
value = (raw or "").strip().lower()
return value if value in TOR_MODES else "auto"
def wants_tor(position: int, sweep: int) -> bool:
"""Whether a capable row takes the onion this sweep. Halved on
``sweep // 2`` so it does not flip in lockstep with the mode rotation."""
return (position + sweep // 2) % 2 == 0
def tor_states(mode: str, *, capable: bool, tor_mode: str) -> list[bool]:
"""The onion states one *mode* is worth running for a priority row.
Args:
mode: the deploy mode the row runs in.
capable: whether the row's variant may take an onion at all.
tor_mode: the run's tor axis.
Returns:
``[True, False]`` on a tor-carrying mode under ``auto`` -- a priority
row is not sampled, it covers both states in the same sweep. An
explicit ``enforced``/``exclusive``/``disabled`` is an operator
narrowing and still wins, and a mode that carries no onion axis at all
(host) only ever yields the clearnet state.
"""
if mode not in TOR_DEPLOY_MODES:
return [False]
if tor_mode == "disabled":
return [False]
if tor_mode in ("enforced", "exclusive"):
if capable:
return [True]
return [] if tor_mode == "exclusive" else [False]
return [True, False] if capable else [False]
def combinations(
offered: Sequence[str], *, capable: bool, tor_mode: str
) -> list[tuple[str, bool]]:
"""Every ``(mode, tor)`` pair a priority row is deployed in.
Priority rows are the ones a run must not sample: they cover the whole
cross-product of the modes their role offers and the onion states each of
those modes can take, in one sweep, instead of walking it over four.
"""
return [
(mode, enabled)
for mode in offered
for enabled in tor_states(mode, capable=capable, tor_mode=tor_mode)
]