Files
computer-playbook/utils/github/variant/selection.py
T
kevinveenbirkenbachandClaude Opus 5 677a63931c Merge branch 'feature/network/multi-network-serving' into feature/store-i18n
Resolves 68 conflicted files. The governing rule was our structure with their
network semantics: the tor boolean becomes a network mode (clearnet | tor |
multi) throughout the variant layer, while this branch's architecture and mesh
axes, its extracted modules and its i18n content stay.

Where the two sides covered different ground, both are kept rather than one
picked:

- ca_injected answers a ca_client on the deployment's TLS mode OR on an SSO
  issuer that resolves self-signed. 3a3e9ed2c9 records why: an issuer is not
  an object store, so keeping only one side drops the other's coverage.
- get_merged_domains keeps the loud refusal when svc-net-tor is deployed
  without a provisioned node, now gated on their tor_serves_host; their
  node_onion_of returns an empty string there, which would have served every
  onion-exclusive app on clearnet in silence.
- sso_cookie_domains stays the single source for oauth2-proxy's cookie scope.
  Their per-network branch omitted the issuer domain on the onion arm, which
  breaks the callback leg.
- The clearnet CSP batch carries --clearnet-timeout again; their batch dict
  hardcoded zero while the role still passes the flag.
- The dashboard keeps its i18n content dir, image, version and design
  resolution; their side reduced the logo sources to the bare service
  provider values.

Their side wins where it is measurably better: the onion mirror rewrite is
gated per vhost rather than per app, the Playwright onion decision reads the
pass's own domain instead of the role's canonical one, and the matomo
reinjection and dashboard config render per network sibling.

Eight files were broken by the merge without ever carrying a conflict marker,
because git marks only the lines both sides touched:

- utils/cache/domains.py lost the onionize block that sat between two hunks
- roles/sys-front-inj-all/templates/body_filter.lua.j2 called it
- web-app-bluesky, keycloak sso_proxy: the get_entity_name filter was renamed
- trigger/__main__.py, playwright_coverage.py and its test: the deleted tor
  module
- cli/administration/deploy/ci/timings.py read Label.tor
- plugins/filter/csp_filters.py called _aligned_url with four of six arguments
- tests/unit/python/utils/cache/test_domains.py referenced a renamed class
- tests/.../sys-ctl-hlth-csp/files/test_script.py mocked a function main no
  longer calls

Verified: the variant layer (155 tests, 65 subtests), ca_injected (18),
the CI layer (175), the domains cache including the restored guard (32),
retrigger inputs (9), timings and the CSP health script (53). make
quality-high clears autoformat; its remaining findings are tracked separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-23 09:53:41 +02:00

