mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 12:47:04 +00:00
CI deploy jobs were titled with the raw role id, so a run page read as a
column of dash-joined prefixes. They now carry a display name: the category
path as hanzi labels, then the role's README heading or the part of the id
the categories do not already say, whichever is shorter.
utils/roles/display.py owns both directions. Encoding happens once, at
discovery time, as a `label` key on every matrix entry; the workflows render
`<glyph> ${{ matrix.label }}` and keep the glyph as a literal because the
retrigger reads the deploy mode off it.
Decoding is what makes the retrigger keep working. It accepts a display name
with or without its variant shard and a bare role id unchanged, so job names
from runs that predate this still parse and a hand-typed dispatch never has
to spell hanzi. The parser therefore stopped matching an id-shaped tail and
asks the codec instead; a job the codec does not recognise is skipped rather
than counted as a role.
The role selections go out as display names too, so the run title reads like
the jobs it spawns. Four consumers decode them back, each the single place
the value is actually used: the whitelist resolver before it checks the role
directory, the discovery query on its env, and the sequencing and plan CLIs
on their arguments.
Every fragment is joined by `·`, including before the title, and the heading
loses its whitespace and any emoji variation selector. A display name holding
a space would make the space-separated whitelist and priority inputs
unparseable with more than one role.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
108 lines
3.8 KiB
Python
108 lines
3.8 KiB
Python
"""Decide whether a CI line deploys its modes in sequence or in parallel.
|
|
|
|
Usage:
|
|
python -m cli.meta.ci.sequencing [--modes "swarm compose host"]
|
|
[--whitelist "..."] [--blacklist "..."] [--lifecycles "..."]
|
|
[--choice auto|serial|parallel]
|
|
|
|
A *line* is the priority (⭐) or the regular (🔁) half of an orchestrator
|
|
run. Its job count is the sum of the deploy-matrix entries every active
|
|
mode contributes, counted on the row basis the discover steps use: swarm
|
|
selections are ``role#variant`` tokens mapping 1:1 onto jobs, compose and
|
|
host selections are whole roles whose variants pack into bundles
|
|
(utils.github.variant.bundles, the same packing the matrix runs).
|
|
|
|
GitHub cancels a job that sat queued for 24 hours. A line that starts every
|
|
mode at once floods the runner pool, so the tail of its matrix waits past
|
|
that cut and dies unrun. ``auto`` therefore serialises the modes -- swarm,
|
|
then compose, then host, heaviest first -- once the line exceeds
|
|
``INFINITO_CI_SEQUENTIAL_THRESHOLD`` jobs, which restarts the queue clock
|
|
per mode. Below the threshold the modes run in parallel for wall-clock.
|
|
|
|
The count omits the runner-storage filter scripts/meta/resolve/apps.sh
|
|
applies inside GitHub Actions, so it can only overestimate, erring towards
|
|
the sequential layout that cannot be cancelled.
|
|
|
|
Writes ``jobs=<n>`` and ``sequencing=<serial|parallel>``, ready to append to
|
|
``$GITHUB_OUTPUT``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
|
|
from cli.meta.ci.query import MODES, discover, expands_variants
|
|
from utils.cache.applications import get_variants
|
|
from utils.github.variant.bundles import compose_bundle_counts
|
|
from utils.roles.display import display_names
|
|
|
|
CHOICES = ("auto", "serial", "parallel")
|
|
|
|
|
|
def mode_jobs(mode: str, *, whitelist: str, blacklist: str, lifecycles: str) -> int:
|
|
"""Deploy-matrix jobs *mode* contributes to the line."""
|
|
selection = discover(
|
|
mode, whitelist=whitelist, blacklist=blacklist, lifecycles=lifecycles
|
|
)
|
|
if expands_variants(mode):
|
|
return len(selection)
|
|
return sum(compose_bundle_counts(selection, get_variants()).values())
|
|
|
|
|
|
def line_jobs(modes: str, *, whitelist: str, blacklist: str, lifecycles: str) -> int:
|
|
return sum(
|
|
mode_jobs(mode, whitelist=whitelist, blacklist=blacklist, lifecycles=lifecycles)
|
|
for mode in MODES
|
|
if mode in modes.split()
|
|
)
|
|
|
|
|
|
def threshold() -> int:
|
|
return int(os.environ["INFINITO_CI_SEQUENTIAL_THRESHOLD"])
|
|
|
|
|
|
def decide(jobs: int | None, choice: str) -> str:
|
|
"""``choice`` verbatim unless it is ``auto``, which compares *jobs*
|
|
against the threshold."""
|
|
if choice != "auto":
|
|
return choice
|
|
return "serial" if jobs > threshold() else "parallel"
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Decide serial or parallel deploy modes for one CI line."
|
|
)
|
|
parser.add_argument("--modes", default=" ".join(MODES))
|
|
parser.add_argument("--whitelist", default="")
|
|
parser.add_argument("--blacklist", default="")
|
|
parser.add_argument("--lifecycles", default="")
|
|
parser.add_argument("--choice", default="auto", choices=CHOICES)
|
|
args = parser.parse_args(argv)
|
|
|
|
codec = display_names()
|
|
args.whitelist = codec.decode_list(args.whitelist)
|
|
args.blacklist = codec.decode_list(args.blacklist)
|
|
|
|
if args.lifecycles.strip():
|
|
os.environ["INFINITO_LIFECYCLES"] = args.lifecycles
|
|
|
|
jobs = None
|
|
if args.choice == "auto":
|
|
jobs = line_jobs(
|
|
args.modes,
|
|
whitelist=args.whitelist,
|
|
blacklist=args.blacklist,
|
|
lifecycles=args.lifecycles,
|
|
)
|
|
|
|
print(f"jobs={'' if jobs is None else jobs}")
|
|
print(f"sequencing={decide(jobs, args.choice)}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|