diff --git a/tools/testing/gatekeeper/jobs.py b/tools/testing/gatekeeper/jobs.py index 4f3717f38b..3787c02f68 100644 --- a/tools/testing/gatekeeper/jobs.py +++ b/tools/testing/gatekeeper/jobs.py @@ -38,6 +38,11 @@ if os.environ.get("GITHUB_TOKEN"): _GH_HEADERS["Authorization"] = f"Bearer {os.environ['GITHUB_TOKEN']}" _GH_API_URL = f"https://api.github.com/repos/{os.environ['GITHUB_REPOSITORY']}" _GH_RUNS_URL = f"{_GH_API_URL}/actions/runs" +_GH_WEB_URL = f"https://github.com/{os.environ['GITHUB_REPOSITORY']}" +_GH_RUN_URL_RE = re.compile( + rf"{re.escape(_GH_WEB_URL)}/actions/runs/(\d+)" +) +_CI_DEVEL_WORKFLOW_PATH = ".github/workflows/ci-devel.yaml" _GH_SUMMARY_URL = ( f"{os.environ.get('GITHUB_SERVER_URL')}/" f"{os.environ.get('GITHUB_REPOSITORY')}/actions/runs/" @@ -59,6 +64,7 @@ class Checker: def __init__(self): self.latest_commit_sha = os.getenv("COMMIT_HASH") self.pr_number = os.getenv("GH_PR_NUMBER") + self.ci_devel_run = None required_labels = os.getenv("REQUIRED_LABELS") if required_labels: self.required_labels = set(required_labels.split(";")) @@ -183,6 +189,14 @@ class Checker: failing = [] running = [] + if self.ci_devel_run: + run_id = self.ci_devel_run["id"] + run_url = self.ci_devel_run.get( + "html_url", f"{_GH_WEB_URL}/actions/runs/{run_id}" + ) + lines.append("## ci-devel run\n") + lines.append(f"[Scanned ci-devel run {run_id}]({run_url})\n") + for name, job in self.results.items(): status = self._job_status(job) url = job.get("html_url", "") @@ -256,16 +270,21 @@ class Checker: return self.paginated_fetch( url, "jobs", f"get_jobs_for_workflow_run__{run_id}") - def check_workflow_runs_status(self, attempt): + def get_workflow_runs(self, attempt): + """Get workflow runs for the commit being checked""" + return self.paginated_fetch( + _GH_RUNS_URL, "workflow_runs", + f"check_workflow_runs_status_{attempt}", + {"head_sha": self.latest_commit_sha}) + + def check_workflow_runs_status(self, attempt, workflow_runs=None): """ Checks if all required jobs passed :returns: 0 - all passing; 1 - any failure; 127 some jobs running """ - workflow_runs = self.paginated_fetch( - _GH_RUNS_URL, "workflow_runs", - f"check_workflow_runs_status_{attempt}", - {"head_sha": self.latest_commit_sha}) + if workflow_runs is None: + workflow_runs = self.get_workflow_runs(attempt) for run in workflow_runs: jobs = self.get_jobs_for_workflow_run(run["id"]) for job in jobs: @@ -274,7 +293,7 @@ class Checker: self.write_step_summary() return self.status() - def wait_for_required_tests(self): + def wait_for_required_tests(self, workflow_runs=None): """ Wait for all required tests to pass or failfast @@ -283,7 +302,8 @@ class Checker: i = 0 while True: i += 1 - ret = self.check_workflow_runs_status(i) + ret = self.check_workflow_runs_status(i, workflow_runs) + workflow_runs = None if ret == RUNNING: running_jobs = len([name for name, job in self.results.items() @@ -293,7 +313,41 @@ class Checker: continue return ret - def check_required_labels(self): + def get_pull_request(self): + """Get pull request metadata from its issue endpoint""" + return self.fetch_json_from_url( + f"{_GH_API_URL}/issues/{self.pr_number}", + f"get_pull_request__{self.pr_number}") + + @staticmethod + def get_linked_run_id(body): + """Get the first same-repository Actions run linked in a PR body""" + match = _GH_RUN_URL_RE.search(body or "") + return int(match.group(1)) if match else None + + def find_ci_devel_run(self, pull_request, workflow_runs): + """Find a linked ci-devel run for the commit being checked""" + run_id = self.get_linked_run_id(pull_request.get("body")) + if run_id is None: + return None + + for run in workflow_runs: + if run["id"] != run_id: + continue + if (run.get("path") == _CI_DEVEL_WORKFLOW_PATH and + run.get("event") == "workflow_dispatch" and + run.get("head_sha") == self.latest_commit_sha): + print(f"Using linked ci-devel run {run['html_url']}", + file=sys.stderr) + return run + break + + print(f"Linked Actions run {run_id} is not a ci-devel run for " + f"{self.latest_commit_sha}; using regular workflow runs.", + file=sys.stderr) + return None + + def check_required_labels(self, pull_request=None): """ Check if all expected labels are present @@ -308,14 +362,10 @@ class Checker: f"required-labels-check ({self.required_labels})") return True - response = requests.get( - f"{_GH_API_URL}/issues/{self.pr_number}", - headers=_GH_HEADERS, - timeout=60 - ) - response.raise_for_status() - labels = set(_["name"] for _ in response.json()["labels"]) - if self.required_labels.issubset(labels): + if pull_request is None: + pull_request = self.get_pull_request() + labels = set(_["name"] for _ in pull_request["labels"]) + if self.required_labels.issubset(labels) or self.ci_devel_run: return True print(f"To run all required tests the PR{self.pr_number} must have " f"{', '.join(self.required_labels.difference(labels))} labels " @@ -330,9 +380,17 @@ class Checker: """ print(f"Gatekeeper for project={os.environ['GITHUB_REPOSITORY']} and " f"SHA={self.latest_commit_sha} PR={self.pr_number}") - if not self.check_required_labels(): + pull_request = None + workflow_runs = None + if self.pr_number: + pull_request = self.get_pull_request() + if self.get_linked_run_id(pull_request.get("body")) is not None: + workflow_runs = self.get_workflow_runs(1) + self.ci_devel_run = self.find_ci_devel_run( + pull_request, workflow_runs) + if not self.check_required_labels(pull_request): sys.exit(2) - sys.exit(self.wait_for_required_tests()) + sys.exit(self.wait_for_required_tests(workflow_runs)) if __name__ == "__main__":