379 lines
14 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Read the deploy axes an operator pins in ``whitelist`` and ``priority``.
A selection token names a role and MAY narrow the axes that role's rows would
otherwise be assigned: which variants run, which deploy mode, which node
network mode, whether it runs over the WireGuard mesh, which distribution it
deploys on, which filesystem its docker data root runs on and which CPU
architecture it lands on. What a token leaves open stays open, exactly as an
unpinned run leaves it: the priority line covers every mode and network mode
the row can take, and the sweep rotation picks the distro, the filesystem and
the architecture on both lines.
Two spellings are accepted, so an operator can either type the token or paste
back the job title of the run they want repeated:
* the job label CI emits (``🐝🧅🚇🌀🦓🦾𓏴𓏛·Nextcloud#2``) -- the glyphs carry
mode, network mode, mesh state, distro, filesystem and architecture, the
``#`` shard the variants;
* an ASCII form (``web-app-nextcloud#0,2@swarm+tor%debian/zfs:arm64&vpn``).
The network mode spells out as ``+clearnet``/``+tor``/``+multi`` rather than
as a ``-tor`` suffix: a role id may itself end in ``-tor`` (``svc-net-tor``),
and a suffix that eats the tail of a role name selects a different role in
silence.
Everything a token pins is checked against what the row can actually do
(:func:`utils.github.variant.axes.assign`) and against the run's own mode and
network inputs. A contradiction aborts the matrix instead of quietly deploying
something else or nothing at all.
"""
from __future__ import annotations
import re
from typing import TYPE_CHECKING, NamedTuple
from utils.github.variant.axes import ARCHITECTURES, DISTROS, FILESYSTEMS, MODES
from utils.networks.reachability import NODE_MODES
from utils.roles.display import VARIANT_SEPARATOR, display_names
from utils.symbol_glossary import to_emoji
if TYPE_CHECKING:
from collections.abc import Iterable, Mapping, Sequence
from typing import Any
MODE_SEPARATOR = "@"
NETWORK_SEPARATOR = "+"
DISTRO_SEPARATOR = "%"
FILESYSTEM_SEPARATOR = "/"
ARCHITECTURE_SEPARATOR = ":"
VPN_SEPARATOR = "&"
NETWORK_WORDS = tuple(NODE_MODES)
VPN_WORDS = {"vpn": True, "direct": False}
_STRIPPED_GLYPHS = ("priority", "test_host")
_VARIATION = re.compile("[︎️]")
_SEPARATORS = (
VARIANT_SEPARATOR
+ MODE_SEPARATOR
+ NETWORK_SEPARATOR
+ DISTRO_SEPARATOR
+ FILESYSTEM_SEPARATOR
+ ARCHITECTURE_SEPARATOR
+ VPN_SEPARATOR
)
_TOKEN = re.compile(
r"^(?P<name>[^" + re.escape(_SEPARATORS) + r"\s]+)"
r"(?:" + re.escape(VARIANT_SEPARATOR) + r"(?P<variants>\d+(?:,\d+)*))?"
r"(?:" + re.escape(MODE_SEPARATOR) + r"(?P<mode>[a-z]+))?"
r"(?:" + re.escape(NETWORK_SEPARATOR) + r"(?P<network>[a-z]+))?"
r"(?:" + re.escape(DISTRO_SEPARATOR) + r"(?P<distro>[a-z0-9]+))?"
r"(?:" + re.escape(FILESYSTEM_SEPARATOR) + r"(?P<filesystem>[a-z0-9]+))?"
r"(?:" + re.escape(ARCHITECTURE_SEPARATOR) + r"(?P<architecture>[a-z0-9]+))?"
r"(?:" + re.escape(VPN_SEPARATOR) + r"(?P<vpn>[a-z]+))?$"
)
_SYNTAX = (
f"<role>[{VARIANT_SEPARATOR}<variant,variant>]"
f"[{MODE_SEPARATOR}<{'|'.join(MODES)}>]"
f"[{NETWORK_SEPARATOR}<{'|'.join(NETWORK_WORDS)}>]"
f"[{DISTRO_SEPARATOR}<{'|'.join(DISTROS)}>]"
f"[{FILESYSTEM_SEPARATOR}<{'|'.join(FILESYSTEMS)}>]"
f"[{ARCHITECTURE_SEPARATOR}<{'|'.join(ARCHITECTURES)}>]"
f"[{VPN_SEPARATOR}<{'|'.join(VPN_WORDS)}>]"
)
class Pin(NamedTuple):
"""One selection token, taken apart.
``variants`` empty and ``mode``/``network``/``distro``/``filesystem``/
``architecture``/``vpn`` ``None`` each mean "not pinned": that axis keeps
whatever the line it stands in would assign.
"""
app: str
variants: tuple[int, ...] = ()
mode: str | None = None
network: str | None = None
distro: str | None = None
filesystem: str | None = None
architecture: str | None = None
vpn: bool | None = None
@property
def pinned(self) -> bool:
"""Whether the token narrows anything at all beyond the role name."""
return bool(self.variants) or any(
axis is not None
for axis in (
self.mode,
self.network,
self.distro,
self.filesystem,
self.architecture,
self.vpn,
)
)
@property
def axes(
self,
) -> tuple[str | None, str | None, str | None, str | None, str | None, bool | None]:
"""What the token narrows, as the key two tokens are equal under."""
return (
self.mode,
self.network,
self.distro,
self.filesystem,
self.architecture,
self.vpn,
)
def describe(pin: Pin) -> str:
"""The token as an operator would have written it, for error messages."""
variants = ",".join(str(index) for index in pin.variants)
return (
pin.app
+ (f"{VARIANT_SEPARATOR}{variants}" if variants else "")
+ (f"{MODE_SEPARATOR}{pin.mode}" if pin.mode else "")
+ (f"{NETWORK_SEPARATOR}{pin.network}" if pin.network else "")
+ (f"{DISTRO_SEPARATOR}{pin.distro}" if pin.distro else "")
+ (f"{FILESYSTEM_SEPARATOR}{pin.filesystem}" if pin.filesystem else "")
+ (f"{ARCHITECTURE_SEPARATOR}{pin.architecture}" if pin.architecture else "")
+ (
f"{VPN_SEPARATOR}{'vpn' if pin.vpn else 'direct'}"
if pin.vpn is not None
else ""
)
)
def _word_glyph(text: str, words: Iterable[str]) -> tuple[str, str | None]:
"""Strip whichever of *words* the text carries as a glyph, and name it."""
found: str | None = None
for word in words:
glyph = to_emoji(word)
if glyph in text:
found, text = word, text.replace(glyph, "")
return text, found
def _glyphs(
text: str,
) -> tuple[
str, str | None, str | None, str | None, str | None, str | None, bool | None
]:
"""Take the label glyphs off a pasted job title and read them as axes."""
text, mode = _word_glyph(text, MODES)
text, network = _word_glyph(text, NETWORK_WORDS)
text, distro = _word_glyph(text, DISTROS)
text, filesystem = _word_glyph(text, FILESYSTEMS)
text, architecture = _word_glyph(text, ARCHITECTURES)
text, mesh = _word_glyph(text, VPN_WORDS)
for word in _STRIPPED_GLYPHS:
text = text.replace(to_emoji(word), "")
return (
text.strip(),
mode,
network,
distro,
filesystem,
architecture,
None if mesh is None else VPN_WORDS[mesh],
)
def _agree(pin: Any, glyph: Any, token: str, axis: str) -> Any:
"""One axis stated twice must state the same thing."""
if pin is not None and glyph is not None and pin != glyph:
raise SystemExit(
f"selection token {token!r} pins two different {axis} values; "
f"drop one of them"
)
return glyph if pin is None else pin
def parse(token: str) -> Pin:
"""One selection token as a :class:`Pin`.
Args:
token: label form or ASCII form; a bare role id or display name pins
nothing and selects every row of that role.
Raises:
SystemExit: the token is unparsable, or names a mode or network mode
that does not exist. A typo must abort the run rather than narrow
it to nothing.
"""
(
text,
glyph_mode,
glyph_network,
glyph_distro,
glyph_fs,
glyph_arch,
glyph_vpn,
) = _glyphs(_VARIATION.sub("", token.strip()))
match = _TOKEN.match(text)
if match is None:
raise SystemExit(f"unparsable selection token {token!r}; expected {_SYNTAX}")
for group, axis, declared in (
("mode", "deploy mode", MODES),
("network", "network mode", NETWORK_WORDS),
("distro", "distro", DISTROS),
("filesystem", "filesystem", FILESYSTEMS),
("architecture", "architecture", ARCHITECTURES),
("vpn", "mesh state", VPN_WORDS),
):
value = match.group(group)
if value is not None and value not in declared:
raise SystemExit(
f"selection token {token!r} names unknown {axis} {value!r}; "
f"expected {', '.join(declared)}"
)
name = match.group("name")
variants = match.group("variants")
mesh_word = match.group("vpn")
return Pin(
display_names().decode(name) or name,
tuple(int(index) for index in variants.split(",")) if variants else (),
_agree(match.group("mode"), glyph_mode, token, "mode"),
_agree(match.group("network"), glyph_network, token, "network"),
_agree(match.group("distro"), glyph_distro, token, "distro"),
_agree(match.group("filesystem"), glyph_fs, token, "filesystem"),
_agree(match.group("architecture"), glyph_arch, token, "architecture"),
_agree(VPN_WORDS[mesh_word] if mesh_word else None, glyph_vpn, token, "mesh"),
)
def covers(pin: Pin, entry: Mapping[str, Any]) -> bool:
"""Whether *pin* names this matrix entry.
An axis the token leaves open matches anything, so ``web-app-x`` covers
every row of that role and ``web-app-x#2@swarm`` only its swarm deploy of
variant 2. Reads the strings a matrix entry carries, not a discovery row's
native types.
"""
if entry.get("apps") != pin.app:
return False
if pin.variants and str(entry.get("variant", "")) not in {
str(index) for index in pin.variants
}:
return False
for value, key in (
(pin.mode, "mode"),
(pin.network, "network"),
(pin.distro, "distro"),
(pin.filesystem, "filesystem"),
(pin.architecture, "architecture"),
):
if value is not None and entry.get(key) != value:
return False
return pin.vpn is None or (entry.get("vpn") == "true") == pin.vpn
def parse_list(tokens: str) -> list[Pin]:
"""Every token of a space-separated ``whitelist``/``priority`` input."""
return [parse(token) for token in tokens.split()]
def names(pins: Iterable[Pin]) -> str:
"""The role ids the pins select, deduplicated, in the order given -- what
the discovery query filters on. The axes are applied afterwards, on the
rows the query returned."""
return " ".join(dict.fromkeys(pin.app for pin in pins))
def apply(
rows: Sequence[Mapping[str, Any]], pins: Sequence[Pin]
) -> list[dict[str, Any]]:
"""Keep the rows the pins select and stamp the pinned axes onto them.
Args:
rows: discovery rows of one line, in query order.
pins: that line's selection tokens; empty means "no selection", and
every row passes through untouched.
Returns:
one entry per (row, pin) the selection asks for, each carrying
``pin_mode``, ``pin_network``, ``pin_distro`` and ``pin_filesystem``
for :func:`utils.github.variant.axes.assign` to honour. Order is the
query's -- a selection narrows what runs, it never re-ranks it.
A row several tokens name is emitted once per token, which is the whole
point of the axes: ``role#1@compose+tor role#1@swarm+tor`` is one
variant that failed in two modes, and it has to come back as two
deploys. Two tokens narrowing a row the same way collapse into one, so
a duplicate in the input cannot become two jobs racing for one artifact
name.
A bare role name loses to any token that narrows the same row: writing
``role role#1@swarm`` asks for a specific deploy, not for that deploy
plus a rotation-picked one on top.
Raises:
SystemExit: a token pins a variant of a role the query DID return, and
no row carries that variant. Silently deploying nothing is how a
mistyped variant index turns into a green run that tested nothing.
A token whose role the query returned nothing for is let through
instead: the diff-derived whitelist legitimately names roles the
run's mode and lifecycle envelope filters out, and since that
resolver now pins the variants a change actually reaches, such a
token would otherwise abort every chunk of the run. Role names are
checked against `roles/` before the run starts
(scripts/github/resolve/effective_whitelist.sh), so a typo is still
caught -- one layer earlier.
"""
if not pins:
return [dict(row) for row in rows]
kept: list[dict[str, Any]] = []
matched: set[int] = set()
discovered = {row["name"] for row in rows}
for row in rows:
hits = [
(index, pin)
for index, pin in enumerate(pins)
if pin.app == row["name"]
and (not pin.variants or row.get("variant") in pin.variants)
]
matched.update(index for index, _pin in hits)
narrowing = [(index, pin) for index, pin in hits if pin.pinned]
seen: set[tuple[Any, ...]] = set()
for _index, pin in narrowing or hits[:1]:
if pin.axes in seen:
continue
seen.add(pin.axes)
kept.append(
{
**row,
"pin_mode": pin.mode,
"pin_network": pin.network,
"pin_distro": pin.distro,
"pin_filesystem": pin.filesystem,
"pin_architecture": pin.architecture,
"pin_vpn": pin.vpn,
}
)
for index, pin in enumerate(pins):
if index not in matched and pin.pinned and pin.app in discovered:
raise SystemExit(
f"selection {describe(pin)!r} matches no discovered row; "
f"check the variant index and the run's lifecycle/mode filters"
)
return kept