spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

claude-code / scripts / backfill-duplicate-comments.ts
213 lines · 6.7 KBRaw
1#!/usr/bin/env bun
2
3declare global {
4 var process: {
5 env: Record<string, string | undefined>;
6 };
7}
8
9interface 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
19interface GitHubComment {
20 id: number;
21 body: string;
22 created_at: string;
23 user: { type: string; id: number };
24}
25
26async 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
47async 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
72async 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
79Usage:
80 GITHUB_TOKEN=your_token bun run scripts/backfill-duplicate-comments.ts
81
82Environment 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 MAX_ISSUE_NUMBER - Only process issues with numbers less than this value (default: 4050)`);
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 maxIssueNumber = parseInt(process.env.MAX_ISSUE_NUMBER || "4050", 10);
93 const minIssueNumber = parseInt(process.env.MIN_ISSUE_NUMBER || "1", 10);
94
95 console.log(`[DEBUG] Repository: ${owner}/${repo}`);
96 console.log(`[DEBUG] Dry run mode: ${dryRun}`);
97 console.log(`[DEBUG] Looking at issues between #${minIssueNumber} and #${maxIssueNumber}`);
98
99 console.log(`[DEBUG] Fetching issues between #${minIssueNumber} and #${maxIssueNumber}...`);
100 const allIssues: GitHubIssue[] = [];
101 let page = 1;
102 const perPage = 100;
103
104 while (true) {
105 const pageIssues: GitHubIssue[] = await githubRequest(
106 `/repos/${owner}/${repo}/issues?state=all&per_page=${perPage}&page=${page}&sort=created&direction=desc`,
107 token
108 );
109
110 if (pageIssues.length === 0) break;
111
112 // Filter to only include issues within the specified range
113 const filteredIssues = pageIssues.filter(issue =>
114 issue.number >= minIssueNumber && issue.number < maxIssueNumber
115 );
116 allIssues.push(...filteredIssues);
117
118 // If the oldest issue in this page is still above our minimum, we need to continue
119 // but if the oldest issue is below our minimum, we can stop
120 const oldestIssueInPage = pageIssues[pageIssues.length - 1];
121 if (oldestIssueInPage && oldestIssueInPage.number >= maxIssueNumber) {
122 console.log(`[DEBUG] Oldest issue in page #${page} is #${oldestIssueInPage.number}, continuing...`);
123 } else if (oldestIssueInPage && oldestIssueInPage.number < minIssueNumber) {
124 console.log(`[DEBUG] Oldest issue in page #${page} is #${oldestIssueInPage.number}, below minimum, stopping`);
125 break;
126 } else if (filteredIssues.length === 0 && pageIssues.length > 0) {
127 console.log(`[DEBUG] No issues in page #${page} are in range #${minIssueNumber}-#${maxIssueNumber}, continuing...`);
128 }
129
130 page++;
131
132 // Safety limit to avoid infinite loops
133 if (page > 200) {
134 console.log("[DEBUG] Reached page limit, stopping pagination");
135 break;
136 }
137 }
138
139 console.log(`[DEBUG] Found ${allIssues.length} issues between #${minIssueNumber} and #${maxIssueNumber}`);
140
141 let processedCount = 0;
142 let candidateCount = 0;
143 let triggeredCount = 0;
144
145 for (const issue of allIssues) {
146 processedCount++;
147 console.log(
148 `[DEBUG] Processing issue #${issue.number} (${processedCount}/${allIssues.length}): ${issue.title}`
149 );
150
151 console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
152 const comments: GitHubComment[] = await githubRequest(
153 `/repos/${owner}/${repo}/issues/${issue.number}/comments`,
154 token
155 );
156 console.log(
157 `[DEBUG] Issue #${issue.number} has ${comments.length} comments`
158 );
159
160 // Look for existing duplicate detection comments (from the dedupe bot)
161 const dupeDetectionComments = comments.filter(
162 (comment) =>
163 comment.body.includes("Found") &&
164 comment.body.includes("possible duplicate") &&
165 comment.user.type === "Bot"
166 );
167
168 console.log(
169 `[DEBUG] Issue #${issue.number} has ${dupeDetectionComments.length} duplicate detection comments`
170 );
171
172 // Skip if there's already a duplicate detection comment
173 if (dupeDetectionComments.length > 0) {
174 console.log(
175 `[DEBUG] Issue #${issue.number} already has duplicate detection comment, skipping`
176 );
177 continue;
178 }
179
180 candidateCount++;
181 const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
182
183 try {
184 console.log(
185 `[INFO] ${dryRun ? '[DRY RUN] ' : ''}Triggering dedupe workflow for issue #${issue.number}: ${issueUrl}`
186 );
187 await triggerDedupeWorkflow(owner, repo, issue.number, token, dryRun);
188
189 if (!dryRun) {
190 console.log(
191 `[SUCCESS] Successfully triggered dedupe workflow for issue #${issue.number}`
192 );
193 }
194 triggeredCount++;
195 } catch (error) {
196 console.error(
197 `[ERROR] Failed to trigger workflow for issue #${issue.number}: ${error}`
198 );
199 }
200
201 // Add a delay between workflow triggers to avoid overwhelming the system
202 await new Promise(resolve => setTimeout(resolve, 1000));
203 }
204
205 console.log(
206 `[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates without duplicate comments, ${dryRun ? 'would trigger' : 'triggered'} ${triggeredCount} workflows`
207 );
208}
209
210backfillDuplicateComments().catch(console.error);
211
212// Make it a module
213export {};