mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 21:51:50 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			206 lines
		
	
	
	
		
			6.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			206 lines
		
	
	
	
		
			6.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """Determine which GitHub Actions workflows to run.
 | |
| 
 | |
| Called by ``.github/workflows/reusable-context.yml``.
 | |
| We only want to run tests on PRs when related files are changed,
 | |
| or when someone triggers a manual workflow run.
 | |
| This improves developer experience by not doing (slow)
 | |
| unnecessary work in GHA, and saves CI resources.
 | |
| """
 | |
| 
 | |
| from __future__ import annotations
 | |
| 
 | |
| import os
 | |
| import subprocess
 | |
| from dataclasses import dataclass
 | |
| from pathlib import Path
 | |
| 
 | |
| TYPE_CHECKING = False
 | |
| if TYPE_CHECKING:
 | |
|     from collections.abc import Set
 | |
| 
 | |
| GITHUB_DEFAULT_BRANCH = os.environ["GITHUB_DEFAULT_BRANCH"]
 | |
| GITHUB_CODEOWNERS_PATH = Path(".github/CODEOWNERS")
 | |
| GITHUB_WORKFLOWS_PATH = Path(".github/workflows")
 | |
| 
 | |
| CONFIGURATION_FILE_NAMES = frozenset({
 | |
|     ".pre-commit-config.yaml",
 | |
|     ".ruff.toml",
 | |
|     "mypy.ini",
 | |
| })
 | |
| UNIX_BUILD_SYSTEM_FILE_NAMES = frozenset({
 | |
|     Path("aclocal.m4"),
 | |
|     Path("config.guess"),
 | |
|     Path("config.sub"),
 | |
|     Path("configure"),
 | |
|     Path("configure.ac"),
 | |
|     Path("install-sh"),
 | |
|     Path("Makefile.pre.in"),
 | |
|     Path("Modules/makesetup"),
 | |
|     Path("Modules/Setup"),
 | |
|     Path("Modules/Setup.bootstrap.in"),
 | |
|     Path("Modules/Setup.stdlib.in"),
 | |
|     Path("Tools/build/regen-configure.sh"),
 | |
| })
 | |
| 
 | |
| SUFFIXES_C_OR_CPP = frozenset({".c", ".h", ".cpp"})
 | |
| SUFFIXES_DOCUMENTATION = frozenset({".rst", ".md"})
 | |
| 
 | |
| 
 | |
| @dataclass(kw_only=True, slots=True)
 | |
| class Outputs:
 | |
|     run_ci_fuzz: bool = False
 | |
|     run_docs: bool = False
 | |
|     run_tests: bool = False
 | |
|     run_windows_msi: bool = False
 | |
|     run_windows_tests: bool = False
 | |
| 
 | |
| 
 | |
| def compute_changes() -> None:
 | |
|     target_branch, head_ref = git_refs()
 | |
|     if os.environ.get("GITHUB_EVENT_NAME", "") == "pull_request":
 | |
|         # Getting changed files only makes sense on a pull request
 | |
|         files = get_changed_files(target_branch, head_ref)
 | |
|         outputs = process_changed_files(files)
 | |
|     else:
 | |
|         # Otherwise, just run the tests
 | |
|         outputs = Outputs(run_tests=True, run_windows_tests=True)
 | |
|     outputs = process_target_branch(outputs, target_branch)
 | |
| 
 | |
|     if outputs.run_tests:
 | |
|         print("Run tests")
 | |
|     if outputs.run_windows_tests:
 | |
|         print("Run Windows tests")
 | |
| 
 | |
|     if outputs.run_ci_fuzz:
 | |
|         print("Run CIFuzz tests")
 | |
|     else:
 | |
|         print("Branch too old for CIFuzz tests; or no C files were changed")
 | |
| 
 | |
|     if outputs.run_docs:
 | |
|         print("Build documentation")
 | |
| 
 | |
|     if outputs.run_windows_msi:
 | |
|         print("Build Windows MSI")
 | |
| 
 | |
|     print(outputs)
 | |
| 
 | |
|     write_github_output(outputs)
 | |
| 
 | |
| 
 | |
| def git_refs() -> tuple[str, str]:
 | |
|     target_ref = os.environ.get("CCF_TARGET_REF", "")
 | |
|     target_ref = target_ref.removeprefix("refs/heads/")
 | |
|     print(f"target ref: {target_ref!r}")
 | |
| 
 | |
|     head_ref = os.environ.get("CCF_HEAD_REF", "")
 | |
|     head_ref = head_ref.removeprefix("refs/heads/")
 | |
|     print(f"head ref: {head_ref!r}")
 | |
|     return f"origin/{target_ref}", head_ref
 | |
| 
 | |
| 
 | |
| def get_changed_files(
 | |
|     ref_a: str = GITHUB_DEFAULT_BRANCH, ref_b: str = "HEAD"
 | |
| ) -> Set[Path]:
 | |
