Skip to content

Commit 3bb33ca

Browse files
Merge branch 'master' into issues/646-releaser-auto-branch-selection
2 parents e338014 + 98540a6 commit 3bb33ca

File tree

14 files changed

+1183
-227
lines changed

14 files changed

+1183
-227
lines changed

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

Lines changed: 135 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import time
2-
from collections import deque
32
from datetime import datetime, timezone
43

54
from base import GitHubBot
65
from utils import unassign_linked_issues_helper
76

7+
# GitHub author_association values that represent project maintainers.
8+
MAINTAINER_ROLES = frozenset({"OWNER", "MEMBER", "COLLABORATOR"})
9+
810

911
class StalePRBot(GitHubBot):
1012
def __init__(self):
@@ -13,6 +15,54 @@ def __init__(self):
1315
self.DAYS_BEFORE_UNASSIGN = 14
1416
self.DAYS_BEFORE_CLOSE = 60
1517

18+
def _get_last_author_activity(
19+
self,
20+
pr,
21+
after_date,
22+
issue_comments=None,
23+
all_reviews=None,
24+
review_comments=None,
25+
):
26+
"""Return the datetime of the PR author's latest activity after *after_date*.
27+
28+
Returns ``None`` when the author has not acted since *after_date*.
29+
"""
30+
pr_author = pr.user.login if pr.user else None
31+
if not pr_author:
32+
return None
33+
last_activity = None
34+
for commit in pr.get_commits():
35+
commit_date = commit.commit.author.date
36+
if commit_date > after_date:
37+
if commit.author and commit.author.login == pr_author:
38+
if not last_activity or commit_date > last_activity:
39+
last_activity = commit_date
40+
if issue_comments is None:
41+
issue_comments = list(pr.get_issue_comments())
42+
for comment in issue_comments:
43+
if comment.user and comment.user.login == pr_author:
44+
comment_date = comment.created_at
45+
if comment_date > after_date:
46+
if not last_activity or comment_date > last_activity:
47+
last_activity = comment_date
48+
if review_comments is None:
49+
review_comments = list(pr.get_review_comments())
50+
for comment in review_comments:
51+
if comment.user and comment.user.login == pr_author:
52+
comment_date = comment.created_at
53+
if comment_date > after_date:
54+
if not last_activity or comment_date > last_activity:
55+
last_activity = comment_date
56+
if all_reviews is None:
57+
all_reviews = list(pr.get_reviews())
58+
for review in all_reviews:
59+
if review.user and review.user.login == pr_author:
60+
review_date = review.submitted_at
61+
if review_date and review_date > after_date:
62+
if not last_activity or review_date > last_activity:
63+
last_activity = review_date
64+
return last_activity
65+
1666
def get_days_since_activity(
1767
self,
1868
pr,
@@ -23,78 +73,95 @@ def get_days_since_activity(
2373
):
2474
if not last_changes_requested:
2575
return 0
76+
try:
77+
last_author_activity = self._get_last_author_activity(
78+
pr,
79+
last_changes_requested,
80+
issue_comments,
81+
all_reviews,
82+
review_comments,
83+
)
84+
reference_date = last_author_activity or last_changes_requested
85+
now = datetime.now(timezone.utc)
86+
return (now - reference_date).days
87+
except Exception as e:
88+
print("Error calculating activity" f" for PR #{pr.number}: {e}")
89+
return 0
90+
91+
def is_waiting_for_maintainer(
92+
self,
93+
pr,
94+
last_changes_requested,
95+
issue_comments=None,
96+
all_reviews=None,
97+
review_comments=None,
98+
):
99+
"""Return True when the contributor has responded but no maintainer has acted since.
100+
101+
The bot should not warn, mark stale, or close a PR when the ball
102+
is in the maintainers' court.
103+
"""
26104
try:
27105
pr_author = pr.user.login if pr.user else None
28106
if not pr_author:
29-
return 0
30-
last_author_activity = None
31-
commits = deque(pr.get_commits(), maxlen=50)
32-
for commit in commits:
33-
commit_date = commit.commit.author.date
34-
if commit_date > last_changes_requested:
35-
if commit.author and commit.author.login == pr_author:
36-
if (
37-
not last_author_activity
38-
or commit_date > last_author_activity
39-
):
40-
last_author_activity = commit_date
107+
return False
108+
last_author_activity = self._get_last_author_activity(
109+
pr,
110+
last_changes_requested,
111+
issue_comments,
112+
all_reviews,
113+
review_comments,
114+
)
115+
if not last_author_activity:
116+
return False
117+
# Check for maintainer activity after the contributor's last action.
118+
# Only OWNER / MEMBER / COLLABORATOR responses count; random
119+
# community comments and bot messages do not.
41120
if issue_comments is None:
42121
issue_comments = list(pr.get_issue_comments())
43-
comments = (
44-
issue_comments[-20:] if len(issue_comments) > 20 else issue_comments
45-
)
46-
for comment in comments:
47-
if comment.user and comment.user.login == pr_author:
48-
comment_date = comment.created_at
49-
if comment_date > last_changes_requested:
50-
if (
51-
not last_author_activity
52-
or comment_date > last_author_activity
53-
):
54-
last_author_activity = comment_date
122+
for comment in issue_comments:
123+
if (
124+
comment.user
125+
and comment.user.login != pr_author
126+
and comment.user.type != "Bot"
127+
and getattr(comment, "author_association", None) in MAINTAINER_ROLES
128+
and comment.created_at > last_author_activity
129+
):
130+
return False
55131
if review_comments is None:
56132
review_comments = list(pr.get_review_comments())
57-
all_review_comments = review_comments
58-
review_comments = (
59-
all_review_comments[-20:]
60-
if len(all_review_comments) > 20
61-
else all_review_comments
62-
)
63133
for comment in review_comments:
64-
if comment.user and comment.user.login == pr_author:
65-
comment_date = comment.created_at
66-
if comment_date > last_changes_requested:
67-
if (
68-
not last_author_activity
69-
or comment_date > last_author_activity
70-
):
71-
last_author_activity = comment_date
134+
if (
135+
comment.user
136+
and comment.user.login != pr_author
137+
and comment.user.type != "Bot"
138+
and getattr(comment, "author_association", None) in MAINTAINER_ROLES
139+
and comment.created_at > last_author_activity
140+
):
141+
return False
72142
if all_reviews is None:
73143
all_reviews = list(pr.get_reviews())
74-
reviews = all_reviews[-20:] if len(all_reviews) > 20 else all_reviews
75-
for review in reviews:
76-
if review.user and review.user.login == pr_author:
77-
review_date = review.submitted_at
78-
if review_date and review_date > last_changes_requested:
79-
if (
80-
not last_author_activity
81-
or review_date > last_author_activity
82-
):
83-
last_author_activity = review_date
84-
reference_date = last_author_activity or last_changes_requested
85-
now = datetime.now(timezone.utc)
86-
return (now - reference_date).days
144+
for review in all_reviews:
145+
if (
146+
review.user
147+
and review.user.login != pr_author
148+
and review.user.type != "Bot"
149+
and getattr(review, "author_association", None) in MAINTAINER_ROLES
150+
and review.submitted_at
151+
and review.submitted_at > last_author_activity
152+
):
153+
return False
154+
return True
87155
except Exception as e:
88-
print("Error calculating activity" f" for PR #{pr.number}: {e}")
89-
return 0
156+
print("Error checking maintainer activity" f" for PR #{pr.number}: {e}")
157+
return False
90158

91159
def get_last_changes_requested(self, pr, all_reviews=None):
92160
try:
93161
if all_reviews is None:
94162
all_reviews = list(pr.get_reviews())
95-
reviews = all_reviews[-50:] if len(all_reviews) > 50 else all_reviews
96163
changes_requested_reviews = [
97-
r for r in reviews if r.state == "CHANGES_REQUESTED"
164+
r for r in all_reviews if r.state == "CHANGES_REQUESTED"
98165
]
99166
if not changes_requested_reviews:
100167
return None
@@ -348,6 +415,18 @@ def process_stale_prs(self):
348415
f"PR #{pr.number}: {days_inactive}"
349416
" days since contributor activity"
350417
)
418+
if self.is_waiting_for_maintainer(
419+
pr,
420+
last_changes_requested,
421+
issue_comments,
422+
all_reviews,
423+
review_comments,
424+
):
425+
print(
426+
f"PR #{pr.number}: waiting for"
427+
" maintainer review, skipping"
428+
)
429+
continue
351430
if days_inactive >= self.DAYS_BEFORE_CLOSE:
352431
if self.close_stale_pr(pr, days_inactive):
353432
processed_count += 1

0 commit comments

Comments
 (0)