Skip to content

Commit b8a33f4

Browse files
authored
Merge branch 'master' into fix/readonlyadmin-cascade-delete
2 parents 021ace7 + 8418935 commit b8a33f4

11 files changed

Lines changed: 863 additions & 95 deletions

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

Lines changed: 155 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,29 @@
1+
import os
12
import re
23

34
from base import GitHubBot
45
from utils import (
56
extract_linked_issues,
7+
find_open_pr_for_issue,
8+
get_assignee_logins,
69
get_valid_linked_issues,
710
unassign_linked_issues_helper,
11+
user_in_logins,
12+
verify_assignment,
813
)
914

1015

1116
class IssueAssignmentBot(GitHubBot):
17+
def __init__(self):
18+
super().__init__()
19+
self.bot_username = os.environ.get("BOT_USERNAME", "openwisp-companion")
20+
21+
def is_bot_assign_command(self, comment_body):
22+
if not comment_body:
23+
return False
24+
pattern = rf"(?<![\w-])@{re.escape(self.bot_username)}\s+assign\b"
25+
return bool(re.search(pattern, comment_body, re.IGNORECASE))
26+
1227
def is_assignment_request(self, comment_body):
1328
if not comment_body:
1429
return False
@@ -194,6 +209,90 @@ def respond_to_assignment_request(self, issue_number, commenter):
194209
print(f"Error responding to assignment request: {e}")
195210
return False
196211