|     """List the files changed between two Git refs, filtered by change type."""
 | |
|     args = ("git", "diff", "--name-only", f"{ref_a}...{ref_b}", "--")
 | |
|     print(*args)
 | |
|     changed_files_result = subprocess.run(
 | |
|         args, stdout=subprocess.PIPE, check=True, encoding="utf-8"
 | |
|     )
 | |
|     changed_files = changed_files_result.stdout.strip().splitlines()
 | |
|     return frozenset(map(Path, filter(None, map(str.strip, changed_files))))
 | |
| 
 | |
| 
 | |
| def process_changed_files(changed_files: Set[Path]) -> Outputs:
 | |
|     run_tests = False
 | |
|     run_ci_fuzz = False
 | |
|     run_docs = False
 | |
|     run_windows_tests = False
 | |
|     run_windows_msi = False
 | |
| 
 | |
|     for file in changed_files:
 | |
|         # Documentation files
 | |
|         doc_or_misc = file.parts[0] in {"Doc", "Misc"}
 | |
|         doc_file = file.suffix in SUFFIXES_DOCUMENTATION or doc_or_misc
 | |
| 
 | |
|         if file.parent == GITHUB_WORKFLOWS_PATH:
 | |
|             if file.name == "build.yml":
 | |
|                 run_tests = run_ci_fuzz = True
 | |
|             if file.name == "reusable-docs.yml":
 | |
|                 run_docs = True
 | |
|             if file.name == "reusable-windows-msi.yml":
 | |
|                 run_windows_msi = True
 | |
| 
 | |
|         if not (
 | |
|             doc_file
 | |
|             or file == GITHUB_CODEOWNERS_PATH
 | |
|             or file.name in CONFIGURATION_FILE_NAMES
 | |
|         ):
 | |
|             run_tests = True
 | |
| 
 | |
|             if file not in UNIX_BUILD_SYSTEM_FILE_NAMES:
 | |
|                 run_windows_tests = True
 | |
| 
 | |
|         # The fuzz tests are pretty slow so they are executed only for PRs
 | |
|         # changing relevant files.
 | |
|         if file.suffix in SUFFIXES_C_OR_CPP:
 | |
|             run_ci_fuzz = True
 | |
|         if file.parts[:2] in {
 | |
|             ("configure",),
 | |
|             ("Modules", "_xxtestfuzz"),
 | |
|         }:
 | |
|             run_ci_fuzz = True
 | |
| 
 | |
|         # Check for changed documentation-related files
 | |
|         if doc_file:
 | |
|             run_docs = True
 | |
| 
 | |
|         # Check for changed MSI installer-related files
 | |
|         if file.parts[:2] == ("Tools", "msi"):
 | |
|             run_windows_msi = True
 | |
| 
 | |
|     return Outputs(
 | |
|         run_ci_fuzz=run_ci_fuzz,
 | |
|         run_docs=run_docs,
 | |
|         run_tests=run_tests,
 | |
|         run_windows_tests=run_windows_tests,
 | |
|         run_windows_msi=run_windows_msi,
 | |
|     )
 | |
| 
 | |
| 
 | |
| def process_target_branch(outputs: Outputs, git_branch: str) -> Outputs:
 | |
|     if not git_branch:
 | |
|         outputs.run_tests = True
 | |
| 
 | |
|     # CIFuzz / OSS-Fuzz compatibility with older branches may be broken.
 | |
|     if git_branch != GITHUB_DEFAULT_BRANCH:
 | |
|         outputs.run_ci_fuzz = False
 | |
| 
 | |
|     if os.environ.get("GITHUB_EVENT_NAME", "").lower() == "workflow_dispatch":
 | |
|         outputs.run_docs = True
 | |
|         outputs.run_windows_msi = True
 | |
| 
 | |
|     return outputs
 | |
| 
 | |
| 
 | |
| def write_github_output(outputs: Outputs) -> None:
 | |
|     # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables
 | |
|     # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-an-output-parameter
 | |
|     if "GITHUB_OUTPUT" not in os.environ:
 | |
|         print("GITHUB_OUTPUT not defined!")
 | |
|         return
 | |
| 
 | |
|     with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
 | |
|         f.write(f"run-ci-fuzz={bool_lower(outputs.run_ci_fuzz)}\n")
 | |
|         f.write(f"run-docs={bool_lower(outputs.run_docs)}\n")
 | |
|         f.write(f"run-tests={bool_lower(outputs.run_tests)}\n")
 | |
|         f.write(f"run-windows-tests={bool_lower(outputs.run_windows_tests)}\n")
 | |
|         f.write(f"run-windows-msi={bool_lower(outputs.run_windows_msi)}\n")
 | |
| 
 | |
| 
 | |
| def bool_lower(value: bool, /) -> str:
 | |
|     return "true" if value else "false"
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     compute_changes()
 | 
