mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
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>
149 lines
5.4 KiB
Python
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)
|
|
]
|