Skip to content

Commit 9539791

Browse files
Merge branch 'master' into feature/709-flag-invalid-prs
2 parents e719d10 + 6496fa3 commit 9539791

14 files changed

Lines changed: 126 additions & 91 deletions

File tree

.github/actions/bot-autoassign/base.py

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ def __init__(self):
2222
self.bot_login = (
2323
bot_username if bot_username.endswith("[bot]") else f"{bot_username}[bot]"
2424
)
25-
2625
if self.github_token and self.repository_name:
2726
try:
2827
self.github = Github(self.github_token)
@@ -68,11 +67,8 @@ def get_issue_projects(self, owner, repo_name, issue_number):
6867
"""
6968
variables = {"owner": owner, "repo": repo_name, "issueNumber": issue_number}
7069
headers, result = self.github.requester.graphql_query(query, variables)
71-
print(f"DEBUG: Raw GraphQL response: {result}")
72-
7370
if "errors" in result:
7471
raise ValueError(f"GraphQL API Permission Error: {result['errors']}")
75-
7672
repo_data = result.get("data", {}).get("repository")
7773
if not repo_data:
7874
raise ValueError(
@@ -108,13 +104,11 @@ def validate_pr_issues(self, pr):
108104
if not self.github or not self.repository_name:
109105
print("GitHub client or repository name not initialized")
110106
return False
111-
112107
pr_author = (
113108
pr.user.login
114109
if pr.user and isinstance(getattr(pr.user, "login", None), str)
115110
else ""
116111
)
117-
118112
exclude_authors_env = os.environ.get(
119113
"EXCLUDE_PR_AUTHORS", DEFAULT_EXCLUDE_PR_AUTHORS
120114
)
@@ -124,36 +118,31 @@ def validate_pr_issues(self, pr):
124118
if pr_author in excluded_authors:
125119
print(f"Author {pr_author} is in the exclude list. Proceeding.")
126120
return True
127-
128121
author_association = str(getattr(pr, "author_association", "") or "")
129122
if author_association in MAINTAINER_ROLES:
130123
print(
131124
f"Author {pr_author} is exempt due to association: "
132125
f"{author_association}. Proceeding."
133126
)
134127
return True
135-
136128
from utils import extract_all_linked_issues
137129

138130
pr_body = pr.body if isinstance(pr.body, str) else ""
139131
linked_issues = extract_all_linked_issues(pr_body, self.repository_name)
140132
if not linked_issues:
141133
print("No linked issues found in PR body for external contributor.")
142134
return False
143-
144135
current_org = self.repository_name.split("/")[0].lower()
145136
required_projects = {
146137
self._project_title_key(p) for p in REQUIRED_CONTRIBUTOR_PROJECTS
147138
}
148-
149139
for owner, repo_name, issue_number in linked_issues:
150140
if owner.lower() != current_org:
151141
print(
152142
f"Issue {owner}/{repo_name}#{issue_number} does not belong "
153143
f"to organization {current_org}, skipping validation."
154144
)
155145
continue
156-
157146
try:
158147
from github import GithubException
159148

@@ -167,19 +156,16 @@ def validate_pr_issues(self, pr):
167156
continue
168157
print(f"Error fetching issue {owner}/{repo_name}#{issue_number}: {e}")
169158
raise
170-
171159
if issue.pull_request:
172160
print(
173161
f"Reference {owner}/{repo_name}#{issue_number} is a pull request, skipping validation."
174162
)
175163
continue
176-
177164
if issue.state != "open":
178165
print(
179166
f"Issue {owner}/{repo_name}#{issue_number} is not open, skipping validation."
180167
)
181168
continue
182-
183169
issue_labels = [label.name.lower() for label in issue.labels]
184170
valid_labels = [
185171
lbl for lbl in issue_labels if lbl not in INVALID_ISSUE_LABELS
@@ -195,22 +181,14 @@ def validate_pr_issues(self, pr):
195181
"invalid/wontfix label, skipping validation."
196182
)
197183
continue
198-
199184
try:
200185
projects = self.get_issue_projects(owner, repo_name, issue_number)
201186
except Exception as e:
202187
print(
203188
f"Error fetching projects for issue {owner}/{repo_name}#{issue_number}: {e}"
204189
)
205190
raise
206-
207-
print(
208-
f"DEBUG: Raw projects for {owner}/{repo_name}#{issue_number}: {projects}"
209-
)
210191
project_keys = [self._project_title_key(p) for p in projects]
211-
print(f"DEBUG: Normalized project keys: {project_keys}")
212-
print(f"DEBUG: Required project keys: {required_projects}")
213-
214192
has_valid_project = any(key in required_projects for key in project_keys)
215193
if has_valid_project:
216194
print(
@@ -223,12 +201,10 @@ def validate_pr_issues(self, pr):
223201
f"to any required project (found: {projects or 'none'}), "
224202
"skipping validation."
225203
)
226-
227204
return False
228205

229206
def get_bot_comment(self, pr, comment_type, after_date=None, issue_comments=None):
230207
"""Get the comment of this bot with the given marker if it exists.
231-
232208
If ``after_date`` is provided, only considers comments posted after that date.
233209
"""
234210
try:
@@ -251,7 +227,6 @@ def get_bot_comment(self, pr, comment_type, after_date=None, issue_comments=None
251227

252228
def has_bot_comment(self, pr, comment_type, after_date=None, issue_comments=None):
253229
"""Check if PR already has a specific type of bot comment.
254-
255230
Uses HTML markers. If ``after_date`` is provided,
256231
only considers comments posted after that date.
257232
"""
@@ -277,11 +252,14 @@ def get_invalid_unvalidated_issue_comment(self, pr_author):
277252
"request.\n\n"
278253
"An issue is considered validated when it is open, has an appropriate "
279254
"label other than `invalid` or `wontfix`, and is assigned to one of "
280-
"the OpenWISP contributor project boards mentioned in the "
281-
"contributing guidelines.\n\n"
282-
"Please see the OpenWISP policy on unsolicited and AI-assisted "
283-
"contributions:\n"
284-
"https://openwisp.io/docs/dev/general/code-of-conduct.html\n\n"
255+
"the project boards mentioned in the "
256+
"[OpenWISP Contributing Guidelines]"
257+
"(https://openwisp.io/docs/dev/developer/contributing.html).\n\n"
258+
"Please see the [OpenWISP Anti AI Spam Policy]"
259+
"(https://openwisp.io/docs/dev/general/code-of-conduct.html).\n\n"
260+
"Feel free to join the [OpenWISP dev chatroom]"
261+
"(https://matrix.to/#/#openwisp_development:gitter.im) "
262+
"to coordinate with the development team.\n\n"
285263
"If this is not resolved within 24 hours, this pull request "
286264
"will be closed automatically. "
287265
"Thank you for your understanding."

.github/actions/bot-autoassign/issue_assignment_bot.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ def get_contributing_guidelines_url(self):
4040

4141
def detect_issue_type(self, issue):
4242
"""Analyzes labels, title and body.
43-
4443
Returns 'bug', 'feature', or None.
4544
"""
4645
bug_keywords = [
@@ -365,11 +364,9 @@ def auto_assign_issues_from_pr(self, pr_number, pr_author, pr_body, max_issues=1
365364

366365
def unassign_issues_from_pr(self, pr_body, pr_author):
367366
"""Unassign linked issues from PR author"""
368-
369367
if not self.repo:
370368
print("GitHub client not initialized")
371369
return []
372-
373370
try:
374371
return unassign_linked_issues_helper(
375372
self.repo, self.repository_name, pr_body, pr_author
@@ -442,17 +439,16 @@ def handle_pull_request(self):
442439
else:
443440
self.unassign_issues_from_pr(pr_body, pr_author)
444441
return True
445-
446442
if action in ["opened", "reopened", "edited", "ready_for_review"]:
447-
self.auto_assign_issues_from_pr(pr_number, pr_author, pr_body)
448443
pr_obj = self.repo.get_pull(pr_number)
449444
is_valid = self.validate_pr_issues(pr_obj)
445+
if is_valid:
446+
self.auto_assign_issues_from_pr(pr_number, pr_author, pr_body)
450447
labels_lower = set()
451448
try:
452449
labels_lower = {label.name.lower() for label in pr_obj.labels}
453450
except (TypeError, AttributeError):
454451
pass
455-
456452
if is_valid:
457453
if "invalid" in labels_lower:
458454
pr_obj.remove_from_labels("invalid")
@@ -461,7 +457,6 @@ def handle_pull_request(self):
461457
if "invalid" not in labels_lower:
462458
pr_obj.add_to_labels("invalid")
463459
print(f"Added 'invalid' label to PR #{pr_number}")
464-
465460
if not self.has_bot_comment(pr_obj, "invalid_unvalidated_issue"):
466461
comment_body = self.get_invalid_unvalidated_issue_comment(
467462
pr_author

.github/actions/bot-autoassign/tests/test_issue_assignment_bot.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import os
22
import sys
3-
from unittest.mock import Mock, patch
3+
from unittest.mock import MagicMock, Mock, patch
44

55
# Add the parent directory to path for importing bot modules
66
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -516,6 +516,7 @@ def test_opened(self, bot_env):
516516
)
517517
mock_issue = _make_issue_with_assignment("testuser")
518518
bot_env["repo"].get_issue.return_value = mock_issue
519+
bot.validate_pr_issues = MagicMock(return_value=True)
519520
assert bot.handle_pull_request()
520521
mock_issue.add_to_assignees.assert_called_once_with("testuser")
521522

@@ -533,8 +534,27 @@ def test_reopened(self, bot_env):
533534
)
534535
mock_issue = _make_issue_with_assignment("testuser")
535536
bot_env["repo"].get_issue.return_value = mock_issue
537+
bot.validate_pr_issues = MagicMock(return_value=True)
536538
assert bot.handle_pull_request()
537539

540+
def test_skips_auto_assign_if_invalid(self, bot_env):
541+
bot = IssueAssignmentBot()
542+
bot.load_event_payload(
543+
{
544+
"action": "opened",
545+
"pull_request": {
546+
"number": 100,
547+
"user": {"login": "testuser"},
548+
"body": "Fixes #123",
549+
},
550+
}
551+
)
552+
mock_issue = _make_issue_with_assignment("testuser")
553+
bot_env["repo"].get_issue.return_value = mock_issue
554+
bot.validate_pr_issues = MagicMock(return_value=False)
555+
assert bot.handle_pull_request()
556+
mock_issue.add_to_assignees.assert_not_called()
557+
538558
def test_closed(self, bot_env):
539559
bot = IssueAssignmentBot()
540560
bot.load_event_payload(
@@ -1037,11 +1057,9 @@ def test_validate_pr_issues_is_pr(self, bot_env):
10371057
mock_pr.user.login = "external-contributor"
10381058
mock_pr.author_association = "NONE"
10391059
mock_pr.body = "Fixes #12"
1040-
10411060
mock_issue = Mock()
10421061
mock_issue.pull_request = {"url": "..."}
10431062
bot_env["repo"].get_issue.return_value = mock_issue
1044-
10451063
assert not bot.validate_pr_issues(mock_pr)
10461064

10471065
def test_validate_pr_issues_closed(self, bot_env):
@@ -1050,12 +1068,10 @@ def test_validate_pr_issues_closed(self, bot_env):
10501068
mock_pr.user.login = "external-contributor"
10511069
mock_pr.author_association = "NONE"
10521070
mock_pr.body = "Fixes #12"
1053-
10541071
mock_issue = Mock()
10551072
mock_issue.pull_request = None
10561073
mock_issue.state = "closed"
10571074
bot_env["repo"].get_issue.return_value = mock_issue
1058-
10591075
assert not bot.validate_pr_issues(mock_pr)
10601076

10611077
def test_validate_pr_issues_invalid_labels(self, bot_env):
@@ -1064,21 +1080,18 @@ def test_validate_pr_issues_invalid_labels(self, bot_env):
10641080
mock_pr.user.login = "external-contributor"
10651081
mock_pr.author_association = "NONE"
10661082
mock_pr.body = "Fixes #12"
1067-
10681083
# Test case 1: No labels
10691084
mock_issue = Mock()
10701085
mock_issue.pull_request = None
10711086
mock_issue.state = "open"
10721087
mock_issue.labels = []
10731088
bot_env["repo"].get_issue.return_value = mock_issue
10741089
assert not bot.validate_pr_issues(mock_pr)
1075-
10761090
# Test case 2: Has wontfix/invalid labels
10771091
label_wontfix = Mock()
10781092
label_wontfix.name = "wontfix"
10791093
mock_issue.labels = [label_wontfix]
10801094
assert not bot.validate_pr_issues(mock_pr)
1081-
10821095
# Test case 3: Has a mix of valid and invalid/wontfix labels
10831096
label_bug = Mock()
10841097
label_bug.name = "bug"
@@ -1091,15 +1104,13 @@ def test_validate_pr_issues_not_in_project(self, bot_env):
10911104
mock_pr.user.login = "external-contributor"
10921105
mock_pr.author_association = "NONE"
10931106
mock_pr.body = "Fixes #12"
1094-
10951107
label_bug = Mock()
10961108
label_bug.name = "bug"
10971109
mock_issue = Mock()
10981110
mock_issue.pull_request = None
10991111
mock_issue.state = "open"
11001112
mock_issue.labels = [label_bug]
11011113
bot_env["repo"].get_issue.return_value = mock_issue
1102-
11031114
with patch.object(
11041115
bot, "get_issue_projects", return_value=["Some Other Project"]
11051116
):
@@ -1111,15 +1122,13 @@ def test_validate_pr_issues_fully_valid(self, bot_env):
11111122
mock_pr.user.login = "external-contributor"
11121123
mock_pr.author_association = "NONE"
11131124
mock_pr.body = "Fixes #12"
1114-
11151125
label_bug = Mock()
11161126
label_bug.name = "bug"
11171127
mock_issue = Mock()
11181128
mock_issue.pull_request = None
11191129
mock_issue.state = "open"
11201130
mock_issue.labels = [label_bug]
11211131
bot_env["repo"].get_issue.return_value = mock_issue
1122-
11231132
with patch.object(
11241133
bot, "get_issue_projects", return_value=["OpenWISP Contributor's Board"]
11251134
):
@@ -1138,11 +1147,9 @@ def test_handle_pull_request_invalid_label_and_comment(self, bot_env):
11381147
},
11391148
}
11401149
)
1141-
11421150
mock_pr_obj = Mock()
11431151
mock_pr_obj.labels = []
11441152
bot_env["repo"].get_pull.return_value = mock_pr_obj
1145-
11461153
with patch.object(bot, "validate_pr_issues", return_value=False), patch.object(
11471154
bot, "has_bot_comment", return_value=False
11481155
):
@@ -1167,13 +1174,11 @@ def test_handle_pull_request_valid_removes_label(self, bot_env):
11671174
},
11681175
}
11691176
)
1170-
11711177
mock_label = Mock()
11721178
mock_label.name = "invalid"
11731179
mock_pr_obj = Mock()
11741180
mock_pr_obj.labels = [mock_label]
11751181
bot_env["repo"].get_pull.return_value = mock_pr_obj
1176-
11771182
with patch.object(bot, "validate_pr_issues", return_value=True):
11781183
assert bot.handle_pull_request()
11791184
mock_pr_obj.remove_from_labels.assert_called_once_with("invalid")

0 commit comments

Comments
 (0)