162 lines
5.0 KiB
Python
162 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Fail closed when required CI jobs or classifier outputs are incomplete."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
from collections.abc import Mapping
|
|
from typing import Final
|
|
|
|
BOOLEAN_FLAGS: Final[tuple[str, ...]] = (
|
|
"docs_only",
|
|
"runtime_changed",
|
|
"test_changed",
|
|
"ci_changed",
|
|
"dependency_changed",
|
|
"release_changed",
|
|
"windows_full_required",
|
|
"frontend_changed",
|
|
"tui_changed",
|
|
"desktop_changed",
|
|
"python_changed",
|
|
"platform_sensitive_changed",
|
|
"build_wheel_required",
|
|
"full_required",
|
|
)
|
|
|
|
ALWAYS_REQUIRED_RESULTS: Final[tuple[tuple[str, str], ...]] = (
|
|
("RESULT_CLASSIFY", "Classify changed files"),
|
|
("RESULT_WORKFLOW_LINT", "Workflow lint"),
|
|
("RESULT_README_LOCALE", "README locale parity"),
|
|
)
|
|
|
|
|
|
def _flag_env(name: str) -> str:
|
|
return f"FLAG_{name.upper()}"
|
|
|
|
|
|
def _read_flags(env: Mapping[str, str], errors: list[str]) -> dict[str, bool]:
|
|
flags: dict[str, bool] = {}
|
|
for name in BOOLEAN_FLAGS:
|
|
raw = env.get(_flag_env(name), "")
|
|
if raw not in {"true", "false"}:
|
|
errors.append(f"Classifier output {name} must be exactly true or false; got {raw!r}.")
|
|
continue
|
|
flags[name] = raw == "true"
|
|
return flags
|
|
|
|
|
|
def _require_result(
|
|
env: Mapping[str, str],
|
|
errors: list[str],
|
|
variable: str,
|
|
label: str,
|
|
*,
|
|
required: bool,
|
|
) -> None:
|
|
result = env.get(variable, "")
|
|
allowed = {"success"} if required else {"success", "skipped"}
|
|
if result not in allowed:
|
|
expectation = "succeed" if required else "be successful or intentionally skipped"
|
|
errors.append(f"{label} must {expectation}; got {result or 'missing'}.")
|
|
|
|
|
|
def check_ci_results(env: Mapping[str, str]) -> list[str]:
|
|
"""Return gate errors; an empty list means the aggregate check may pass."""
|
|
|
|
errors: list[str] = []
|
|
flags = _read_flags(env, errors)
|
|
|
|
for variable, label in ALWAYS_REQUIRED_RESULTS:
|
|
_require_result(env, errors, variable, label, required=True)
|
|
|
|
if len(flags) != len(BOOLEAN_FLAGS):
|
|
return errors
|
|
|
|
full = flags["full_required"]
|
|
conditional_results = (
|
|
("RESULT_FRONTEND", "Frontend build and typecheck", flags["frontend_changed"] or full),
|
|
("RESULT_TUI", "OpenTUI package tests", flags["tui_changed"] or full),
|
|
("RESULT_DESKTOP", "Desktop Electron unit tests", flags["desktop_changed"] or full),
|
|
("RESULT_UBUNTU", "Ubuntu quality gate", flags["python_changed"] or full),
|
|
(
|
|
"RESULT_WINDOWS_SMOKE",
|
|
"Windows compatibility smoke tests",
|
|
flags["python_changed"]
|
|
or flags["platform_sensitive_changed"]
|
|
or flags["dependency_changed"]
|
|
or flags["release_changed"]
|
|
or full,
|
|
),
|
|
(
|
|
"RESULT_WINDOWS_FULL",
|
|
"Windows high-risk matrix",
|
|
flags["windows_full_required"] or full,
|
|
),
|
|
(
|
|
"RESULT_MACOS_RECOVERY",
|
|
"macOS profile recovery and native no-replace tests",
|
|
flags["platform_sensitive_changed"] or flags["desktop_changed"] or full,
|
|
),
|
|
(
|
|
"RESULT_DESKTOP_RECOVERY_E2E",
|
|
"Desktop recovery E2E matrix",
|
|
flags["platform_sensitive_changed"] or flags["desktop_changed"] or full,
|
|
),
|
|
(
|
|
"RESULT_RELEASE",
|
|
"Release packaging contracts",
|
|
flags["release_changed"] or full,
|
|
),
|
|
)
|
|
for variable, label, required in conditional_results:
|
|
_require_result(env, errors, variable, label, required=required)
|
|
|
|
if flags["platform_sensitive_changed"] and not flags["windows_full_required"]:
|
|
errors.append("Platform-sensitive changes must require the Windows high-risk matrix.")
|
|
|
|
if full:
|
|
if flags["docs_only"]:
|
|
errors.append("A full CI run cannot be classified as docs-only.")
|
|
for name in BOOLEAN_FLAGS:
|
|
if name in {"docs_only", "full_required"}:
|
|
continue
|
|
if not flags[name]:
|
|
errors.append(f"Full CI classification must set {name}=true.")
|
|
|
|
if flags["docs_only"]:
|
|
active = [
|
|
name
|
|
for name in BOOLEAN_FLAGS
|
|
if name != "docs_only" and flags[name]
|
|
]
|
|
if active:
|
|
errors.append(
|
|
"Docs-only classification cannot enable other flags: " + ", ".join(active)
|
|
)
|
|
|
|
return errors
|
|
|
|
|
|
def main() -> int:
|
|
errors = check_ci_results(os.environ)
|
|
for variable, label in ALWAYS_REQUIRED_RESULTS:
|
|
print(f"{label}: {os.environ.get(variable, 'missing')}")
|
|
print(
|
|
"Classifier flags: "
|
|
+ " ".join(
|
|
f"{name}={os.environ.get(_flag_env(name), 'missing')}" for name in BOOLEAN_FLAGS
|
|
)
|
|
)
|
|
if not errors:
|
|
print("All required CI results are complete and successful.")
|
|
return 0
|
|
for error in errors:
|
|
print(f"ERROR: {error}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|