blob: 85ae342fd4cfcdf9c8cc6b39cf0a1a640e3d969f [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2022 The IREE Authors
#
# Licensed under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""Determines whether CI should run on a given PR.
The following environment variables are required:
- GITHUB_EVENT_NAME: GitHub event name, e.g. pull_request.
- GITHUB_OUTPUT: path to write workflow output variables.
When GITHUB_EVENT_NAME is "pull_request", there are additional environment
variables to be set:
- PR_TITLE (required): PR title.
- PR_BODY (optional): PR description.
- BASE_REF (required): base commit SHA of the PR.
Exit code 0 indicates that it should and exit code 2 indicates that it should
not.
"""
import fnmatch
import os
import subprocess
from typing import Iterable, Mapping, MutableMapping
PULL_REQUEST_EVENT_NAME = "pull_request"
SKIP_CI_KEY = "skip-ci"
RUNNER_ENV_KEY = "runner-env"
BENCHMARK_PRESET_KEY = "benchmarks"
# Note that these are fnmatch patterns, which are not the same as gitignore
# patterns because they don't treat '/' specially. The standard library doesn't
# contain a function for gitignore style "wildmatch". There's a third-party
# library pathspec (https://pypi.org/project/pathspec/), but it doesn't seem
# worth the dependency.
SKIP_PATH_PATTERNS = [
"docs/*",
"third_party/mkdocs-material/*",
"experimental/*",
"build_tools/buildkite/*",
# These configure the runners themselves and don't affect presubmit.
"build_tools/github_actions/runner/*",
".github/ISSUE_TEMPLATE/*",
"*.cff",
"*.clang-format",
"*.git-ignore",
"*.md",
"*.natvis",
"*.pylintrc",
"*.rst",
"*.toml",
"*.yamllint.yml",
"*.yapf",
"*CODEOWNERS",
"*AUTHORS",
"*LICENSE",
]
RUNNER_ENV_DEFAULT = "prod"
RUNNER_ENV_OPTIONS = [RUNNER_ENV_DEFAULT, "testing"]
BENCHMARK_PRESET_OPTIONS = ["all", "cuda", "x86_64", "comp-stats"]
def skip_path(path: str) -> bool:
return any(fnmatch.fnmatch(path, pattern) for pattern in SKIP_PATH_PATTERNS)
def set_output(d: Mapping[str, str]):
print(f"Setting outputs: {d}")
step_output_file = os.environ["GITHUB_OUTPUT"]
with open(step_output_file, "a") as f:
f.writelines(f"{k}={v}" "\n" for k, v in d.items())
def get_trailers() -> Mapping[str, str]:
title = os.environ["PR_TITLE"]
body = os.environ.get("PR_BODY", "")
description = f"{title}" "\n\n" f"{body}"
print("Parsing PR description:", description, sep="\n")
trailer_lines = subprocess.run(
["git", "interpret-trailers", "--parse", "--no-divider"],
input=description,
stdout=subprocess.PIPE,
check=True,
text=True,
timeout=60).stdout.splitlines()
return {
k.lower().strip(): v.strip()
for k, v in (line.split(":", maxsplit=1) for line in trailer_lines)
}
def get_modified_paths(base_ref: str) -> Iterable[str]:
return subprocess.run(["git", "diff", "--name-only", base_ref],
stdout=subprocess.PIPE,
check=True,
text=True,
timeout=60).stdout.splitlines()
def modifies_included_path(base_ref: str) -> bool:
return any(not skip_path(p) for p in get_modified_paths(base_ref))
def should_run_ci(event_name, trailers) -> bool:
if event_name != PULL_REQUEST_EVENT_NAME:
print(f"Running CI independent of diff because run was not triggered by a"
f" pull request event (event name is '{event_name}')")
return True
if SKIP_CI_KEY in trailers:
print(f"Not running CI because PR description has '{SKIP_CI_KEY}' trailer.")
return False
base_ref = os.environ["BASE_REF"]
try:
modifies = modifies_included_path(base_ref)
except TimeoutError as e:
print("Computing modified files timed out. Running the CI")
return True
if not modifies:
print("Skipping CI because all modified files are marked as excluded.")
return False
print("CI should run")
return True
def get_runner_env(trailers: Mapping[str, str]) -> str:
runner_env = trailers.get(RUNNER_ENV_KEY)
if runner_env is None:
print(f"Using '{RUNNER_ENV_DEFAULT}' runners because '{RUNNER_ENV_KEY}'"
f" not found in {trailers}")
runner_env = RUNNER_ENV_DEFAULT
else:
print(
f"Using runner environment '{runner_env}' from PR description trailers")
return runner_env
def get_ci_stage(event_name):
return 'presubmit' if event_name == PULL_REQUEST_EVENT_NAME else 'postsubmit'
def get_benchmark_presets(ci_stage: str, trailers: Mapping[str, str]) -> str:
"""Parses and validates the benchmark presets from trailers.
Args:
trailers: trailers from PR description.
Returns:
A comma separated preset string, which later will be parsed by
build_tools/benchmarks/export_benchmark_config.py.
"""
if ci_stage == "postsubmit":
preset_options = ["all"]
else:
trailer = trailers.get(BENCHMARK_PRESET_KEY)
if trailer is None:
return ""
print(f"Using benchmark preset '{trailer}' from PR description trailers")
preset_options = [option.strip() for option in trailer.split(",")]
for preset_option in preset_options:
if preset_option not in BENCHMARK_PRESET_OPTIONS:
raise ValueError(f"Unknown benchmark preset option: '{preset_option}'.\n"
f"Available options: '{BENCHMARK_PRESET_OPTIONS}'.")
if "all" in preset_options:
preset_options = list(
option for option in BENCHMARK_PRESET_OPTIONS if option != "all")
return ",".join(preset_options)
def main():
output: MutableMapping[str, str] = {}
event_name = os.environ["GITHUB_EVENT_NAME"]
trailers = get_trailers() if event_name == PULL_REQUEST_EVENT_NAME else {}
if should_run_ci(event_name, trailers):
output["should-run"] = "true"
else:
output["should-run"] = "false"
output[RUNNER_ENV_KEY] = get_runner_env(trailers)
ci_stage = get_ci_stage(event_name)
output["ci-stage"] = ci_stage
output["runner-group"] = ci_stage
write_caches = "0"
if ci_stage == "postsubmit":
write_caches = "1"
output["write-caches"] = write_caches
output["benchmark-presets"] = get_benchmark_presets(ci_stage, trailers)
set_output(output)
if __name__ == "__main__":
main()