mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
The discovery step builds its table with plan.py, not matrix.py, so passing --vpn aborted the chunk before a runner was spent on it. The plan now takes the flag and shows the state as a column, the way it already shows the onion.
215 lines
7.4 KiB
Python
215 lines
7.4 KiB
Python
"""Render the plan of one CI sweep: every candidate row, and where it lands.
|
|
|
|
Usage:
|
|
python -m cli.meta.ci.plan [--distros "debian"] [--filesystem "zfs"]
|
|
[--whitelist "..."] [--priority "..."] [--modes auto]
|
|
[--lifecycles "..."] [--sweep N] [--cli]
|
|
|
|
The plan runs the same pipeline the deploy jobs discover through
|
|
(``cli.meta.ci.matrix``), so what the table shows and what CI deploys cannot
|
|
diverge. One row per ``role#variant`` candidate in global query order, with
|
|
the chunk it falls into and the axes it was assigned.
|
|
|
|
Status per row: ⭐ a priority row, ✅ deployed by this sweep, ❌ beyond the
|
|
sweep's budget (raise ``--offset`` to reach it), ✂️ cut as redundant coverage —
|
|
a clone or a row an earlier one already embeds, which no sweep deploys.
|
|
``--cli`` renders fixed-width terminal tables instead of Markdown.
|
|
|
|
📖 marks the row that replays the role's README instructions after its deploy,
|
|
and names the mode the replay runs in -- which is the guide's own, not the
|
|
row's (:mod:`utils.roles.guide`).
|
|
|
|
🆔 numbers the rows of this table, 1..n, and 🔰 names the 🆔 of the earlier row
|
|
that already embeds this one -- empty when nothing does. A row with a 🔰 is
|
|
redundant coverage: whatever it would prove, the row it points at proves first. 🐑 marks the other reason a row is cut: it
|
|
shares its service set with an earlier role, which stands in for it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
|
|
from cli.meta.ci import matrix, query, slots
|
|
from cli.meta.roles.applications.complexity.render import _dwidth
|
|
from utils.github.variant import axes, instructions, pools, tor, vpn
|
|
from utils.roles.display import display_names
|
|
from utils.symbol_glossary import to_emoji
|
|
|
|
_STAR = to_emoji("priority")
|
|
_OK = to_emoji("enabled")
|
|
_OFF = to_emoji("disabled")
|
|
_CUT = to_emoji("redundant")
|
|
|
|
_COLUMNS = (
|
|
"id",
|
|
"chunk",
|
|
"name",
|
|
"variant",
|
|
"mode",
|
|
"distro",
|
|
"filesystem",
|
|
"tor",
|
|
"vpn",
|
|
"triggered",
|
|
"covered_by",
|
|
"instructions",
|
|
"clone",
|
|
)
|
|
_GLYPH = {"triggered": "enabled", "distro": "distros"}
|
|
_HEADERS = tuple(
|
|
f"{to_emoji(_GLYPH.get(key, key))} {key.replace('_', ' ').capitalize()}"
|
|
for key in _COLUMNS
|
|
)
|
|
|
|
|
|
def _key(entry: dict[str, str]) -> tuple[str, ...]:
|
|
"""What identifies one deploy row: every axis it was assigned.
|
|
|
|
A priority role runs every combination of its variant, and a selection can
|
|
ask for one variant on two distros, so app+variant alone matches several
|
|
rows and would report the first one's chunk for all of them -- including
|
|
for rows the budget never reaches, which the table would then mark as
|
|
deployed.
|
|
"""
|
|
return (
|
|
entry["apps"],
|
|
entry["variant"],
|
|
entry["mode"],
|
|
entry["tor"],
|
|
entry["vpn"],
|
|
entry["distro"],
|
|
entry["filesystem"],
|
|
)
|
|
|
|
|
|
def _chunk_of(entry: dict[str, str], plan: list[list[dict[str, str]]]) -> int | None:
|
|
"""Index of the chunk *entry* landed in, or ``None`` when this sweep
|
|
leaves it to the next one."""
|
|
key = _key(entry)
|
|
for index, chunk in enumerate(plan):
|
|
if any(_key(row) == key for row in chunk):
|
|
return index
|
|
return None
|
|
|
|
|
|
def cells(
|
|
entries: list[dict[str, str]],
|
|
plan: list[list[dict[str, str]]],
|
|
) -> list[tuple[str, ...]]:
|
|
rows = []
|
|
positions: dict[str, str] = {}
|
|
for counter, entry in enumerate(entries, start=1):
|
|
positions.setdefault(entry.get("id", ""), str(counter))
|
|
for counter, entry in enumerate(entries, start=1):
|
|
chunk = _chunk_of(entry, plan)
|
|
if instructions.redundant(entry):
|
|
status, where = _CUT, ""
|
|
elif chunk is None:
|
|
status, where = _OFF, ""
|
|
else:
|
|
status = _STAR if entry["priority"] == "true" else _OK
|
|
where = str(chunk)
|
|
covered = entry.get("covered", "0")
|
|
rows.append(
|
|
(
|
|
str(counter),
|
|
where,
|
|
entry["apps"],
|
|
entry["variant"],
|
|
to_emoji(entry["mode"]),
|
|
to_emoji(entry["distro"]),
|
|
to_emoji(entry["filesystem"]),
|
|
to_emoji("tor" if entry["tor"] == "true" else "clearnet"),
|
|
to_emoji("vpn" if entry["vpn"] == "true" else "direct"),
|
|
status,
|
|
positions.get(covered, covered) if covered not in ("", "0") else "",
|
|
to_emoji(entry["instructions"]) if entry["instructions"] else "",
|
|
to_emoji("clone") if entry.get("clone") == "true" else "",
|
|
)
|
|
)
|
|
return rows
|
|
|
|
|
|
def _title(plan: list[list[dict[str, str]]], sweep: int) -> str:
|
|
sizes = ", ".join(str(len(chunk)) for chunk in plan) or "-"
|
|
return (
|
|
f"sweep {sweep} · chunk size {slots.chunk_size()} "
|
|
f"· {len(plan)} chunk(s) [{sizes}] · budget {slots.available()}"
|
|
)
|
|
|
|
|
|
def render_markdown(title: str, rows: list[tuple[str, ...]]) -> str:
|
|
lines = [
|
|
"## Plan 🗺️",
|
|
"",
|
|
f"### {title}",
|
|
"",
|
|
"| " + " | ".join(_HEADERS) + " |",
|
|
"|" + "---|" * len(_HEADERS),
|
|
]
|
|
lines += ["| " + " | ".join(cell) + " |" for cell in rows]
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _pad(value: str, width: int) -> str:
|
|
return value + " " * max(width - _dwidth(value), 0)
|
|
|
|
|
|
def render_cli(title: str, rows: list[tuple[str, ...]]) -> str:
|
|
widths = [
|
|
max([_dwidth(header), *(_dwidth(cell[i]) for cell in rows)])
|
|
for i, header in enumerate(_HEADERS)
|
|
]
|
|
header = " ".join(_pad(h, w) for h, w in zip(_HEADERS, widths, strict=True))
|
|
rule = " ".join("-" * w for w in widths)
|
|
body = [
|
|
" ".join(_pad(value, w) for value, w in zip(cell, widths, strict=True))
|
|
for cell in rows
|
|
]
|
|
return "\n".join([title, header, rule, *body])
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="Render the plan of one CI sweep.")
|
|
parser.add_argument("--distros", default="")
|
|
parser.add_argument("--whitelist", default="")
|
|
parser.add_argument("--priority", default="")
|
|
parser.add_argument("--modes", default=query.ALL_MODES)
|
|
parser.add_argument("--lifecycles", default="")
|
|
parser.add_argument("--sweep", type=int, default=None)
|
|
parser.add_argument("--tor", default=None)
|
|
parser.add_argument("--vpn", default=None)
|
|
parser.add_argument("--filesystem", default="")
|
|
parser.add_argument("--offset", default=None)
|
|
parser.add_argument("--cli", action="store_true")
|
|
args = parser.parse_args(argv)
|
|
|
|
codec = display_names()
|
|
if args.lifecycles.strip():
|
|
os.environ["INFINITO_LIFECYCLES"] = args.lifecycles
|
|
|
|
sweep = axes.resolve_sweep() if args.sweep is None else args.sweep
|
|
entries = matrix.entries_of(
|
|
modes=query.resolve_modes(args.modes),
|
|
whitelist=codec.decode_list(args.whitelist),
|
|
priority=codec.decode_list(args.priority),
|
|
lifecycles=args.lifecycles,
|
|
sweep=sweep,
|
|
tor_mode=tor.resolve_tor_mode(args.tor),
|
|
vpn_mode=vpn.resolve_vpn_mode(args.vpn),
|
|
distros=pools.resolve_distros(args.distros),
|
|
filesystems=pools.resolve_filesystems(args.filesystem),
|
|
)
|
|
plan = matrix.chunks_of(entries, matrix.resolve_offset(args.offset))
|
|
rows = cells(entries, plan)
|
|
|
|
render = render_cli if args.cli else render_markdown
|
|
print(render(_title(plan, sweep), rows))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|