mirror of https://github.com/python/cpython.git
207 lines
6.4 KiB
Python
207 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()
|