Files
computer-playbook/utils/roles/guide.py
T
kevinveenbirkenbachandClaude Opus 5 74a91e7adf refactor(ci): declare the README replay as a service edge instead of a matrix flag
The replay used to be decided by the matrix: utils.roles.guide picked a
variant, instructions.mark wrote a flag, and the workflow appended
web-app-docs to the row's apps. Now each invokable role declares the
guide host in meta/services.yml with bond 0.5 behind the group gate and
switches it on in exactly one variant, and the deploy pulls it in
through that edge. The matrix only reads the declaration back.

web-app-docs was no provider at all: the registry requires shared,
provides or an alias on the role's own entity entry and it had none, so
the 188 declarations would have been decorative. Registering it brings
a structural contract with it -- a dynamically loaded service role must
set its run_once flag as its very first task or load/app.yml includes
it again -- so the role body moves into tasks/00_core.yml and main.yml
becomes the guard.

smallest_variant no longer counts the guide. It picks the variant that
then switches the guide on, which on the next read would make that
variant one service larger and no longer the smallest; without the
exclusion 30 of 188 roles disagreed with the file.

The job title loses the marker. The row is an ordinary deploy of its
variant, so nothing distinguishes it any more -- which is why the unit
test now asserts the label is left alone.

Measured against the previous tree, the sweep marks 68 rows where it
marked 88. Twenty roles declare the guide on a variant this sweep does
not run in the guide's mode, and by decision the declaration is
absolute: the file is the truth and those roles carry no replay in such
a run. The old code instead searched the deployed variants, which hid
the mismatch behind a fallback.

The same measurement found that the predicate first read the group gate
as a declaration -- the gate renders to a non-empty string, so every
variant looked like the declaring one. It now tests for a literal true.
Neither defect showed up in the unit tests, which check the new logic
rather than its difference to the old.

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

118 lines
4.3 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 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, Mapping, 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 _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 ("", "")