mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
INFINITO_DISCOVERY_SORT is the single source of truth for the discovery order, but 'asc clone' lived in sort_spec() and was concatenated in front of it, so the declared value never showed the order a run actually takes. Declare the key where the rest of the order is declared and let sort_spec return the value verbatim. test_clones_sort_last_ahead_of_the_declared_spec now guards the declaration instead of the concatenation, and covered rows sort directly behind the clone key so they land at the tail of the ranking. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
224 lines
7.1 KiB
Python
224 lines
7.1 KiB
Python
"""Single source of truth for the CI app-discovery query.
|
|
|
|
Usage:
|
|
python -m cli.meta.ci.query [--modes "swarm compose host"] [--matrix]
|
|
[--format json]
|
|
|
|
One query per run, not one per deploy mode. Every row is a ``role#variant``
|
|
selection, so the row basis, the ranking and the budget cut all speak the
|
|
same unit and the cut can select a subset of a role's variants. Which deploy
|
|
mode a row actually runs in is decided afterwards by the rotation in
|
|
``utils.github.variant.bundles``; the query only decides which rows exist.
|
|
|
|
The mode selection is a *filter*, not an axis: ``--modes swarm`` keeps the
|
|
rows whose role can be deployed as a swarm stack, ``--modes "compose swarm"``
|
|
keeps the rows at least one of the two can take. The shared filter is
|
|
mode-clause + INFINITO_WHITELIST + INFINITO_BLACKLIST, the order is
|
|
INFINITO_DISCOVERY_SORT, whose first key sorts clones last (one representative
|
|
per dna cluster stays ahead of the budget cut, so redundant same-service-set
|
|
variants fall behind it first), and the lifecycle envelope is
|
|
INFINITO_LIFECYCLES.
|
|
|
|
``--matrix`` renders the full ordered candidate list, so the matrix order IS
|
|
the selection priority. Every human-facing view of that list goes through here
|
|
rather than re-deriving the sort and the filter: hand-rolling
|
|
``--sort "$INFINITO_DISCOVERY_SORT" --filter "compose == true"`` reads the
|
|
wrong column (``compose`` is what a role can do, ``test_compose`` is what CI
|
|
tests), which silently shows an order no run will ever take.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
|
|
from utils.cache.files import PROJECT_ROOT
|
|
from utils.env.parser import env_setting
|
|
from utils.github.variant.axes import MODES
|
|
from utils.roles.display import display_names
|
|
|
|
ALL_MODES = "auto"
|
|
|
|
|
|
def resolve_modes(raw: str) -> tuple[str, ...]:
|
|
"""The deploy modes a run may draw from. ``auto`` (or empty) means every
|
|
mode; anything else is the listed subset, in :data:`MODES` order.
|
|
|
|
Raises:
|
|
SystemExit: a token that names no known mode -- a typo must not
|
|
silently narrow the run to nothing.
|
|
"""
|
|
tokens = raw.replace(",", " ").split()
|
|
if not tokens or tokens == [ALL_MODES]:
|
|
return MODES
|
|
unknown = [token for token in tokens if token not in MODES]
|
|
if unknown:
|
|
raise SystemExit(
|
|
f"unknown deploy mode(s): {' '.join(unknown)}; expected "
|
|
f"{', '.join(MODES)} or {ALL_MODES!r}"
|
|
)
|
|
return tuple(mode for mode in MODES if mode in tokens)
|
|
|
|
|
|
def build_filter(
|
|
modes: tuple[str, ...], whitelist: str = "", blacklist: str = ""
|
|
) -> str:
|
|
"""The complexity ``--filter`` expression for one run."""
|
|
clause = " or ".join(f"test_{mode} == true" for mode in modes)
|
|
parts = [f"({clause})" if len(modes) > 1 else clause]
|
|
include = ",".join(whitelist.split())
|
|
if include:
|
|
parts.append(f"name %% {{{include}}}")
|
|
exclude = ",".join(blacklist.split())
|
|
if exclude:
|
|
parts.append(f"not (name %% {{{exclude}}})")
|
|
return " and ".join(parts)
|
|
|
|
|
|
def sort_spec() -> str:
|
|
"""The discovery sort as declared in default.env."""
|
|
return env_setting("INFINITO_DISCOVERY_SORT").strip()
|
|
|
|
|
|
def _query_argv(
|
|
modes: tuple[str, ...],
|
|
*,
|
|
whitelist: str,
|
|
blacklist: str,
|
|
lifecycles: str,
|
|
fmt: list[str],
|
|
variant: bool = True,
|
|
) -> list[str]:
|
|
"""The complexity call this run discovers through.
|
|
|
|
Args:
|
|
variant: ``False`` collapses the rows to whole roles. Only ``--matrix``
|
|
renders that view; discovery itself is always per ``role#variant``,
|
|
because that is the unit the budget cut and the deploy speak.
|
|
"""
|
|
args = [
|
|
sys.executable,
|
|
"-m",
|
|
"cli.meta.roles.applications.complexity",
|
|
*(["--variant"] if variant else []),
|
|
"--filter",
|
|
build_filter(modes, whitelist, blacklist),
|
|
"--sort",
|
|
sort_spec(),
|
|
*fmt,
|
|
]
|
|
envelope = lifecycles or env_setting("INFINITO_LIFECYCLES")
|
|
if envelope.strip():
|
|
args += ["--lifecycles", envelope]
|
|
return args
|
|
|
|
|
|
def discover_rows(
|
|
modes: tuple[str, ...] = MODES,
|
|
*,
|
|
whitelist: str = "",
|
|
blacklist: str = "",
|
|
lifecycles: str = "",
|
|
) -> list[dict]:
|
|
"""The ordered candidate rows, each the complexity payload of one
|
|
``role#variant``. Uncapped: the chunker decides what a sweep spends."""
|
|
out = subprocess.run(
|
|
_query_argv(
|
|
modes,
|
|
whitelist=whitelist,
|
|
blacklist=blacklist,
|
|
lifecycles=lifecycles,
|
|
fmt=["--format", "json"],
|
|
),
|
|
cwd=PROJECT_ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
).stdout
|
|
return json.loads(out) if out.strip() else []
|
|
|
|
|
|
def token(row: dict) -> str:
|
|
"""The ``role#variant`` selection token of one query row."""
|
|
return f"{row['name']}#{row['variant']}"
|
|
|
|
|
|
def row_modes(row: dict, modes: tuple[str, ...] = MODES) -> tuple[str, ...]:
|
|
"""The selected deploy modes this row can actually run in. Never empty
|
|
for a row the query returned -- the filter kept it precisely because at
|
|
least one selected mode claims it."""
|
|
return tuple(mode for mode in modes if row.get(f"test_{mode}"))
|
|
|
|
|
|
def discover(
|
|
modes: tuple[str, ...] = MODES,
|
|
*,
|
|
whitelist: str = "",
|
|
blacklist: str = "",
|
|
lifecycles: str = "",
|
|
) -> list[str]:
|
|
"""The ordered ``role#variant`` tokens the query yields."""
|
|
return [
|
|
token(row)
|
|
for row in discover_rows(
|
|
modes, whitelist=whitelist, blacklist=blacklist, lifecycles=lifecycles
|
|
)
|
|
]
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Run the CI app-discovery query for one workflow run."
|
|
)
|
|
parser.add_argument("--modes", default=ALL_MODES)
|
|
parser.add_argument(
|
|
"--matrix",
|
|
action="store_true",
|
|
help=(
|
|
"Render the full complexity matrix in query order: one row per "
|
|
"role#variant. The matrix order is the selection priority."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--roles",
|
|
action="store_true",
|
|
help=(
|
|
"With --matrix: one row per role instead of per role#variant, in "
|
|
"the same query order."
|
|
),
|
|
)
|
|
parser.add_argument("--format", choices=("json",), dest="fmt")
|
|
args = parser.parse_args(argv)
|
|
|
|
codec = display_names()
|
|
whitelist = codec.decode_list(env_setting("INFINITO_WHITELIST"))
|
|
blacklist = codec.decode_list(env_setting("INFINITO_BLACKLIST"))
|
|
modes = resolve_modes(args.modes)
|
|
|
|
if args.matrix:
|
|
return subprocess.run(
|
|
_query_argv(
|
|
modes,
|
|
whitelist=whitelist,
|
|
blacklist=blacklist,
|
|
lifecycles="",
|
|
fmt=["-s"],
|
|
variant=not args.roles,
|
|
),
|
|
cwd=PROJECT_ROOT,
|
|
check=False,
|
|
).returncode
|
|
|
|
tokens = discover(modes, whitelist=whitelist, blacklist=blacklist)
|
|
if args.fmt == "json":
|
|
print(json.dumps(tokens))
|
|
else:
|
|
print("\n".join(tokens))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|