From 46ae792ff26f7cdefa8f81c7ded512ba17cc67e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bombo?= Date: Fri, 24 Jul 2026 15:31:54 -0500 Subject: [PATCH] ci.gatekeeper: Recognize linked ci-devel runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gatekeeper requires ok-to-test before checking required jobs, even when a maintainer has dispatched ci-devel for the pull request tip. Recognize the first linked ci-devel run only when its workflow, event, and commit match. Use that run as alternate authorization and link it from the gatekeeper step summary. Invalid links retain the regular path. Generated-By: GitHub Copilot Signed-off-by: Aurélien Bombo --- tools/testing/gatekeeper/jobs.py | 94 ++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 18 deletions(-) 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__":