Make duplicate bot less aggressive (#2981)

This commit is contained in:
Jeremiah Lowin 2026-01-22 09:22:12 -05:00 committed by GitHub
commit 3745810db2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 113 additions and 73 deletions

View file

@ -47,7 +47,7 @@ jobs:
4. Next, consider the results from steps 2 and 3 and filter out false positives that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed.
5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates). If there are no duplicates, DO NOT COMMENT. Just exit.
5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates). If there are no duplicates, DO NOT COMMENT. Just exit. After commenting, add the `potential-duplicate` label to the issue using `gh issue edit`.
Notes for your agents:
- Use `gh` to interact with GitHub, rather than web fetch
@ -85,7 +85,7 @@ jobs:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }}
allowed_non_write_users: "*"
claude_args: |
--allowedTools Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh api:*),Bash(gh issue comment:*),Task
--allowedTools Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh api:*),Bash(gh issue comment:*),Bash(gh issue edit:*),Task
settings: |
{
"model": "claude-sonnet-4-5-20250929",

View file

@ -65,34 +65,38 @@ class GitHubClient:
}
self.base_url = f"https://api.github.com/repos/{owner}/{repo}"
def get_open_issues(
self, created_before: datetime, page: int = 1, per_page: int = 100
) -> list[Issue]:
"""Fetch open issues created before a certain date."""
def get_potential_duplicate_issues(self) -> list[Issue]:
"""Fetch open issues with the potential-duplicate label."""
url = f"{self.base_url}/issues"
issues = []
with httpx.Client() as client:
response = client.get(
url,
headers=self.headers,
params={"state": "open", "per_page": per_page, "page": page},
)
if response.status_code != 200:
print(f"Error fetching issues: {response.status_code}")
return issues
data = response.json()
for item in data:
# Skip pull requests
if "pull_request" in item:
continue
created_at = datetime.fromisoformat(
item["created_at"].replace("Z", "+00:00")
page = 1
while page <= 10: # Safety limit
response = client.get(
url,
headers=self.headers,
params={
"state": "open",
"labels": "potential-duplicate",
"per_page": 100,
"page": page,
},
)
if created_at <= created_before:
if response.status_code != 200:
print(f"Error fetching issues: {response.status_code}")
break
data = response.json()
if not data:
break
for item in data:
# Skip pull requests
if "pull_request" in item:
continue
issues.append(
Issue(
number=item["number"],
@ -104,6 +108,8 @@ class GitHubClient:
)
)
page += 1
return issues
def get_issue_comments(self, issue_number: int) -> list[Comment]:
@ -168,6 +174,13 @@ class GitHubClient:
return reactions
def remove_label(self, issue_number: int, label: str) -> bool:
"""Remove a label from an issue."""
url = f"{self.base_url}/issues/{issue_number}/labels/{label}"
with httpx.Client() as client:
response = client.delete(url, headers=self.headers)
return response.status_code in [200, 204]
def close_issue(self, issue_number: int, comment: str) -> bool:
"""Close an issue with a comment and add duplicate label."""
# First add the comment
@ -181,7 +194,9 @@ class GitHubClient:
print(f"Failed to add comment to issue #{issue_number}")
return False
# Add the duplicate label
# Swap labels: remove potential-duplicate, add duplicate
self.remove_label(issue_number, "potential-duplicate")
labels_url = f"{self.base_url}/issues/{issue_number}/labels"
with httpx.Client() as client:
response = client.post(
@ -213,22 +228,36 @@ def find_duplicate_comment(comments: list[Comment]) -> Comment | None:
return None
def should_close_as_duplicate(
def was_already_auto_closed(comments: list[Comment]) -> bool:
"""Check if this issue was already auto-closed once."""
for comment in comments:
if (
comment.user_type == "Bot"
and "closing this issue as a duplicate" in comment.body.lower()
):
return True
return False
def is_past_cooldown(duplicate_comment: Comment) -> bool:
"""Check if the 3-day cooldown period has passed."""
comment_date = datetime.fromisoformat(
duplicate_comment.created_at.replace("Z", "+00:00")
)
three_days_ago = datetime.now(timezone.utc) - timedelta(days=3)
return comment_date <= three_days_ago
def has_human_activity(
issue: Issue,
duplicate_comment: Comment,
all_comments: list[Comment],
reactions: list[Reaction],
) -> bool:
"""Determine if an issue should be closed as duplicate."""
# Check if comment is old enough (3 days)
"""Check if there's human activity that should prevent auto-closure."""
comment_date = datetime.fromisoformat(
duplicate_comment.created_at.replace("Z", "+00:00")
)
three_days_ago = datetime.now(timezone.utc) - timedelta(days=3)
if comment_date > three_days_ago:
return False
# Check for preventing reactions (thumbs down)
for reaction in reactions:
@ -236,22 +265,21 @@ def should_close_as_duplicate(
print(
f"Issue #{issue.number}: Has preventing reaction from {reaction.user_login}"
)
return False
return True
# Check for user activity after the duplicate comment
# Check for any human comment after the duplicate marking
for comment in all_comments:
comment_date_check = datetime.fromisoformat(
comment.created_at.replace("Z", "+00:00")
)
if comment_date_check > comment_date:
# Issue author commented after duplicate marking
if comment.user_id == issue.user_id:
if comment.user_type != "Bot":
print(
f"Issue #{issue.number}: Author commented after duplicate marking"
f"Issue #{issue.number}: {comment.user_login} commented after duplicate marking"
)
return False
return True
return True
return False
def main():
@ -271,60 +299,72 @@ def main():
# Initialize client
client = GitHubClient(token, owner, repo)
# Get issues created more than 3 days ago
three_days_ago = datetime.now(timezone.utc) - timedelta(days=3)
# Only fetch issues with the potential-duplicate label
all_issues = client.get_potential_duplicate_issues()
all_issues = []
page = 1
print(f"[DEBUG] Found {len(all_issues)} open issues with potential-duplicate label")
while page <= 20: # Safety limit
issues = client.get_open_issues(three_days_ago, page=page)
if not issues:
break
all_issues.extend(issues)
page += 1
print(f"[DEBUG] Found {len(all_issues)} open issues created more than 3 days ago")
processed_count = 0
closed_count = 0
cleared_count = 0
for issue in all_issues:
processed_count += 1
if processed_count % 10 == 0:
print(f"[DEBUG] Processed {processed_count}/{len(all_issues)} issues")
# Get comments for this issue
comments = client.get_issue_comments(issue.number)
# Look for duplicate marking comment
duplicate_comment = find_duplicate_comment(comments)
if not duplicate_comment:
# Label exists but no bot comment - clean up the label
print(f"[DEBUG] Issue #{issue.number} has label but no duplicate comment")
client.remove_label(issue.number, "potential-duplicate")
cleared_count += 1
continue
# Skip if already auto-closed once (someone reopened it intentionally)
if was_already_auto_closed(comments):
print(
f"[DEBUG] Issue #{issue.number} was already auto-closed, removing label"
)
client.remove_label(issue.number, "potential-duplicate")
cleared_count += 1
continue
print(f"[DEBUG] Issue #{issue.number} has duplicate comment")
# Still in cooldown period - skip for now, check again later
if not is_past_cooldown(duplicate_comment):
print(f"[DEBUG] Issue #{issue.number} still in 3-day cooldown period")
continue
# Get reactions on the duplicate comment
reactions = client.get_comment_reactions(issue.number, duplicate_comment.id)
# Check if we should close
if should_close_as_duplicate(issue, duplicate_comment, comments, reactions):
close_message = (
"Closing this issue as a duplicate based on the automated analysis above.\n\n"
"The duplicate issues identified contain existing discussions and potential solutions. "
"Please add your 👍 to those issues if they match your use case.\n\n"
"If this was closed in error, please leave a comment explaining why this is not "
"a duplicate and we'll reopen it."
)
# Check for human activity that prevents closure
if has_human_activity(issue, duplicate_comment, comments, reactions):
print(f"[DEBUG] Issue #{issue.number} has human activity, removing label")
client.remove_label(issue.number, "potential-duplicate")
cleared_count += 1
continue
if client.close_issue(issue.number, close_message):
print(f"[SUCCESS] Closed issue #{issue.number} as duplicate")
closed_count += 1
else:
print(f"[ERROR] Failed to close issue #{issue.number}")
# No human activity after cooldown - close as duplicate
close_message = (
"Closing this issue as a duplicate based on the automated analysis above.\n\n"
"The duplicate issues identified contain existing discussions and potential solutions. "
"Please add your 👍 to those issues if they match your use case.\n\n"
"If this was closed in error, please leave a comment explaining why this is not "
"a duplicate and we'll reopen it."
)
print(f"[DEBUG] Processing complete. Closed {closed_count} duplicate issues")
if client.close_issue(issue.number, close_message):
print(f"[SUCCESS] Closed issue #{issue.number} as duplicate")
closed_count += 1
else:
print(f"[ERROR] Failed to close issue #{issue.number}")
print(
f"[DEBUG] Processing complete. Closed {closed_count} duplicates, "
f"cleared {cleared_count} from review"
)
if __name__ == "__main__":