From d3e02c8c5e6a854c9cc45b2b8d5756c5f70aebfd Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:23:27 +0200 Subject: [PATCH] fix: redeploy the site on any script it imports --- .github/workflows/deploy-site.yml | 45 ++++++++++++++-- tests/test_workflow_paths.py | 89 +++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 tests/test_workflow_paths.py diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index f2b792d8..d0e758d4 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -8,11 +8,47 @@ on: - "emulators/**" - "provenance/**" - "wiki/**" - - "scripts/generate_site.py" - - "scripts/generate_readme.py" - - "scripts/validate_site.py" - - "scripts/verify.py" + # Every script the build runs or imports (tests/test_workflow_paths.py). + - "scripts/artifacts.py" + - "scripts/auto_fetch.py" - "scripts/common.py" + - "scripts/cross_reference.py" + - "scripts/crypto_verify.py" + - "scripts/deterministic_zip.py" + - "scripts/dumpcatalog.py" + - "scripts/generate_pack.py" + - "scripts/generate_readme.py" + - "scripts/generate_site.py" + - "scripts/hashing.py" + - "scripts/largefiles.py" + - "scripts/list_platforms.py" + - "scripts/nativemode.py" + - "scripts/packextras.py" + - "scripts/packpaths.py" + - "scripts/packreadme.py" + - "scripts/packresolve.py" + - "scripts/packverify.py" + - "scripts/profile_sync.py" + - "scripts/provenance_report.py" + - "scripts/refresh_data_dirs.py" + - "scripts/region.py" + - "scripts/release_record.py" + - "scripts/restore_large_files.py" + - "scripts/safeparse.py" + - "scripts/sect233r1.py" + - "scripts/siterender.py" + - "scripts/sitesources.py" + - "scripts/sitewrite.py" + - "scripts/slot.py" + - "scripts/slots.py" + - "scripts/split_pack.py" + - "scripts/truth.py" + - "scripts/upstream.py" + - "scripts/validate_schemas.py" + - "scripts/validate_site.py" + - "scripts/validation.py" + - "scripts/verify.py" + - "scripts/ziptools.py" - "schemas/**" - "tests/**" - "docs_assets/**" @@ -20,7 +56,6 @@ on: - "install.py" - "database.json" - "release.json" - - "scripts/release_record.py" - "mkdocs.yml" - ".github/workflows/deploy-site.yml" workflow_dispatch: diff --git a/tests/test_workflow_paths.py b/tests/test_workflow_paths.py new file mode 100644 index 00000000..a918b695 --- /dev/null +++ b/tests/test_workflow_paths.py @@ -0,0 +1,89 @@ +"""The site redeploys whenever a script it runs changes. + +Deploy Site listed five scripts by hand while the build imports forty: +a change to siterender.py, which writes every page, published nothing +until some unrelated file moved. +""" + +from __future__ import annotations + +import ast +import fnmatch +import unittest +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parent.parent +SCRIPTS = REPO_ROOT / "scripts" + + +def _module_file(name: str) -> Path | None: + name = name.removeprefix("scripts.") + for candidate in ( + SCRIPTS / (name.replace(".", "/") + ".py"), + SCRIPTS / name.replace(".", "/") / "__init__.py", + ): + if candidate.exists(): + return candidate + return None + + +def _imports(path: Path) -> set[Path]: + found: set[Path] = set() + for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))): + if isinstance(node, ast.Import): + names = [alias.name for alias in node.names] + elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module: + names = [node.module, *(f"{node.module}.{a.name}" for a in node.names)] + elif isinstance(node, ast.ImportFrom): + base = path.parent / (node.module or "").replace(".", "/") + candidates = [base / f"{a.name}.py" for a in node.names] + candidates += [base.with_suffix(".py"), base / "__init__.py"] + found.update(c for c in candidates if c.is_file()) + continue + else: + continue + found.update(f for f in map(_module_file, names) if f) + return found + + +def closure(entry_points: list[str]) -> set[str]: + seen: set[Path] = set() + stack = [SCRIPTS / name for name in entry_points] + while stack: + path = stack.pop() + if path not in seen: + seen.add(path) + stack.extend(_imports(path)) + return {str(p.relative_to(REPO_ROOT)) for p in seen} + + +class DeploySiteTriggers(unittest.TestCase): + def test_every_script_the_build_imports_triggers_it(self): + workflow = yaml.safe_load( + (REPO_ROOT / ".github/workflows/deploy-site.yml").read_text(encoding="utf-8") + ) + on = workflow.get("on", workflow.get(True)) + patterns = on["push"]["paths"] + run = "\n".join( + step.get("run", "") for job in workflow["jobs"].values() for step in job["steps"] + ) + entry_points = sorted( + { + word.removeprefix("scripts/") + for word in run.split() + if word.startswith("scripts/") and word.endswith(".py") + } + ) + self.assertIn("generate_site.py", entry_points) + uncovered = sorted( + path + for path in closure(entry_points) + if not any(fnmatch.fnmatch(path, p.replace("**", "*")) for p in patterns) + ) + self.assertEqual(uncovered, []) + + +if __name__ == "__main__": + unittest.main()