Files
computer-playbook/cli/meta/ci/sequencing.py
T
kevinveenbirkenbachandClaude Opus 5 636701ac26 feat(ci): serialise a big deploy line's modes
GitHub cancels a job queued for 24h. A line starting swarm, compose and
host at once queues its whole matrix in one go, so with more roles than
runner slots the tail waits past that cut and dies unrun -- 54 of 241
jobs in run 30937913632.

Deploy the modes one after the other instead, heaviest first, which
restarts the queue clock per mode. cli.meta.ci.sequencing counts a
line's matrix jobs on the row basis the discover steps use and
serialises above INFINITO_CI_SEQUENTIAL_THRESHOLD; below it the modes
still start together, so a small diff pays no wall-clock for this.

A needs: edge is static, so both layouts exist as jobs and the decision
skips one. Their conditions are structurally asserted, not evaluated:
ten near-identical if expressions invite a copied sentinel or a missing
chain edge, and both failures are silent at runtime.

mode_fail_fast stops a serial line at its first failed mode, on by
default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 10:47:40 +02:00

103 lines
3.6 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
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)
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())