mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-11 13:17:07 +00:00
Every role's base services carry `docs.enabled: "{{ 'web-app-docs' in
group_names }}"`, and the static closure counts any "in group_names" flag
as possibly true (is_explicit_truth). A round therefore pulled web-app-docs
in through any member whose active variant did not pin docs off, and a
round hands every role its index or clamps it to variant 0, where 84 roles
declare docs on. The README replay and everything it drags in (dashboard,
matomo, keycloak, prometheus with its MCP partners) landed in 365 of 469
rounds instead of one per role, which ran CI run 40625679270 out of
resources: Listmonk#0 memory, OpenProject#0 I/O in the DR drill,
Nextcloud#0/#2 budget (4310-4539 s in the replay each, 60 s over),
BookWyrm#0 drill disk, Matrix#1/#3 pids in the replay; none of those
primaries declares docs in the failing variant.
The guide host now stays on only where the primary app's own variant
declares it (declares_guide, the predicate instructions.py already used);
every other role has it switched off (guide_off), in the planner's round
overrides and in the swarm include set. The host_vars bake drops a docs
edge whenever the round does not deploy web-app-docs. Measured for
listmonk, openproject, translate and mariadb: web-app-docs is deployed in
exactly each primary's declared guide round, swarm and planner agree in
all 12 rounds, and rounds shed up to ten apps (mariadb#1 19 -> 9).
test_guide_rides_the_primary_only pins that on real role data (it fails
with the rule disabled); test_variant_pin_required leaves the guide host
out of its clone comparison and no longer counts the docs key as a
pairing edge, because the replay no longer follows a partner's index.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
5.4 KiB
Python
150 lines
5.4 KiB
Python
"""Which deploy row replays the role's README instructions, and in what mode.
|
|
|
|
The instructions test used to be a job of its own that picked one random role
|
|
per run. Riding along on a deploy row instead costs no extra runner and covers
|
|
every role the sweep touches. The row it rides on is the role's smallest
|
|
variant -- the one enabling the fewest services -- because that is the shape
|
|
the README's Production block describes: the bare role, with the optional
|
|
providers switched off.
|
|
|
|
The replay is a second deploy on the same runner, so the row it picks must be
|
|
one the guide can actually deploy: the role has to be invokable, because that
|
|
is what makes the documentation build give it a Production block, and the
|
|
guide's own mode (``compose`` for a role that ships a stack, ``host`` for one
|
|
installed onto the machine) has to be a mode the role runs and tests.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
from functools import cache
|
|
from typing import TYPE_CHECKING
|
|
|
|
from utils.cache.files import PROJECT_ROOT
|
|
from utils.roles.deploy import role_has_stack
|
|
from utils.roles.meta_lookup import get_role_mode_enabled, get_role_test_skips
|
|
from utils.roles.validation.invokable import _get_invokable_paths, _is_role_invokable
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Collection, Sequence
|
|
from typing import Any
|
|
|
|
|
|
GUIDE_SERVICE_KEY = "docs"
|
|
"""The guide host is the replay's own carrier, not a provider the README's
|
|
Production block describes. Counting it would make the measure circular: the
|
|
variant this function picks is the one that then switches the guide on, which
|
|
on the next read makes that variant one service larger than it was."""
|
|
|
|
|
|
def without_guide(services: Mapping[str, Any]) -> dict[str, Any]:
|
|
"""``services`` without the guide host's entry.
|
|
|
|
Args:
|
|
services: a role's services map or a variant's services override.
|
|
"""
|
|
return {key: value for key, value in services.items() if key != GUIDE_SERVICE_KEY}
|
|
|
|
|
|
def declares_guide(services: Mapping[str, Any]) -> bool:
|
|
"""Whether ``services`` switches the guide host on, as a literal.
|
|
|
|
Args:
|
|
services: a role's resolved services map.
|
|
"""
|
|
entry = services.get(GUIDE_SERVICE_KEY)
|
|
return isinstance(entry, Mapping) and entry.get("enabled") is True
|
|
|
|
|
|
def guide_off(services: Mapping[str, Any]) -> dict[str, Any]:
|
|
"""``services`` with the guide host switched off, when it declares one.
|
|
|
|
Args:
|
|
services: a role's resolved services map.
|
|
"""
|
|
entry = services.get(GUIDE_SERVICE_KEY)
|
|
if not isinstance(entry, Mapping):
|
|
return dict(services)
|
|
return {**services, GUIDE_SERVICE_KEY: {**entry, "enabled": False, "shared": False}}
|
|
|
|
|
|
def _enabled_services(variant: Mapping[str, Any]) -> int:
|
|
services = variant.get("services") or {}
|
|
return sum(
|
|
1
|
|
for key, config in services.items()
|
|
if key != GUIDE_SERVICE_KEY
|
|
and isinstance(config, dict)
|
|
and config.get("enabled")
|
|
)
|
|
|
|
|
|
def smallest_variant(
|
|
variants: Sequence[Mapping[str, Any]], allowed: Collection[str] | None = None
|
|
) -> str:
|
|
"""The variant that enables the fewest services, as the matrix writes it;
|
|
the lowest index wins a tie, and ``''`` means none qualifies.
|
|
|
|
Args:
|
|
variants: the role's rendered variant configs, in index order.
|
|
allowed: the variants this sweep actually deploys. A variant the sweep
|
|
cuts as redundant coverage never reaches a runner, so hanging the
|
|
replay on it would silently drop the role from the instructions
|
|
test; the next-smallest deployed variant carries it instead.
|
|
"""
|
|
candidates = [
|
|
index
|
|
for index in range(len(variants))
|
|
if allowed is None or str(index) in allowed
|
|
]
|
|
if not candidates:
|
|
return ""
|
|
return str(
|
|
min(candidates, key=lambda index: (_enabled_services(variants[index]), index))
|
|
)
|
|
|
|
|
|
@cache
|
|
def guide_deployable(app: str) -> str:
|
|
"""The mode the guide would replay *app* in, or ``''`` when it cannot.
|
|
|
|
Independent of the variant, and cached: the answer is a property of the
|
|
role's files, and :func:`utils.github.variant.axes.assign` asks it once per
|
|
row of every chunk.
|
|
"""
|
|
role_dir = PROJECT_ROOT / "roles" / app
|
|
if not _is_role_invokable(app, _get_invokable_paths()):
|
|
return ""
|
|
mode = "compose" if role_has_stack(role_dir) else "host"
|
|
if mode in get_role_test_skips(role_dir, role_name=app):
|
|
return ""
|
|
if not get_role_mode_enabled(role_dir, mode=mode, role_name=app):
|
|
return ""
|
|
return mode
|
|
|
|
|
|
def guide_variant(
|
|
app: str,
|
|
variants_per_app: Mapping[str, Sequence[Mapping[str, Any]]] | None,
|
|
allowed: Collection[str],
|
|
) -> tuple[str, str]:
|
|
"""The variant of *app* that carries the replay, and the mode it runs in.
|
|
|
|
``('', '')`` when the role has no instructions to replay or this sweep
|
|
deploys no variant that could carry them.
|
|
|
|
Args:
|
|
app: the role.
|
|
variants_per_app: rendered variant configs per app; without them no
|
|
variant can be told to be the smallest one.
|
|
allowed: the variants this sweep deploys, as the matrix writes them.
|
|
"""
|
|
variants = (variants_per_app or {}).get(app)
|
|
if not variants:
|
|
return "", ""
|
|
mode = guide_deployable(app)
|
|
if not mode:
|
|
return "", ""
|
|
variant = smallest_variant(variants, allowed)
|
|
return (variant, mode) if variant else ("", "")
|