mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-13 22:26:49 +00:00
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>
118 lines
4.3 KiB
Python
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 ("", "")
|