212+
def _cannot_auto_assign_message(self, pr_author, pr_number):
213+
return (
214+
f"Hi @{pr_author} 👋,\n\n"
215+
f"Thank you for opening PR #{pr_number} to address this issue!\n\n"
216+
"GitHub did not allow the bot to assign this issue to you"
217+
" automatically, because you are not yet an organization"
218+
" member and have not previously commented on this issue.\n\n"
219+
f"To get assigned, please comment `@{self.bot_username} assign`"
220+
" on this issue. Your comment satisfies GitHub's requirement"
221+
" and the bot will then assign the issue to you."
222+
)
223+
224+
def handle_bot_assign_request(self, issue_number, commenter):
225+
if not self.repo:
226+
print("GitHub client not initialized")
227+
return False
228+
try:
229+
issue = self.repo.get_issue(issue_number)
230+
if (
231+
hasattr(issue, "repository")
232+
and issue.repository.full_name != self.repository_name
233+
):
234+
print(
235+
f"Issue #{issue_number} is from a different"
236+
" repository, ignoring bot command"
237+
)
238+
return True
239+
if issue.pull_request:
240+
print(f"#{issue_number} is a PR, ignoring bot command")
241+
return True
242+
if getattr(issue, "state", "open") == "closed":
243+
print(f"#{issue_number} is closed, ignoring bot command")
244+
return True
245+
if user_in_logins(commenter, get_assignee_logins(issue)):
246+
print(f"{commenter} is already assigned to #{issue_number}")
247+
return True
248+
try:
249+
pr = find_open_pr_for_issue(
250+
self.github, self.repository_name, commenter, issue_number
251+
)
252+
except Exception as e:
253+
# Don't post "no PR found" on a search error — that's
254+
# not the same as a verified miss.
255+
print(f"Error searching open PRs by {commenter}: {e}")
256+
return True
257+
if pr is None:
258+
issue.create_comment(
259+
f"Hi @{commenter} 👋,\n\n"
260+
"I could not find an open PR by you that references"
261+
f" this issue (#{issue_number}). Please open a PR"
262+
f" linking to this issue (e.g. `Fixes #{issue_number}`)"
263+
f" and then comment `@{self.bot_username} assign` again."
264+
)
265+
return True
266+
issue.add_to_assignees(commenter)
267+
verified = verify_assignment(self.repo, issue_number, commenter)
268+
if verified is True:
269+
issue.create_comment(
270+
f"This issue has been assigned to @{commenter}"
271+
f" who opened PR #{pr.number} to address it. 🎯"
272+
)
273+
print(f"Assigned #{issue_number} to {commenter} via bot command")
274+
elif verified is False:
275+
# Commenter has commented but the assignment still
276+
# failed (perm block, outage, etc.).
277+
issue.create_comment(
278+
f"Sorry @{commenter}, GitHub still did not allow"
279+
" this assignment. A maintainer will need to assign"
280+
" this issue manually."
281+
)
282+
print(
283+
f"Bot-command assignment of #{issue_number} to"
284+
f" {commenter} was silently rejected."
285+
)
286+
else:
287+
print(
288+
f"Skipping comment for #{issue_number}:"
289+
" assignment state could not be verified."
290+
)
291+
return True
292+
except Exception as e:
293+
print(f"Error handling bot assign command: {e}")
294+
return False
295+
197296
def auto_assign_issues_from_pr(self, pr_number, pr_author, pr_body, max_issues=10):
198297
if not self.repo:
199298
print("GitHub client not initialized")
@@ -211,18 +310,20 @@ def auto_assign_issues_from_pr(self, pr_number, pr_author, pr_body, max_issues=1
211310
)
212311
assigned_issues = []
213312
for issue_number, issue in get_valid_linked_issues(
214-
self.repo, self.repository_name, pr_body
313+
self.repo, self.repository_name, linked_issues
215314
):
216315
if len(assigned_issues) >= max_issues:
217316
break
218317
try:
219-
current_assignees = [
220-
assignee.login
221-
for assignee in issue.assignees
222-
if hasattr(assignee, "login")
223-
]
318+
if getattr(issue, "state", "open") == "closed":
319+
print(
320+
f"Issue #{issue_number} is closed, skipping"
321+
" auto-assignment"
322+
)
323+
continue
324+
current_assignees = get_assignee_logins(issue)
224325
if current_assignees:
225-
if pr_author in current_assignees:
326+
if user_in_logins(pr_author, current_assignees):
226327
print(
227328
f"Issue #{issue_number} already"
228329
f" assigned to {pr_author}"
@@ -235,17 +336,32 @@ def auto_assign_issues_from_pr(self, pr_number, pr_author, pr_body, max_issues=1
235336
)
236337
continue
237338
issue.add_to_assignees(pr_author)
238-
assigned_issues.append(issue_number)
239-
print(f"Assigned issue #{issue_number}" f" to {pr_author}")
240-
comment_message = (
241-
"This issue has been automatically"
242-
f" assigned to @{pr_author}"
243-
f" who opened PR #{pr_number}"
244-
" to address it. 🎯"
245-
)
246-
issue.create_comment(comment_message)
339+
verified = verify_assignment(self.repo, issue_number, pr_author)
340+
if verified is True:
341+
assigned_issues.append(issue_number)
342+
print(f"Assigned issue #{issue_number} to {pr_author}")
343+
comment_message = (
344+
"This issue has been automatically"
345+
f" assigned to @{pr_author}"
346+
f" who opened PR #{pr_number}"
347+
" to address it. 🎯"
348+
)
349+
issue.create_comment(comment_message)
350+
elif verified is False:
351+
print(
352+
f"Assignment of #{issue_number} to {pr_author}"
353+
" was silently rejected by GitHub."
354+
)
355+
issue.create_comment(
356+
self._cannot_auto_assign_message(pr_author, pr_number)
357+
)
358+
else:
359+
print(
360+
f"Skipping comment for #{issue_number}:"
361+
" assignment state could not be verified."
362+
)
247363
except Exception as e:
248-
print(f"Error processing issue" f" #{issue_number}: {e}")
364+
print(f"Error processing issue #{issue_number}: {e}")
249365
return assigned_issues
250366
except Exception as e:
251367
print(f"Error in auto_assign_issues_from_pr: {e}")
@@ -266,11 +382,27 @@ def unassign_issues_from_pr(self, pr_body, pr_author):
266382
print(f"Error in unassign_issues_from_pr: {e}")
267383
return []
268384

385+
def _is_bot_comment(self, comment):
386+
user = comment.get("user") or {}
387+
if (user.get("type") or "").lower() == "bot":
388+
return True
389+
if comment.get("performed_via_github_app"):
390+
return True
391+
login = (user.get("login") or "").lower()
392+
return login in {
393+
self.bot_username.lower(),
394+
f"{self.bot_username}[bot]".lower(),
395+
}
396+
269397
def handle_issue_comment(self):
270398
if not self.event_payload:
271399
print("No event payload available")
272400
return False
273401
try:
402+
action = self.event_payload.get("action")
403+
if action and action != "created":
404+
print(f"Ignoring issue_comment action '{action}'")
405+
return True
274406
if self.event_payload.get("issue", {}).get("pull_request"):
275407
print("Comment is on a PR, not an issue - skipping")
276408
return True
@@ -282,9 +414,14 @@ def handle_issue_comment(self):
282414
if not all([comment_body, commenter, issue_number]):
283415
print("Missing required comment data")
284416
return False
417+
if self._is_bot_comment(comment):
418+
print("Ignoring comment posted by a bot")
419+
return True
420+
if self.is_bot_assign_command(comment_body):
421+
return self.handle_bot_assign_request(issue_number, commenter)
285422
if self.is_assignment_request(comment_body):
286423
return self.respond_to_assignment_request(issue_number, commenter)
287-
print("Comment does not contain assignment request")
424+
print("Comment does not contain an assignment request or bot command")
288425
return True
289426
except Exception as e:
290427
print(f"Error handling issue comment: {e}")

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

Lines changed: 52 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,57 @@
22
import os
33

44
from base import GitHubBot
5-
from utils import get_valid_linked_issues
5+
from utils import (
6+
extract_linked_issues,
7+
get_assignee_logins,
8+
get_valid_linked_issues,
9+
user_in_logins,
10+
verify_assignment,
11+
)
612

713

814
class PRReopenBot(GitHubBot):
915
def reassign_issues_to_author(self, pr_number, pr_author, pr_body):
1016
try:
1117
reassigned_issues = []
18+
linked_issues = extract_linked_issues(pr_body)
1219
for issue_number, issue in get_valid_linked_issues(
13-
self.repo, self.repository_name, pr_body
20+
self.repo, self.repository_name, linked_issues
1421
):
1522
try:
16-
current_assignees = [
17-
assignee.login
18-
for assignee in issue.assignees
19-
if hasattr(assignee, "login")
20-
]
21-
if current_assignees and pr_author not in current_assignees:
23+
current_assignees = get_assignee_logins(issue)
24+
if current_assignees and not user_in_logins(
25+
pr_author, current_assignees
26+
):
2227
print(
2328
f"Issue #{issue_number} is assigned"
2429
" to others:"
2530
f' {", ".join(current_assignees)}'
2631
)
2732
continue
28-
if pr_author not in current_assignees:
29-
issue.add_to_assignees(pr_author)
30-
reassigned_issues.append(issue_number)
31-
print(f"Reassigned issue #{issue_number}" f" to {pr_author}")
32-
welcome_message = (
33-
f"Welcome back, @{pr_author}! 🎉"
34-
" This issue has been reassigned"
35-
" to you as you've reopened"
36-
f" PR #{pr_number}."
33+
if user_in_logins(pr_author, current_assignees):
34+
continue
35+
issue.add_to_assignees(pr_author)
36+
if (
37+
verify_assignment(self.repo, issue_number, pr_author)
38+
is not True
39+
):
40+
print(
41+
f"Reassign of #{issue_number} to {pr_author}"
42+
" was silently rejected or unverifiable."
3743
)
38-
issue.create_comment(welcome_message)
44+
continue
45+
reassigned_issues.append(issue_number)
46+
print(f"Reassigned issue #{issue_number} to {pr_author}")
47+
welcome_message = (
48+
f"Welcome back, @{pr_author}! 🎉"
49+
" This issue has been reassigned"
50+
" to you as you've reopened"
51+
f" PR #{pr_number}."
52+
)
53+
issue.create_comment(welcome_message)
3954
except Exception as e:
40-
print(f"Error processing issue" f" #{issue_number}: {e}")
55+
print(f"Error processing issue #{issue_number}: {e}")
4156
return reassigned_issues
4257
except Exception as e:
4358
print(f"Error in reassign_issues_to_author: {e}")
@@ -115,7 +130,7 @@ def handle_contributor_activity(self):
115130
print("Comment is on an issue," " not a PR, skipping")
116131
return True
117132
pr = self.repo.get_pull(pr_number)
118-
if not pr.user or commenter != pr.user.login:
133+
if not pr.user or not user_in_logins(commenter, [pr.user.login]):
119134
print("Comment not from PR author, skipping")
120135
return True
121136
labels = [label.name for label in pr.get_labels()]
@@ -128,21 +143,27 @@ def handle_contributor_activity(self):
128143
except Exception as e:
129144
print(f"Could not remove stale label: {e}")
130145
reassigned_count = 0
146+
linked_issues = extract_linked_issues(pr.body or "")
131147
for issue_number, issue in get_valid_linked_issues(
132-
self.repo, self.repository_name, pr.body or ""
148+
self.repo, self.repository_name, linked_issues
133149
):
134150
try:
135-
current_assignees = [
136-
assignee.login
137-
for assignee in issue.assignees
138-
if hasattr(assignee, "login")
139-
]
140-
if not current_assignees:
141-
issue.add_to_assignees(commenter)
142-
reassigned_count += 1
143-
print(f"Reassigned issue #{issue_number}" f" to {commenter}")
151+
if get_assignee_logins(issue):
152+
continue
153+
issue.add_to_assignees(commenter)
154+
if (
155+
verify_assignment(self.repo, issue_number, commenter)
156+
is not True
157+
):
158+
print(
159+
f"Reassign of #{issue_number} to {commenter}"
160+
" was silently rejected or unverifiable."
161+
)
162+
continue
163+
reassigned_count += 1
164+
print(f"Reassigned issue #{issue_number} to {commenter}")
144165
except Exception as e:
145-
print(f"Error reassigning issue" f" #{issue_number}: {e}")
166+
print(f"Error reassigning issue #{issue_number}: {e}")
146167
if reassigned_count > 0:
147168
encouragement_message = (
148169
f"Thanks for following up, @{commenter}! 🙌"

0 commit comments

Comments
 (0)