[CI] Fix CI authorization notification fallback (#51095)
Signed-off-by: khluu <khluu000@gmail.com> Co-authored-by: OpenAI Codex <noreply@openai.com>
Kevin H. Luu committed
Aug 5, 2026 at 01:34 UTC
a3b86752fbcd2fb1a9adfe44634ac0d955228e2a
5 files changed
+97
-13
.github/workflows/add_label_automerge.yml
+1
-1
@@ -7,7 +7,7 @@ on:
7
- auto_merge_enabled
8
jobs:
9
add-label-on-auto-merge:
10
- runs-on: ubuntu-latest
10
+ runs-on: [self-hosted, linux, x64, vllm-runners]
11
steps:
12
- name: Add label
13
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
.github/workflows/notify-ci-authorized.yml
+4
-4
@@ -11,7 +11,8 @@ concurrency:
11
group: >-
12
notify-ci-authorized-${{
13
github.event.pull_request.number ||
14
- github.event.workflow_run.pull_requests[0].number
14
+ github.event.workflow_run.pull_requests[0].number ||
15
+ github.event.workflow_run.id
16
}}
17
cancel-in-progress: false
18
@@ -29,9 +30,8 @@ jobs:
30
github.event.label.name == 'ready-run-all-tests')) ||
31
(github.event_name == 'workflow_run' &&
32
github.event.workflow_run.event == 'pull_request_review' &&
32
- github.event.workflow_run.conclusion == 'success' &&
33
- github.event.workflow_run.pull_requests[0])
34
- runs-on: ubuntu-latest
33
+ github.event.workflow_run.conclusion == 'success')
34
+ runs-on: [self-hosted, linux, x64, vllm-runners]
35
steps:
36
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
37
with:
.github/workflows/record-ci-approval.yml
+1
-1
@@ -9,6 +9,6 @@ permissions: {}
9
jobs:
10
approved:
11
if: github.event.review.state == 'approved'
12
- runs-on: ubuntu-latest
12
+ runs-on: [self-hosted, linux, x64, vllm-runners]
13
steps:
14
- run: echo "Approval recorded"
.github/workflows/scripts/run_ci_command.py
+44
-4
@@ -203,6 +203,13 @@ class GitHubClient:
203
def get_pr(self, number: int) -> dict[str, Any]:
204
return self._request(self._repo_path(f"/pulls/{number}"))
205
206
+ def list_pulls_for_commit(self, commit: str) -> list[dict[str, Any]]:
207
+ commit = urllib.parse.quote(commit, safe="")
208
+ response = self._request(self._repo_path(f"/commits/{commit}/pulls"))
209
+ if not isinstance(response, list):
210
+ raise ApiError(None, "GitHub API returned an invalid pull request list.")
211
+ return response
212
+
213
def get_permission(self, actor: str) -> str:
214
username = urllib.parse.quote(actor, safe="")
215
try:
@@ -622,7 +629,6 @@ def notify_authorized(
629
if (
630
event.get("action") != "labeled"
631
or event["label"]["name"] not in READY_LABELS
625
- or has_trusted_approval(github, pr["number"], trusted_users)
632
):
633
return
634
elif "review" in event:
@@ -650,6 +656,40 @@ def notify_authorized(
656
)
657
658
659
+def resolve_workflow_run_pr(
660
+ workflow_run: Mapping[str, Any],
661
+ github: GitHubClient,
662
+) -> dict[str, Any] | None:
663
+ head_sha = str(workflow_run.get("head_sha", ""))
664
+ if not head_sha:
665
+ return None
666
+
667
+ seen: set[int] = set()
668
+
669
+ def find_matching_pr(
670
+ candidates: Sequence[Mapping[str, Any]],
671
+ ) -> dict[str, Any] | None:
672
+ for candidate in candidates:
673
+ candidate_number = candidate.get("number")
674
+ if not isinstance(candidate_number, int) or candidate_number in seen:
675
+ continue
676
+ seen.add(candidate_number)
677
+ try:
678
+ pr = github.get_pr(candidate_number)
679
+ except ApiError as error:
680
+ if error.status == 404:
681
+ continue
682
+ raise
683
+ if pr["state"] == "open" and pr["head"]["sha"] == head_sha:
684
+ return pr
685
+ return None
686
+
687
+ associated_pr = find_matching_pr(workflow_run.get("pull_requests") or [])
688
+ if associated_pr is not None:
689
+ return associated_pr
690
+ return find_matching_pr(github.list_pulls_for_commit(head_sha))
691
+
692
+
693
def handle_run_ci(
694
*,
695
actor: str,
@@ -897,10 +937,10 @@ def main() -> None:
937
)
938
return
939
if event_name == "workflow_run":
900
- pull_requests = event["workflow_run"].get("pull_requests") or []
901
- if not pull_requests:
940
+ pr = resolve_workflow_run_pr(event["workflow_run"], github)
941
+ if pr is None:
942
+ print("Could not resolve an open PR for the approval workflow run.")
943
return
903
- pr = github.get_pr(int(pull_requests[0]["number"]))
944
notify_authorized(
945
{
946
"action": "submitted",
.github/workflows/scripts/test_run_ci_command.py
+47
-3
@@ -15,6 +15,7 @@ from run_ci_command import (
15
COMMAND_RUN_CI_ALL,
16
COMMAND_RUN_CI_NIGHTLY,
17
RETRY_STATES,
18
+ ApiError,
19
BuildkiteClient,
20
HttpTransport,
21
authorize,
@@ -25,6 +26,7 @@ from run_ci_command import (
26
notify_authorized,
27
parse_command,
28
parse_trusted_users,
29
+ resolve_workflow_run_pr,
30
run,
31
select_latest_build,
32
)
@@ -72,6 +74,8 @@ class FakeGitHub:
74
review_decision: str = "REVIEW_REQUIRED",
75
reviews: list[dict[str, Any]] | None = None,
76
comments: list[str] | None = None,
77
+ pulls_for_commit: list[dict[str, Any]] | None = None,
78
+ prs: dict[int, dict[str, Any]] | None = None,
79
) -> None:
80
self.comments = comments or []
81
self.permission = permission
@@ -80,13 +84,21 @@ class FakeGitHub:
84
self.reactions: list[str] = []
85
self.review_decision = review_decision
86
self.reviews = reviews or []
87
+ self.pulls_for_commit = pulls_for_commit or []
88
+ self.prs = prs or {self.pr["number"]: self.pr}
89
90
def get_pr(self, number: int) -> dict[str, Any]:
85
- return self.pr
91
+ try:
92
+ return self.prs[number]
93
+ except KeyError as error:
94
+ raise ApiError(404, "Not found") from error
95
96
def get_permission(self, actor: str) -> str:
97
return self.permissions.get(actor, self.permission)
98
99
+ def list_pulls_for_commit(self, commit: str) -> list[dict[str, Any]]:
100
+ return self.pulls_for_commit
101
+
102
def get_review_decision(self, number: int) -> str:
103
return self.review_decision
104
@@ -525,7 +537,7 @@ class RunCiCommandTest(unittest.TestCase):
537
self.assertNotIn(COMMAND_RUN_CI_NIGHTLY, comment)
538
self.assertIn(CI_AUTHORIZED_COMMENT_MARKER, comment)
539
528
- def test_ready_label_does_not_notify_after_trusted_approval(self) -> None:
540
+ def test_ready_label_notifies_after_missed_approval_notification(self) -> None:
541
pr = make_pr(labels=[{"name": "ready"}])
542
event = {
543
"action": "labeled",
@@ -547,7 +559,8 @@ class RunCiCommandTest(unittest.TestCase):
559
560
notify_authorized(event, github)
561
550
- self.assertEqual(github.comments, [])
562
+ self.assertEqual(len(github.comments), 1)
563
+ self.assertIn("@author", github.comments[0])
564
565
def test_trusted_approval_notifies_author_once(self) -> None:
566
event = {
@@ -573,6 +586,37 @@ class RunCiCommandTest(unittest.TestCase):
586
self.assertEqual(len(github.comments), 1)
587
self.assertIn("@author", github.comments[0])
588
589
+ def test_workflow_run_resolves_pr_from_head_commit(self) -> None:
590
+ pr = make_pr()
591
+ github = FakeGitHub(
592
+ pr=pr,
593
+ pulls_for_commit=[{"number": pr["number"]}],
594
+ )
595
+
596
+ resolved = resolve_workflow_run_pr(
597
+ {"head_sha": pr["head"]["sha"], "pull_requests": []},
598
+ github,
599
+ )
600
+
601
+ self.assertEqual(resolved, pr)
602
+
603
+ def test_workflow_run_ignores_unrelated_pr_association(self) -> None:
604
+ pr = make_pr()
605
+ github = FakeGitHub(
606
+ pr=pr,
607
+ pulls_for_commit=[{"number": pr["number"]}],
608
+ )
609
+
610
+ resolved = resolve_workflow_run_pr(
611
+ {
612
+ "head_sha": pr["head"]["sha"],
613
+ "pull_requests": [{"number": 1}],
614
+ },
615
+ github,
616
+ )
617
+
618
+ self.assertEqual(resolved, pr)
619
+
620
def test_approval_does_not_notify_when_ready_label_exists(self) -> None:
621
pr = make_pr(labels=[{"name": "ready"}])
622
event = {