Skip to content

Commit f5da5ea

Browse files
authored
Update oncall reviewer assignment (NVIDIA#5093)
1 parent 907ebcd commit f5da5ea

2 files changed

Lines changed: 159 additions & 3 deletions

File tree

.github/scripts/oncall_manager.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
ROTATION_TEAM_SLUG = "mcore-oncall-rotation"
2929
ACTIVE_ONCALL_TEAM_SLUG = "mcore-oncall"
3030
SLACK_USERGROUP_HANDLE = "mcore-oncall"
31+
COMMUNITY_REQUEST_LABEL = "community-request"
3132
TARGET_WEEKS = 12
3233

3334
# Caches for email and Slack lookups
@@ -391,14 +392,44 @@ def ensure_schedule_filled(schedule, repo_owner):
391392
print(f"Appended: {new_entry}")
392393

393394
def assign_reviewer(pr_number):
394-
"""Assigns the mcore-oncall team as the reviewer for the PR."""
395+
"""Assigns mcore-oncall if no reviewers are set or community-request is applied."""
395396
owner, repo = get_repo_info()
397+
398+
pr_url = f"{GITHUB_API_URL}/repos/{owner}/{repo}/pulls/{pr_number}"
399+
pr_resp = requests.get(pr_url, headers=get_headers())
400+
401+
if pr_resp.status_code != 200:
402+
print(f"Failed to fetch PR: {pr_resp.status_code} {pr_resp.text}")
403+
sys.exit(1)
404+
405+
pr_data = pr_resp.json()
406+
requested_reviewers = pr_data.get("requested_reviewers", [])
407+
requested_teams = pr_data.get("requested_teams", [])
408+
labels = {label.get("name") for label in pr_data.get("labels", [])}
409+
requested_team_slugs = {team.get("slug") for team in requested_teams}
410+
has_community_request_label = COMMUNITY_REQUEST_LABEL in labels
411+
412+
if ACTIVE_ONCALL_TEAM_SLUG in requested_team_slugs:
413+
print(
414+
f"Skipping reviewer request: team NVIDIA/{ACTIVE_ONCALL_TEAM_SLUG} "
415+
"is already requested"
416+
)
417+
return
418+
419+
if not has_community_request_label and (requested_reviewers or requested_teams):
420+
print(
421+
"Skipping reviewer request: PR already has "
422+
f"{len(requested_reviewers)} user reviewer(s) and "
423+
f"{len(requested_teams)} team reviewer(s)"
424+
)
425+
return
426+
396427
url = f"{GITHUB_API_URL}/repos/{owner}/{repo}/pulls/{pr_number}/requested_reviewers"
397-
428+
398429
# Assign the oncall team as reviewer
399430
data = {"team_reviewers": [ACTIVE_ONCALL_TEAM_SLUG]}
400431
resp = requests.post(url, headers=get_headers(), json=data)
401-
432+
402433
if resp.status_code in [201, 200]:
403434
print(f"Successfully requested review from team NVIDIA/{ACTIVE_ONCALL_TEAM_SLUG}")
404435
else:
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import importlib.util
16+
import sys
17+
import types
18+
from pathlib import Path
19+
20+
import pytest
21+
22+
REPO_ROOT = Path(__file__).resolve().parents[3]
23+
ONCALL_MANAGER_PATH = REPO_ROOT / ".github" / "scripts" / "oncall_manager.py"
24+
25+
26+
class FakeResponse:
27+
def __init__(self, status_code, json_data=None, text=""):
28+
self.status_code = status_code
29+
self._json_data = json_data
30+
self.text = text
31+
32+
def json(self):
33+
return self._json_data
34+
35+
36+
class FakeRequests:
37+
def __init__(self, pr_data):
38+
self.pr_data = pr_data
39+
self.posts = []
40+
41+
def get(self, url, headers=None):
42+
return FakeResponse(200, self.pr_data)
43+
44+
def post(self, url, headers=None, json=None):
45+
self.posts.append({"url": url, "headers": headers, "json": json})
46+
return FakeResponse(201)
47+
48+
49+
@pytest.fixture
50+
def oncall_manager(monkeypatch):
51+
slack_module = types.ModuleType("slack_sdk")
52+
slack_module.WebClient = object
53+
54+
slack_errors_module = types.ModuleType("slack_sdk.errors")
55+
slack_errors_module.SlackApiError = Exception
56+
requests_module = types.ModuleType("requests")
57+
58+
monkeypatch.setitem(sys.modules, "requests", requests_module)
59+
monkeypatch.setitem(sys.modules, "slack_sdk", slack_module)
60+
monkeypatch.setitem(sys.modules, "slack_sdk.errors", slack_errors_module)
61+
monkeypatch.setenv("GITHUB_REPOSITORY", "NVIDIA/Megatron-LM")
62+
monkeypatch.setenv("GH_TOKEN", "token")
63+
64+
spec = importlib.util.spec_from_file_location("oncall_manager", ONCALL_MANAGER_PATH)
65+
module = importlib.util.module_from_spec(spec)
66+
spec.loader.exec_module(module)
67+
return module
68+
69+
70+
@pytest.mark.parametrize(
71+
"pr_data",
72+
[
73+
{
74+
"user": {"login": "maintainer"},
75+
"requested_reviewers": [{"login": "alice"}],
76+
"requested_teams": [],
77+
},
78+
{
79+
"user": {"login": "maintainer"},
80+
"requested_reviewers": [],
81+
"requested_teams": [{"slug": "mcore"}],
82+
},
83+
{
84+
"requested_reviewers": [{"login": "alice"}],
85+
"requested_teams": [{"slug": "mcore-oncall"}],
86+
"labels": [{"name": "community-request"}],
87+
},
88+
],
89+
)
90+
def test_assign_reviewer_skips_when_oncall_is_not_needed(
91+
oncall_manager, monkeypatch, capsys, pr_data
92+
):
93+
fake_requests = FakeRequests(pr_data)
94+
monkeypatch.setattr(oncall_manager, "requests", fake_requests)
95+
96+
oncall_manager.assign_reviewer(123)
97+
98+
assert fake_requests.posts == []
99+
assert "Skipping reviewer request" in capsys.readouterr().out
100+
101+
102+
@pytest.mark.parametrize(
103+
"pr_data",
104+
[
105+
{"user": {"login": "maintainer"}, "requested_reviewers": [], "requested_teams": []},
106+
{
107+
"requested_reviewers": [{"login": "alice"}],
108+
"requested_teams": [{"slug": "mcore"}],
109+
"labels": [{"name": "community-request"}],
110+
},
111+
],
112+
)
113+
def test_assign_reviewer_requests_oncall_when_needed(oncall_manager, monkeypatch, pr_data):
114+
fake_requests = FakeRequests(pr_data)
115+
monkeypatch.setattr(oncall_manager, "requests", fake_requests)
116+
117+
oncall_manager.assign_reviewer(123)
118+
119+
assert fake_requests.posts == [
120+
{
121+
"url": "https://api.github.com/repos/NVIDIA/Megatron-LM/pulls/123/requested_reviewers",
122+
"headers": {"Authorization": "token token", "Accept": "application/vnd.github.v3+json"},
123+
"json": {"team_reviewers": ["mcore-oncall"]},
124+
}
125+
]

0 commit comments

Comments
 (0)