Skip to content

Commit d7aa61e

Browse files
bchernyClaude
andcommitted
Add script to backfill duplicate comments for old issues
Creates a script that identifies old issues without duplicate detection comments and triggers the existing claude-dedupe-issues workflow for each one. This helps ensure historical issues get proper duplicate detection coverage. Features: - Scans issues from configurable time period (default 30 days) - Skips issues that already have duplicate detection comments - Triggers existing workflow instead of duplicating logic - Includes dry-run mode and rate limiting for safety 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 072856d commit d7aa61e

1 file changed

Lines changed: 198 additions & 0 deletions

File tree

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/usr/bin/env bun
2+
3+
declare global {
4+
var process: {
5+
env: Record<string, string | undefined>;
6+
};
7+
}
8+
9+
interface GitHubIssue {
10+
number: number;
11+
title: string;
12+
state: string;
13+
state_reason?: string;
14+
user: { id: number };
15+
created_at: string;
16+
closed_at?: string;
17+
}
18+
19+
interface GitHubComment {
20+
id: number;
21+
body: string;
22+
created_at: string;
23+
user: { type: string; id: number };
24+
}
25+
26+
async function githubRequest<T>(endpoint: string, token: string, method: string = 'GET', body?: any): Promise<T> {
27+
const response = await fetch(`https://api.github.com${endpoint}`, {
28+
method,
29+
headers: {
30+
Authorization: `Bearer ${token}`,
31+
Accept: "application/vnd.github.v3+json",
32+
"User-Agent": "backfill-duplicate-comments-script",
33+
...(body && { "Content-Type": "application/json" }),
34+
},
35+
...(body && { body: JSON.stringify(body) }),
36+
});
37+
38+
if (!response.ok) {
39+
throw new Error(
40+
`GitHub API request failed: ${response.status} ${response.statusText}`
41+
);
42+
}
43+
44+
return response.json();
45+
}
46+
47+
async function triggerDedupeWorkflow(
48+
owner: string,
49+
repo: string,
50+
issueNumber: number,
51+
token: string,
52+
dryRun: boolean = true
53+
): Promise<void> {
54+
if (dryRun) {
55+
console.log(`[DRY RUN] Would trigger dedupe workflow for issue #${issueNumber}`);
56+
return;
57+
}
58+
59+
await githubRequest(
60+
`/repos/${owner}/${repo}/actions/workflows/claude-dedupe-issues.yml/dispatches`,
61+
token,
62+
'POST',
63+
{
64+
ref: 'main',
65+
inputs: {
66+
issue_number: issueNumber.toString()
67+
}
68+
}
69+
);
70+
}
71+
72+
async function backfillDuplicateComments(): Promise<void> {
73+
console.log("[DEBUG] Starting backfill duplicate comments script");
74+
75+
const token = process.env.GITHUB_TOKEN;
76+
if (!token) {
77+
throw new Error(`GITHUB_TOKEN environment variable is required
78+
79+
Usage:
80+
GITHUB_TOKEN=your_token bun run scripts/backfill-duplicate-comments.ts
81+
82+
Environment Variables:
83+
GITHUB_TOKEN - GitHub personal access token with repo and actions permissions (required)
84+
DRY_RUN - Set to "false" to actually trigger workflows (default: true for safety)
85+
DAYS_BACK - How many days back to look for old issues (default: 90)`);
86+
}
87+
console.log("[DEBUG] GitHub token found");
88+
89+
const owner = "anthropics";
90+
const repo = "claude-code";
91+
const dryRun = process.env.DRY_RUN !== "false";
92+
const daysBack = parseInt(process.env.DAYS_BACK || "90", 10);
93+
94+
console.log(`[DEBUG] Repository: ${owner}/${repo}`);
95+
console.log(`[DEBUG] Dry run mode: ${dryRun}`);
96+
console.log(`[DEBUG] Looking back ${daysBack} days`);
97+
98+
const cutoffDate = new Date();
99+
cutoffDate.setDate(cutoffDate.getDate() - daysBack);
100+
101+
console.log(`[DEBUG] Fetching issues created since ${cutoffDate.toISOString()}...`);
102+
const allIssues: GitHubIssue[] = [];
103+
let page = 1;
104+
const perPage = 100;
105+
106+
while (true) {
107+
const pageIssues: GitHubIssue[] = await githubRequest(
108+
`/repos/${owner}/${repo}/issues?state=all&per_page=${perPage}&page=${page}&since=${cutoffDate.toISOString()}`,
109+
token
110+
);
111+
112+
if (pageIssues.length === 0) break;
113+
114+
allIssues.push(...pageIssues);
115+
page++;
116+
117+
// Safety limit to avoid infinite loops
118+
if (page > 100) {
119+
console.log("[DEBUG] Reached page limit, stopping pagination");
120+
break;
121+
}
122+
}
123+
124+
console.log(`[DEBUG] Found ${allIssues.length} issues from the last ${daysBack} days`);
125+
126+
let processedCount = 0;
127+
let candidateCount = 0;
128+
let triggeredCount = 0;
129+
130+
for (const issue of allIssues) {
131+
processedCount++;
132+
console.log(
133+
`[DEBUG] Processing issue #${issue.number} (${processedCount}/${allIssues.length}): ${issue.title}`
134+
);
135+
136+
console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
137+
const comments: GitHubComment[] = await githubRequest(
138+
`/repos/${owner}/${repo}/issues/${issue.number}/comments`,
139+
token
140+
);
141+
console.log(
142+
`[DEBUG] Issue #${issue.number} has ${comments.length} comments`
143+
);
144+
145+
// Look for existing duplicate detection comments (from the dedupe bot)
146+
const dupeDetectionComments = comments.filter(
147+
(comment) =>
148+
comment.body.includes("Found") &&
149+
comment.body.includes("possible duplicate") &&
150+
comment.user.type === "Bot"
151+
);
152+
153+
console.log(
154+
`[DEBUG] Issue #${issue.number} has ${dupeDetectionComments.length} duplicate detection comments`
155+
);
156+
157+
// Skip if there's already a duplicate detection comment
158+
if (dupeDetectionComments.length > 0) {
159+
console.log(
160+
`[DEBUG] Issue #${issue.number} already has duplicate detection comment, skipping`
161+
);
162+
continue;
163+
}
164+
165+
candidateCount++;
166+
const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
167+
168+
try {
169+
console.log(
170+
`[INFO] ${dryRun ? '[DRY RUN] ' : ''}Triggering dedupe workflow for issue #${issue.number}: ${issueUrl}`
171+
);
172+
await triggerDedupeWorkflow(owner, repo, issue.number, token, dryRun);
173+
174+
if (!dryRun) {
175+
console.log(
176+
`[SUCCESS] Successfully triggered dedupe workflow for issue #${issue.number}`
177+
);
178+
}
179+
triggeredCount++;
180+
} catch (error) {
181+
console.error(
182+
`[ERROR] Failed to trigger workflow for issue #${issue.number}: ${error}`
183+
);
184+
}
185+
186+
// Add a delay between workflow triggers to avoid overwhelming the system
187+
await new Promise(resolve => setTimeout(resolve, 1000));
188+
}
189+
190+
console.log(
191+
`[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates without duplicate comments, ${dryRun ? 'would trigger' : 'triggered'} ${triggeredCount} workflows`
192+
);
193+
}
194+
195+
backfillDuplicateComments().catch(console.error);
196+
197+
// Make it a module
198+
export {};

0 commit comments

Comments
 (0)