Skip to content

Commit 82742c3

Browse files
authored
[ISV-4528] Automatically merge approved PRs (#746)
* [ISV-4528] Automatically merge approved PRs --------- Signed-off-by: Maurizio Porrato <[email protected]>
1 parent ca83aa3 commit 82742c3

File tree

2 files changed

+135
-29
lines changed

2 files changed

+135
-29
lines changed

operator-pipeline-images/operatorcert/entrypoints/check_permissions.py

+49-13
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,34 @@ def github_repo_org(self) -> str:
167167
"""
168168
return urllib.parse.urlparse(self.pull_request_url).path.split("/")[1]
169169

170+
@property
171+
def github_repo_name(self) -> str:
172+
"""
173+
The name of the github repo the pull request belongs to
174+
175+
Returns:
176+
str: the github repo name in the form "organization_name/repo_name"
177+
"""
178+
return "/".join(
179+
urllib.parse.urlparse(self.pull_request_url).path.split("/")[1:3]
180+
)
181+
182+
@property
183+
def pr_labels(self) -> set[str]:
184+
"""
185+
The set of labels applied to the pull request
186+
187+
Returns:
188+
set[str]: the labels applied to the pull request
189+
"""
190+
github_auth = Auth.Token(os.environ.get("GITHUB_TOKEN") or "")
191+
github = Github(auth=github_auth)
192+
pr_no = int(urllib.parse.urlparse(self.pull_request_url).path.split("/")[-1])
193+
return {
194+
x.name
195+
for x in github.get_repo(self.github_repo_name).get_pull(pr_no).get_labels()
196+
}
197+
170198
def check_permissions(self) -> bool:
171199
"""
172200
Check if the pull request owner has permissions to submit a PR for the operator
@@ -271,20 +299,27 @@ def check_permission_for_community(self) -> bool:
271299
f"{self.operator} does not have any reviewers in the base repository "
272300
"or is brand new."
273301
)
274-
if self.pr_owner not in self.reviewers:
302+
303+
if self.pr_owner in self.reviewers:
275304
LOGGER.info(
276-
"Pull request owner %s is not in the list of reviewers %s",
305+
"Pull request owner %s can submit PR for operator %s",
277306
self.pr_owner,
278-
self.reviewers,
307+
self.operator.operator_name,
279308
)
280-
self.request_review_from_owners()
281-
return False
309+
return True
310+
311+
LOGGER.info("Checking if the pull request is approved by a reviewer")
312+
if "approved" in self.pr_labels:
313+
LOGGER.info("Pull request is approved by a reviewer")
314+
return True
315+
282316
LOGGER.info(
283-
"Pull request owner %s can submit PR for operator %s",
317+
"Pull request owner %s is not in the list of reviewers %s",
284318
self.pr_owner,
285-
self.operator.operator_name,
319+
self.reviewers,
286320
)
287-
return True
321+
self.request_review_from_owners()
322+
return False
288323

289324
def request_review_from_maintainers(self) -> None:
290325
"""
@@ -312,11 +347,12 @@ def request_review_from_owners(self) -> None:
312347
"""
313348
reviewers_with_at = ", ".join(map(lambda x: f"@{x}", self.reviewers))
314349
comment_text = (
315-
"Author of the PR is not listed as one of the reviewers in ci.yaml.\n"
316-
"Please review the PR and approve it with \`/lgtm\` comment.\n" # pylint: disable=anomalous-backslash-in-string
317-
f"{reviewers_with_at} \n\n"
318-
"Consider adding author of the PR to the ci.yaml file if you want automated "
319-
"approval for a followup submissions."
350+
"The author of the PR is not listed as one of the reviewers in ci.yaml.\n"
351+
f"{reviewers_with_at}: please review the PR and approve it with an "
352+
"`/approve` comment.\n\n"
353+
"Consider adding the author of the PR to the list of reviewers in "
354+
"the ci.yaml file if you want automated merge without explicit "
355+
"approval."
320356
)
321357

