mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-09 12:17:13 +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>
244 lines
7.7 KiB
Python
244 lines
7.7 KiB
Python
"""Single source of truth for the CI app-discovery query.
|
|
|
|
Usage:
|
|
python -m cli.meta.ci.query --mode compose|swarm|host [--matrix] [--format json]
|
|
|
|
Both the production discovery (scripts/meta/resolve/apps.sh) and the
|
|
plan table (cli.meta.ci.plan) resolve role lists through this
|
|
module, so the plan shows exactly the query the run executes. One query
|
|
per mode, sharing filter (mode + INFINITO_WHITELIST + INFINITO_BLACKLIST),
|
|
INFINITO_DISCOVERY_SORT order, lifecycle envelope and the
|
|
INFINITO_MAX_JOBS cap ('auto' resolves per mode via cli.meta.ci.slots):
|
|
|
|
compose whole-role rows, clones sorted last, variants packed into
|
|
size/storage bundles for the job count
|
|
host whole-role rows, clones and tested-elsewhere roles sorted
|
|
last, bundled like compose
|
|
swarm one row per variant (``role#variant`` tokens), ranked and
|
|
budget-cut on per-variant metrics; the cut can select a
|
|
subset of a role's variants
|
|
|
|
``--matrix`` renders exactly the row basis the mode's selection runs
|
|
on, so the matrix order IS the selection priority. ``capped=False``
|
|
returns the full ordered candidate list so callers can show what fell
|
|
behind the budget cut.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
from cli.meta.ci import slots
|
|
from utils.cache.files import PROJECT_ROOT, read_text
|
|
from utils.roles.display import display_names
|
|
|
|
MODES = ("compose", "swarm", "host")
|
|
|
|
|
|
def expands_variants(mode: str) -> bool:
|
|
"""Whether *mode* queries, ranks and triggers one CI job per variant.
|
|
Swarm does (``role#variant`` tokens through the whole chain); compose
|
|
and host bundle every variant into whole-role jobs (variant.bundles
|
|
SPOT), so their query rows stay whole-role too."""
|
|
return mode == "swarm"
|
|
|
|
|
|
def build_filter(
|
|
mode: str,
|
|
whitelist: str = "",
|
|
blacklist: str = "",
|
|
covered: tuple[str, ...] = (),
|
|
) -> str:
|
|
parts = [f"test_{mode} == true"]
|
|
include = ",".join(whitelist.split())
|
|
if include:
|
|
parts.append(f"name %% {{{include}}}")
|
|
exclude = ",".join(blacklist.split())
|
|
if exclude:
|
|
parts.append(f"not (name %% {{{exclude}}})")
|
|
if covered:
|
|
parts.append(f"not (name %% {{{','.join(covered)}}})")
|
|
return " and ".join(parts)
|
|
|
|
|
|
def compose_covered(
|
|
mode: str, *, whitelist: str, blacklist: str, lifecycles: str
|
|
) -> tuple[str, ...]:
|
|
"""Roles the same run's compose line already triggers; the host line
|
|
drops them instead of redeploying them in a second single-node mode.
|
|
Active only when the caller signals a co-running compose line via
|
|
INFINITO_HOST_EXCLUDE_COMPOSE."""
|
|
if mode != "host":
|
|
return ()
|
|
flag = os.environ["INFINITO_HOST_EXCLUDE_COMPOSE"].strip().lower()
|
|
if flag not in ("1", "true", "yes"):
|
|
return ()
|
|
return tuple(
|
|
discover(
|
|
"compose",
|
|
whitelist=whitelist,
|
|
blacklist=blacklist,
|
|
lifecycles=lifecycles,
|
|
)
|
|
)
|
|
|
|
|
|
def _sort_spec(mode: str) -> str:
|
|
"""The discovery sort for *mode*. Every mode prepends clones-last (one
|
|
representative per dna cluster stays ahead of the budget cut, so redundant
|
|
same-service-set variants fall behind it first); host additionally sorts
|
|
tested-elsewhere roles down so its slots go to roles no compose or swarm
|
|
matrix already covers."""
|
|
spec = os.environ["INFINITO_DISCOVERY_SORT"]
|
|
if not spec.strip():
|
|
for line in read_text(str(PROJECT_ROOT / "default.env")).splitlines():
|
|
if line.startswith("INFINITO_DISCOVERY_SORT="):
|
|
spec = line.split("=", 1)[1].strip().strip('"')
|
|
break
|
|
prefixes = {
|
|
"compose": "asc clone",
|
|
"swarm": "asc clone",
|
|
"host": "asc clone,asc tested_elsewhere",
|
|
}
|
|
prefix = prefixes.get(mode)
|
|
if prefix:
|
|
return f"{prefix},{spec}" if spec.strip() else prefix
|
|
return spec
|
|
|
|
|
|
def max_jobs(mode: str, *, blacklist: str = "", lifecycles: str = "") -> int:
|
|
"""The mode's slot budget minus the jobs the run's priority line
|
|
already occupies (the priority whitelist arrives here as the regular
|
|
line's blacklist), floored at 0."""
|
|
raw = os.environ["INFINITO_MAX_JOBS"].strip()
|
|
budget = slots.mode_slots()[mode] if raw in ("", "auto") else int(raw)
|
|
if blacklist.strip():
|
|
budget -= len(discover(mode, whitelist=blacklist, lifecycles=lifecycles))
|
|
return max(budget, 0)
|
|
|
|
|
|
def _query_argv(
|
|
mode: str,
|
|
*,
|
|
whitelist: str,
|
|
blacklist: str,
|
|
lifecycles: str,
|
|
job_cap: int | None,
|
|
fmt: list[str],
|
|
covered: tuple[str, ...] = (),
|
|
) -> list[str]:
|
|
args = [
|
|
sys.executable,
|
|
"-m",
|
|
"cli.meta.roles.applications.complexity",
|
|
"--deploy-mode",
|
|
mode,
|
|
"--filter",
|
|
build_filter(mode, whitelist, blacklist, covered),
|
|
"--sort",
|
|
_sort_spec(mode),
|
|
*fmt,
|
|
]
|
|
if expands_variants(mode):
|
|
args.append("--variant")
|
|
envelope = lifecycles or os.environ["INFINITO_LIFECYCLES"]
|
|
if envelope.strip():
|
|
args += ["--lifecycles", envelope]
|
|
if job_cap is not None:
|
|
args += ["--max-jobs", str(job_cap)]
|
|
return args
|
|
|
|
|
|
def discover(
|
|
mode: str,
|
|
*,
|
|
whitelist: str = "",
|
|
blacklist: str = "",
|
|
lifecycles: str = "",
|
|
capped: bool = True,
|
|
) -> list[str]:
|
|
"""The ordered selection the discovery query yields for *mode*:
|
|
role names for compose and host, ``role#variant`` tokens for swarm."""
|
|
job_cap = None
|
|
if capped:
|
|
job_cap = max_jobs(mode, blacklist=blacklist, lifecycles=lifecycles)
|
|
if job_cap == 0:
|
|
return []
|
|
covered = compose_covered(
|
|
mode, whitelist=whitelist, blacklist=blacklist, lifecycles=lifecycles
|
|
)
|
|
out = subprocess.run(
|
|
_query_argv(
|
|
mode,
|
|
whitelist=whitelist,
|
|
blacklist=blacklist,
|
|
lifecycles=lifecycles,
|
|
job_cap=job_cap,
|
|
fmt=["--format", "string"],
|
|
covered=covered,
|
|
),
|
|
cwd=PROJECT_ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
).stdout
|
|
return [line.strip() for line in out.splitlines() if line.strip()]
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Run the CI app-discovery query for one deploy mode."
|
|
)
|
|
parser.add_argument("--mode", required=True, choices=MODES)
|
|
parser.add_argument(
|
|
"--matrix",
|
|
action="store_true",
|
|
help=(
|
|
"Render the full complexity matrix in query order (uncapped) "
|
|
"on the mode's own row basis: per-variant rows for swarm, "
|
|
"whole-role rows for compose and host. The matrix order is "
|
|
"the selection priority."
|
|
),
|
|
)
|
|
parser.add_argument("--format", choices=("json",), dest="fmt")
|
|
args = parser.parse_args(argv)
|
|
|
|
codec = display_names()
|
|
whitelist = codec.decode_list(os.environ["INFINITO_WHITELIST"])
|
|
blacklist = codec.decode_list(os.environ["INFINITO_BLACKLIST"])
|
|
|
|
if args.matrix:
|
|
return subprocess.run(
|
|
_query_argv(
|
|
args.mode,
|
|
whitelist=whitelist,
|
|
blacklist=blacklist,
|
|
lifecycles="",
|
|
job_cap=None,
|
|
fmt=["-s"],
|
|
covered=compose_covered(
|
|
args.mode,
|
|
whitelist=whitelist,
|
|
blacklist=blacklist,
|
|
lifecycles="",
|
|
),
|
|
),
|
|
cwd=PROJECT_ROOT,
|
|
check=False,
|
|
).returncode
|
|
|
|
roles = discover(args.mode, whitelist=whitelist, blacklist=blacklist)
|
|
if args.fmt == "json":
|
|
print(json.dumps(roles))
|
|
else:
|
|
print("\n".join(roles))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|