diff --git a/.github/scripts/stale_issue_pr_ping.py b/.github/scripts/stale_issue_pr_ping.py index 0effcd5d75..9c865213ad 100644 --- a/.github/scripts/stale_issue_pr_ping.py +++ b/.github/scripts/stale_issue_pr_ping.py @@ -1,9 +1,11 @@ # Copyright (c) Microsoft. All rights reserved. -"""Scan open issues and PRs for stale follow-ups from external authors. +"""Scan open issues and PRs labeled 'waiting-for-author' for stale follow-ups. -If a team member commented and the external author hasn't replied within -DAYS_THRESHOLD days, post a reminder comment and add the 'needs-info' label. +Team members manually add the 'waiting-for-author' label when they need a +response from the external author. If the author hasn't replied within +DAYS_THRESHOLD days of the last team comment, post a reminder and add the +'requested-info' label to prevent duplicate pings. """ from __future__ import annotations @@ -22,7 +24,8 @@ PING_COMMENT = ( "@{author}, friendly reminder — this issue is waiting on your response. " "Please share any updates when you get a chance. (This is an automated message.)" ) -LABEL = "needs-info" +TRIGGER_LABEL = "waiting-for-author" +PINGED_LABEL = "requested-info" def get_team_members(g: Github, org: str, team_slug: str) -> set[str]: @@ -76,15 +79,21 @@ def should_ping( days_threshold: int, now: datetime, ) -> bool: - """Determine whether this issue/PR should be pinged.""" + """Determine whether this issue/PR should be pinged. + + Only issues/PRs carrying the 'waiting-for-author' label are candidates. + """ author = issue.user.login + # Skip if the trigger label is not present + if not any(label.name == TRIGGER_LABEL for label in issue.labels): + return False # Skip if author is a team member if author in team_members: return False - # Skip if already labeled - if any(label.name == LABEL for label in issue.labels): + # Skip if already pinged + if any(label.name == PINGED_LABEL for label in issue.labels): return False # Skip if no comments at all @@ -112,7 +121,7 @@ def should_ping( def ping(issue: Issue, dry_run: bool) -> bool: - """Post a reminder comment and add the needs-info label. Returns True on success.""" + """Post a reminder comment and add the 'requested-info' label. Returns True on success.""" author = issue.user.login kind = "PR" if issue.pull_request else "Issue" @@ -129,7 +138,7 @@ def ping(issue: Issue, dry_run: bool) -> bool: issue.create_comment(PING_COMMENT.format(author=author)) commented = True if not labeled: - issue.add_to_labels(LABEL) + issue.add_to_labels(PINGED_LABEL) labeled = True print(f" Pinged {kind} #{issue.number} (@{author})") return True @@ -184,9 +193,9 @@ def main() -> None: failed = [] scanned = 0 - print(f"Scanning open issues and PRs (threshold: {days_threshold} days)...\n") + print(f"Scanning open issues and PRs labeled '{TRIGGER_LABEL}' (threshold: {days_threshold} days)...\n") - for issue in repo.get_issues(state="open"): + for issue in repo.get_issues(state="open", labels=[TRIGGER_LABEL]): scanned += 1 if should_ping(issue, team_members, days_threshold, now): diff --git a/.github/tests/test_stale_issue_pr_ping.py b/.github/tests/test_stale_issue_pr_ping.py index f114a84ed8..b9a7ad5d43 100644 --- a/.github/tests/test_stale_issue_pr_ping.py +++ b/.github/tests/test_stale_issue_pr_ping.py @@ -15,8 +15,9 @@ import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts")) from stale_issue_pr_ping import ( - LABEL, + PINGED_LABEL, PING_COMMENT, + TRIGGER_LABEL, author_replied_after, find_last_team_comment, get_team_members, @@ -63,7 +64,10 @@ def _make_issue( issue.user = MagicMock() issue.user.login = author issue.number = number - issue.labels = [_make_label(n) for n in (labels or [])] + # Default to having the trigger label, since the API query pre-filters. + if labels is None: + labels = [TRIGGER_LABEL] + issue.labels = [_make_label(n) for n in labels] issue.comments = comment_count issue.pull_request = MagicMock() if pull_request else None if comments is not None: @@ -136,11 +140,11 @@ class TestShouldPing: assert should_ping(issue, TEAM, 4, NOW) is True def test_skip_team_member_author(self): - issue = _make_issue(author="alice", comment_count=1) + issue = _make_issue(author="alice", labels=[TRIGGER_LABEL], comment_count=1) assert should_ping(issue, TEAM, 4, NOW) is False - def test_skip_already_labeled(self): - issue = _make_issue(labels=[LABEL], comment_count=1) + def test_skip_already_pinged(self): + issue = _make_issue(labels=[TRIGGER_LABEL, PINGED_LABEL], comment_count=1) assert should_ping(issue, TEAM, 4, NOW) is False def test_skip_no_comments(self): @@ -194,7 +198,7 @@ class TestPing: issue = _make_issue() assert ping(issue, dry_run=False) is True issue.create_comment.assert_called_once() - issue.add_to_labels.assert_called_once_with(LABEL) + issue.add_to_labels.assert_called_once_with(PINGED_LABEL) @patch("stale_issue_pr_ping.time.sleep") def test_retry_on_failure(self, mock_sleep):