322358
run_command(

operator-pipeline-images/tests/entrypoints/test_check_permissions.py

+86-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from pathlib import Path
2-
from typing import Any
2+
from typing import Any, Optional
33
from unittest import mock
44
from unittest.mock import MagicMock, call, patch
55

@@ -105,6 +105,33 @@ def test_OperatorReview_github_repo_org(
105105
assert review_community.github_repo_org == "my-org"
106106

107107

108+
def test_OperatorReview_github_repo_name(
109+
review_community: check_permissions.OperatorReview,
110+
) -> None:
111+
assert review_community.github_repo_name == "my-org/repo-123"
112+
113+
114+
@patch("operatorcert.entrypoints.check_permissions.Github")
115+
@patch("operatorcert.entrypoints.check_permissions.Auth.Token")
116+
def test_OperatorReview_pr_labels(
117+
mock_token: MagicMock,
118+
mock_github: MagicMock,
119+
review_community: check_permissions.OperatorReview,
120+
) -> None:
121+
repo = MagicMock()
122+
pull = MagicMock()
123+
124+
def _mock_label(name: str) -> MagicMock:
125+
m = MagicMock()
126+
m.name = name
127+
return m
128+
129+
pull.get_labels.return_value = [_mock_label("foo"), _mock_label("bar")]
130+
repo.get_pull.return_value = pull
131+
mock_github.return_value.get_repo.return_value = repo
132+
assert review_community.pr_labels == {"foo", "bar"}
133+
134+
108135
@pytest.mark.parametrize(
109136
"is_org_member, is_partner, permission_partner, permission_community, permission_partner_called, permission_community_called, expected_result",
110137
[
@@ -225,31 +252,73 @@ def test_OperatorReview_check_permission_for_partner(
225252
review_partner.check_permission_for_partner()
226253

227254

255+
@pytest.mark.parametrize(
256+
["reviewers", "labels", "owners_review", "result"],
257+
[
258+
pytest.param(
259+
[],
260+
set(),
261+
False,
262+
check_permissions.MaintainersReviewNeeded,
263+
id="No reviewers",
264+
),
265+
pytest.param(
266+
["owner", "foo"],
267+
set(),
268+
False,
269+
True,
270+
id="author is reviewer",
271+
),
272+
pytest.param(
273+
["foo", "bar"],
274+
set(),
275+
True,
276+
False,
277+
id="author is not reviewer",
278+
),
279+
pytest.param(
280+
["bar", "baz"],
281+
{"approved"},
282+
False,
283+
True,
284+
id="author is not reviewer, PR approved",
285+
),
286+
],
287+
)
228288
@patch(
229289
"operatorcert.entrypoints.check_permissions.OperatorReview.request_review_from_owners"
230290
)
231291
@patch(
232292
"operatorcert.entrypoints.check_permissions.OperatorReview.reviewers",
233293
new_callable=mock.PropertyMock,
234294
)
295+
@patch(
296+
"operatorcert.entrypoints.check_permissions.OperatorReview.pr_labels",
297+
new_callable=mock.PropertyMock,
298+
)
235299
def test_OperatorReview_check_permission_for_community(
300+
mock_labels: MagicMock,
236301
mock_reviewers: MagicMock,
237302
mock_review_from_owners: MagicMock,
238303
review_community: check_permissions.OperatorReview,
304+
reviewers: list[str],
305+
labels: set[str],
306+
owners_review: bool,
307+
result: bool | type,
239308
) -> None:
240-
mock_reviewers.return_value = []
241-
with pytest.raises(check_permissions.MaintainersReviewNeeded):
242-
review_community.check_permission_for_community()
243-
244-
mock_reviewers.return_value = ["user1", "user2"]
309+
mock_reviewers.return_value = reviewers
310+
mock_labels.return_value = labels
245311

246-
assert review_community.check_permission_for_community() == False
247-
mock_review_from_owners.assert_called_once()
312+
if isinstance(result, bool):
313+
assert review_community.check_permission_for_community() == result
314+
else:
315+
with pytest.raises(result):
316+
review_community.check_permission_for_community()
248317

249-
mock_review_from_owners.reset_mock()
250-
mock_reviewers.return_value = ["owner"]
251-
assert review_community.check_permission_for_community() == True
252-
mock_review_from_owners.assert_not_called()
318+
if owners_review:
319+
mock_review_from_owners.assert_called_once()
320+
else:
321+
mock_review_from_owners.assert_not_called()
253322

254323

255324
@patch("operatorcert.entrypoints.check_permissions.run_command")
@@ -283,10 +352,11 @@ def test_OperatorReview_request_review_from_owners(
283352
"comment",
284353
review_community.pull_request_url,
285354
"--body",
286-
"Author of the PR is not listed as one of the reviewers in ci.yaml.\n"
287-
"Please review the PR and approve it with \\`/lgtm\\` comment.\n"
288-
"@user1, @user2 \n\nConsider adding author of the PR to the ci.yaml "
289-
"file if you want automated approval for a followup submissions.",
355+
"The author of the PR is not listed as one of the reviewers in ci.yaml.\n"
356+
"@user1, @user2: please review the PR and approve it with an `/approve` comment.\n\n"
357+
"Consider adding the author of the PR to the list of reviewers in "
358+
"the ci.yaml file if you want automated merge without explicit "
359+
"approval.",
290360
]
291361
)
292362

0 commit comments

Comments
 (0)