diff --git a/.github/workflows/main_workflow.yml b/.github/workflows/main_workflow.yml index 1bfd950fde..92640689f9 100644 --- a/.github/workflows/main_workflow.yml +++ b/.github/workflows/main_workflow.yml @@ -11,6 +11,31 @@ on: description: "Whether to run the light CI" required: true type: boolean + # check_cpp: + # description: "Whether C++ sources changed in the PR diff" + # required: false + # default: true + # type: boolean + # run_sph_tests: + # description: "Whether to run SPH-related CI (core or shammodels/sph changes)" + # required: false + # default: true + # type: boolean + # run_ramses_tests: + # description: "Whether to run Ramses-related CI (core or shammodels/ramses changes)" + # required: false + # default: true + # type: boolean + # run_zeus_tests: + # description: "Whether to run Zeus-related CI (core or shammodels/zeus changes)" + # required: false + # default: true + # type: boolean + # run_gsph_tests: + # description: "Whether to run GSPH-related CI (core or shammodels/gsph changes)" + # required: false + # default: true + # type: boolean secrets: CODECOV_TOKEN: description: "Codecov upload token" diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 024500ed70..28070e7c5e 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -11,12 +11,37 @@ concurrency: jobs: + detect_ci_flags: + name: Detect CI flags + runs-on: ubuntu-latest + outputs: + check_cpp: ${{ steps.flags.outputs.check_cpp }} + run_sph_tests: ${{ steps.flags.outputs.run_sph_tests }} + run_ramses_tests: ${{ steps.flags.outputs.run_ramses_tests }} + run_zeus_tests: ${{ steps.flags.outputs.run_zeus_tests }} + run_gsph_tests: ${{ steps.flags.outputs.run_gsph_tests }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Diff against PR base + run: git diff ${{ github.event.pull_request.base.sha }} HEAD > pr.diff + - name: Detect flags + id: flags + run: python3 tools/detect_ci_flags.py --diff pr.diff --github-output "$GITHUB_OUTPUT" + main_workflow: name: CI + needs: [detect_ci_flags] uses: ./.github/workflows/main_workflow.yml with: run_build_push_docker: true light_ci: ${{ contains(github.event.pull_request.labels.*.name, 'light-ci') }} + # check_cpp: ${{ needs.detect_ci_flags.outputs.check_cpp == 'true' }} + # run_sph_tests: ${{ needs.detect_ci_flags.outputs.run_sph_tests == 'true' }} + # run_ramses_tests: ${{ needs.detect_ci_flags.outputs.run_ramses_tests == 'true' }} + # run_zeus_tests: ${{ needs.detect_ci_flags.outputs.run_zeus_tests == 'true' }} + # run_gsph_tests: ${{ needs.detect_ci_flags.outputs.run_gsph_tests == 'true' }} light_ci: name: Notify Light CI diff --git a/tools/detect_ci_flags.py b/tools/detect_ci_flags.py new file mode 100644 index 0000000000..c0d26eb1dd --- /dev/null +++ b/tools/detect_ci_flags.py @@ -0,0 +1,112 @@ +"""Detect CI flags from a unified diff against the PR base.""" + +import argparse +import re +import sys +from pathlib import Path + +MODELS = ("sph", "ramses", "zeus", "gsph") + +DIFF_PATH_RE = re.compile(r"^(?:---|\+\+\+)\s+(?:a/|b/)?(.+?)\s*$") + + +def parse_changed_paths(diff_text: str) -> set[str]: + paths: set[str] = set() + for line in diff_text.splitlines(): + match = DIFF_PATH_RE.match(line) + if not match: + continue + path = match.group(1) + if path == "/dev/null": + continue + paths.add(path) + return paths + + +def is_cpp_path(path: str) -> bool: + return path.endswith(".cpp") or path.endswith(".hpp") + + +def is_under_prefix(path: str, prefix: str) -> bool: + normalized = path.replace("\\", "/") + return normalized == prefix or normalized.startswith(prefix + "/") + + +def is_model_specific_path(path: str) -> bool: + normalized = path.replace("\\", "/") + for model in MODELS: + if is_under_prefix(normalized, f"src/shammodels/{model}"): + return True + if is_under_prefix(normalized, f"src/tests/shammodels/{model}"): + return True + return False + + +def is_core_component_path(path: str) -> bool: + normalized = path.replace("\\", "/") + if not is_under_prefix(normalized, "src"): + return False + return not is_model_specific_path(normalized) + + +def is_model_path(path: str, model: str) -> bool: + normalized = path.replace("\\", "/") + return is_under_prefix(normalized, f"src/shammodels/{model}") or is_under_prefix( + normalized, f"src/tests/shammodels/{model}" + ) + + +def evaluate_flags(paths: set[str]) -> dict[str, bool]: + check_cpp = any(is_cpp_path(p) for p in paths) + + flags = { + "check_cpp": check_cpp, + "run_sph_tests": any(is_core_component_path(p) or is_model_path(p, "sph") for p in paths), + "run_ramses_tests": any( + is_core_component_path(p) or is_model_path(p, "ramses") for p in paths + ), + "run_zeus_tests": any(is_core_component_path(p) or is_model_path(p, "zeus") for p in paths), + "run_gsph_tests": any(is_core_component_path(p) or is_model_path(p, "gsph") for p in paths), + } + return flags + + +def write_github_output(flags: dict[str, bool], output_path: str) -> None: + with open(output_path, "a", encoding="utf-8") as out: + out.writelines(f"{name}={str(value).lower()}\n" for name, value in flags.items()) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Detect CI flags from a PR diff.") + parser.add_argument("--diff", required=True, help="Path to unified diff file.") + parser.add_argument( + "--github-output", + help="Path to GITHUB_OUTPUT file for workflow job outputs.", + ) + args = parser.parse_args() + + diff_path = Path(args.diff) + if not diff_path.exists(): + print(f"error: diff file not found: {args.diff}", file=sys.stderr) + return 1 + + diff_text = diff_path.read_text(encoding="utf-8", errors="replace") + paths = parse_changed_paths(diff_text) + flags = evaluate_flags(paths) + + print(f"Changed paths ({len(paths)}):") + for path in sorted(paths): + print(f" {path}") + + print("CI flags:") + for name, value in flags.items(): + print(f" {name}={value}") + + if args.github_output: + write_github_output(flags, args.github_output) + + return 0 + + +if __name__ == "__main__": + sys.exit(main())