mirror of
				https://github.com/python/cpython.git
				synced 2025-11-03 23:21:29 +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()
 |