#!/usr/bin/env python3 """Run the checks validate.yml runs on a pull request, in its order. Usage: python scripts/pr_checks.py python scripts/pr_checks.py --list Each step runs even when an earlier one failed, as CI runs them, and the summary names every step that failed or could not run. A step whose tool is absent (npm, python3.8, vermin) is reported as not run, with what provides it. """ from __future__ import annotations import argparse import os import shutil import subprocess import sys from dataclasses import dataclass, field from pathlib import Path ROOT = Path(__file__).resolve().parent.parent @dataclass(frozen=True) class Step: command: str why: str needs: str = "" provider: str = "" env: dict[str, str] = field(default_factory=dict) # The order of validate.yml: data fetched, site generated and built, then the # suite, its 3.8 run and the built-site check. STEPS: tuple[Step, ...] = ( Step("python scripts/validate_pr.py --changed", "the BIOS files the branch adds"), Step("python scripts/validate_schemas.py --source-only", "profiles and platform files"), Step("python scripts/profile_sync.py --added --strict", "each profile the branch adds"), Step("vermin --no-tips -t=3.8- --violations install.py", "install.py stays on Python 3.8", needs="vermin", provider="the pip line of CONTRIBUTING.md"), Step("python scripts/export_native.py --all --fetch --output-dir tmp/native", "files the exporter tests compare with"), Step("python scripts/refresh_data_dirs.py", "data directories the pack-building tests read"), Step("python scripts/generate_site.py", "the pages the site tests read"), Step("npm ci --prefix website", "the site's packages, Node 24", needs="npm", provider="Node 24"), Step("npm run build --prefix website", "the built site", needs="npm", provider="Node 24"), Step("python -m unittest discover -s tests -t .", "the suite", env={"RETROBIOS_OFFLINE": "1"}), Step("python3.8 -m unittest tests.test_install_tui", "the full screen on the installer's floor", needs="python3.8", provider="a Python 3.8 with pyte", env={"RETROBIOS_OFFLINE": "1"}), Step("python scripts/validate_site.py --site-dir website/dist", "links and page budgets"), ) def argv(step: Step) -> list[str]: """The command, with `python` run by the interpreter running this script.""" words = step.command.split() return [sys.executable, *words[1:]] if words[0] == "python" else words def run(steps: tuple[Step, ...] = STEPS) -> int: failed: list[str] = [] not_run: list[str] = [] for step in steps: print(f"\n== {step.command}", flush=True) if step.needs and shutil.which(step.needs) is None: print(f"not run: {step.needs} is not installed ({step.provider})") not_run.append(f"{step.command} ({step.needs}: {step.provider})") continue code = subprocess.run(argv(step), cwd=ROOT, env={**os.environ, **step.env}).returncode if code: failed.append(f"{step.command} (exit {code})") print() for line in failed: print(f"FAILED {line}") for line in not_run: print(f"NOT RUN {line}") print(f"{len(steps) - len(failed) - len(not_run)}/{len(steps)} checks passed") return 1 if failed else 0 def main() -> None: parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.splitlines()[0]) parser.add_argument("--list", action="store_true", help="print the steps and exit") args = parser.parse_args() if args.list: for step in STEPS: print(step.command) return sys.exit(run()) if __name__ == "__main__": main()