mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-13 22:26:49 +00:00
PR #1424's Blender#0 batch (run 42540252191) died on centos aarch64
with "community.general.flatpak: Nothing matches org.blender.Blender in
remote flathub": centos installs Blender from Flathub, which publishes
it for x86_64 only, and CentOS Stream 10 has no other source (no EPEL 10
package, official tarballs linux-x64 only). Debian, Ubuntu, Fedora and
Manjaro ARM ship aarch64 Blender, so a role-wide amd64 pin would drop
arm64 coverage there for nothing.
- meta/packages.yml: blender.centos declares architectures: [amd64];
utils/packages/schema.py validates the key, rejects unknown entry
keys, and registry.py rejects an entry key at id level, so a typo
cannot silently drop the narrowing.
- CI matrix (utils/github/variant/pins.py, axes.py,
cli/meta/ci/validate.py): rows keep their distro and architecture
rotation; only a (distro, arch) pair whose packages are not published
moves, a pinned %centos:arm64 is refused with one line, and a broken
registry is refused instead of raising a traceback.
- ci trigger --failed (cli/administration/deploy/ci/selections.py)
replays such a failed job with its distro and without the refused
architecture, warning about it, instead of aborting the dispatch.
- package_install refuses before any module runs on a host whose
architecture the entry does not publish, reading
HOST_ARCHITECTURE_BY_KERNEL from group_vars; removal is not refused.
- Docs: .github/workflows/README.md and ADR 0002 describe the
per-(distro, arch) narrowing.
Accepted MEDUSA finding: MEDUSA-FTSCAN-231 (critical, SQL injection
signature) on plugins/action/package_install.py, the unchanged failure
message "could not gather facts from the delegation target"; present on
origin/main 63e933e413 at line 250, the plugin has no SQL or database
sink, reviewed with kvb-think-criticize as a false positive.
🎯 Fix: 90 %
💥 Break: 4 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 86
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
240 lines
9.4 KiB
Python
240 lines
9.4 KiB
Python
"""Check a run's selection inputs against the branch before anything deploys.
|
|
|
|
Usage:
|
|
python -m cli.meta.ci.validate [--whitelist "..."] [--priority "..."]
|
|
[--modes auto] [--network auto] [--vpn auto]
|
|
[--distros "..."] [--filesystem "..."]
|
|
[--architectures "..."]
|
|
[--lifecycles "..."]
|
|
|
|
A selection token names a row that has to exist *on this branch*
|
|
(:mod:`utils.github.variant.selection`). Nothing else in the chain can tell
|
|
the operator that early: the matrix builder raises on the first bad token, one
|
|
per chunk job, so a stale list costs one full CI round per entry and reports
|
|
the second problem only after the first is fixed. Every problem is collected
|
|
here instead, and the exit code gates the run.
|
|
|
|
What makes a token bad, in the order this checks it:
|
|
|
|
* it does not parse, or names a mode or network mode that does not exist;
|
|
* it pins a variant the role does not declare (any more): the usual case is a
|
|
list carried over from an older run whose variants have since been renumbered;
|
|
* it pins a mode the row cannot take, a network mode the row, the mode or the
|
|
run's own network axis rules out, or a distro, filesystem or architecture
|
|
the run's own pool does not hold.
|
|
|
|
A token naming a role the discovery query does not return at all is reported
|
|
too, but as a warning, whether or not it pins axes: the diff-derived whitelist
|
|
legitimately names roles the envelope filters out, and it pins the variants a
|
|
change reaches. Role names themselves are checked against `roles/` before the
|
|
run starts (scripts/github/resolve/effective_whitelist.sh), so a typo is caught
|
|
there rather than swallowed here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
|
|
from cli.meta.ci import matrix, query
|
|
from utils.cache.applications import get_variants
|
|
from utils.github.variant import axes, network, pools, selection, vpn
|
|
from utils.roles.display import display_names
|
|
|
|
|
|
def problems(
|
|
tokens: str,
|
|
*,
|
|
modes: tuple[str, ...],
|
|
network_input: str,
|
|
vpn_mode: str,
|
|
distros: tuple[str, ...],
|
|
filesystems: tuple[str, ...],
|
|
architectures: tuple[str, ...] = pools.ARCHITECTURES,
|
|
lifecycles: str,
|
|
label: str,
|
|
priority: bool = False,
|
|
) -> tuple[list[str], list[str]]:
|
|
"""Every reason the tokens of one input cannot deploy on this branch.
|
|
|
|
A token that narrows nothing beyond the role name is answered by existence:
|
|
it asks for the role in whatever axes the rotation picks, so any discovered
|
|
row satisfies it. Rows are keyed by ``(name, variant)`` and a variant is an
|
|
int, so looking such a token up variant-first can only miss, which reported
|
|
every collapsed priority entry as matching nothing and left a role that
|
|
really had no row indistinguishable from one with a full set.
|
|
|
|
Args:
|
|
tokens: the raw ``whitelist``/``priority`` value.
|
|
modes: the run's selected deploy modes.
|
|
network_input: the run's network axis.
|
|
vpn_mode: the run's mesh axis.
|
|
distros: the distro pool the run draws from.
|
|
filesystems: the filesystem pool the run draws from.
|
|
architectures: the CPU architecture pool the run draws from; a role
|
|
that declares its own narrows it further.
|
|
lifecycles: the run's lifecycle envelope.
|
|
label: the input's name, for the messages.
|
|
priority: whether the tokens lead the priority line.
|
|
|
|
Returns:
|
|
``(errors, warnings)``.
|
|
"""
|
|
pins = selection.parse_list(display_names().decode_list(tokens))
|
|
if not pins:
|
|
return [], []
|
|
|
|
discovered = {
|
|
(row["name"], row["variant"]): row
|
|
for row in query.discover_rows(
|
|
modes, whitelist=selection.names(pins), lifecycles=lifecycles
|
|
)
|
|
}
|
|
rows = {key: query.row_modes(row, modes) for key, row in discovered.items()}
|
|
discovered_apps = {app for app, _variant in rows}
|
|
declared = get_variants()
|
|
errors: list[str] = []
|
|
warnings: list[str] = []
|
|
|
|
def states_of(app: str, variant: int | None) -> list[str]:
|
|
deployed = matrix.with_deployed_services(discovered[(app, variant)])
|
|
return network.row_states(
|
|
app, variant, declared, pulls_tor=network.row_pulls_tor(deployed)
|
|
)
|
|
|
|
def deploys(pin: selection.Pin, variant: int | None) -> bool:
|
|
return bool(
|
|
axes.priority_picks(
|
|
rows[(pin.app, variant)],
|
|
pin_mode=pin.mode,
|
|
pin_network=pin.network,
|
|
pin_vpn=pin.vpn,
|
|
states=states_of(pin.app, variant),
|
|
network_input=network_input,
|
|
vpn_mode=vpn_mode,
|
|
)
|
|
)
|
|
|
|
nothing_left = (
|
|
"leaves no (mode, network, mesh) combination to deploy in this run "
|
|
"(the role's reachability or the run's network or mesh axis rules out "
|
|
"every one), so the run would report green for a selection that never ran"
|
|
)
|
|
for pin in pins:
|
|
token = selection.describe(pin)
|
|
if pin.app not in discovered_apps:
|
|
warnings.append(
|
|
f"{label}: {token!r} matches no discovered row "
|
|
f"(no row in this run's mode and lifecycle envelope)"
|
|
)
|
|
continue
|
|
if not pin.pinned:
|
|
if priority and not any(
|
|
deploys(pin, variant) for app, variant in rows if app == pin.app
|
|
):
|
|
errors.append(f"{label}: {token!r} {nothing_left}")
|
|
continue
|
|
variants = pin.variants or (None,)
|
|
for variant in variants:
|
|
offered = rows.get((pin.app, variant))
|
|
if offered is None:
|
|
count = len(declared.get(pin.app) or [])
|
|
reason = (
|
|
f"role declares {count} variant(s)"
|
|
if pin.variants
|
|
else "no row in this run's mode and lifecycle envelope"
|
|
)
|
|
message = f"{label}: {token!r} matches no discovered row ({reason})"
|
|
(errors if pin.pinned else warnings).append(message)
|
|
continue
|
|
deployed = matrix.with_deployed_services(discovered[(pin.app, variant)])
|
|
try:
|
|
row_architectures = axes.row_architectures(
|
|
pin.app, architectures, deployed["services"]
|
|
)
|
|
axes.check_pins(
|
|
pin.app,
|
|
"" if variant is None else str(variant),
|
|
offered,
|
|
pin_mode=pin.mode,
|
|
pin_network=pin.network,
|
|
pin_distro=pin.distro,
|
|
pin_filesystem=pin.filesystem,
|
|
pin_architecture=pin.architecture,
|
|
pin_vpn=pin.vpn,
|
|
states=states_of(pin.app, variant),
|
|
network_input=network_input,
|
|
vpn_mode=vpn_mode,
|
|
distros=distros,
|
|
filesystems=filesystems,
|
|
architectures=row_architectures,
|
|
pairs=axes.row_pairs(
|
|
pin.app, distros, row_architectures, deployed["services"]
|
|
),
|
|
)
|
|
except SystemExit as refusal:
|
|
errors.append(f"{label}: {token!r} {str(refusal).split(': ', 1)[-1]}")
|
|
continue
|
|
if priority and not deploys(pin, variant):
|
|
errors.append(f"{label}: {token!r} {nothing_left}")
|
|
return errors, warnings
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Validate a run's selection inputs against this branch."
|
|
)
|
|
parser.add_argument("--whitelist", default="")
|
|
parser.add_argument("--priority", default="")
|
|
parser.add_argument("--modes", default=query.ALL_MODES)
|
|
parser.add_argument("--network", default=None)
|
|
parser.add_argument("--vpn", default=None)
|
|
parser.add_argument("--distros", default="")
|
|
parser.add_argument("--filesystem", default="")
|
|
parser.add_argument("--architectures", default="")
|
|
parser.add_argument("--lifecycles", default="")
|
|
args = parser.parse_args(argv)
|
|
|
|
modes = query.resolve_modes(args.modes)
|
|
network_input = network.resolve_network_input(args.network)
|
|
vpn_mode = vpn.resolve_vpn_mode(args.vpn)
|
|
distros = pools.resolve_distros(args.distros)
|
|
filesystems = pools.resolve_filesystems(args.filesystem)
|
|
architectures = pools.resolve_architectures(args.architectures)
|
|
|
|
errors: list[str] = []
|
|
warnings: list[str] = []
|
|
for label, tokens in (("whitelist", args.whitelist), ("priority", args.priority)):
|
|
found, warned = problems(
|
|
tokens,
|
|
modes=modes,
|
|
network_input=network_input,
|
|
vpn_mode=vpn_mode,
|
|
distros=distros,
|
|
filesystems=filesystems,
|
|
architectures=architectures,
|
|
lifecycles=args.lifecycles,
|
|
label=label,
|
|
priority=label == "priority",
|
|
)
|
|
errors += found
|
|
warnings += warned
|
|
|
|
for warning in warnings:
|
|
print(f"::warning::{warning}")
|
|
for error in errors:
|
|
print(f"::error::{error}", file=sys.stderr)
|
|
if errors:
|
|
print(
|
|
f"\n{len(errors)} unusable selection(s). Every one of them would abort a "
|
|
f"chunk's discovery or never deploy, so the run is refused here instead.",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
print("Selection inputs are valid for this branch.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|