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 / auto-close-duplicates.ts
205 lines · 5.6 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 user: { id: number };
13 created_at: string;
14}
15
16interface GitHubComment {
17 id: number;
18 body: string;
19 created_at: string;
20 user: { type: string; id: number };
21}
22
23interface GitHubReaction {
24 user: { id: number };
25 content: string;
26}
27
28async function githubRequest<T>(endpoint: string, token: string): Promise<T> {
29 const response = await fetch(`https://api.github.com${endpoint}`, {
30 headers: {
31 Authorization: `Bearer ${token}`,
32 Accept: "application/vnd.github.v3+json",
33 "User-Agent": "auto-close-duplicates-script",
34 },
35 });
36
37 if (!response.ok) {
38 throw new Error(
39 `GitHub API request failed: ${response.status} ${response.statusText}`
40 );
41 }
42
43 return response.json();
44}
45
46async function autoCloseDuplicates(): Promise<void> {
47 console.log("[DEBUG] Starting auto-close duplicates script");
48
49 const token = process.env.GITHUB_TOKEN;
50 if (!token) {
51 throw new Error("GITHUB_TOKEN environment variable is required");
52 }
53 console.log("[DEBUG] GitHub token found");
54
55 const owner = process.env.GITHUB_REPOSITORY_OWNER || "anthropics";
56 const repo = process.env.GITHUB_REPOSITORY_NAME || "claude-code";
57 console.log(`[DEBUG] Repository: ${owner}/${repo}`);
58
59 const threeDaysAgo = new Date();
60 threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
61 console.log(
62 `[DEBUG] Checking for duplicate comments older than: ${threeDaysAgo.toISOString()}`
63 );
64
65 console.log("[DEBUG] Fetching open issues created more than 3 days ago...");
66 const allIssues: GitHubIssue[] = [];
67 let page = 1;
68 const perPage = 100;
69
70 while (true) {
71 const pageIssues: GitHubIssue[] = await githubRequest(
72 `/repos/${owner}/${repo}/issues?state=open&per_page=${perPage}&page=${page}`,
73 token
74 );
75
76 if (pageIssues.length === 0) break;
77
78 // Filter for issues created more than 3 days ago
79 const oldEnoughIssues = pageIssues.filter(issue =>
80 new Date(issue.created_at) <= threeDaysAgo
81 );
82
83 allIssues.push(...oldEnoughIssues);
84 page++;
85
86 // Safety limit to avoid infinite loops
87 if (page > 20) break;
88 }
89
90 const issues = allIssues;
91 console.log(`[DEBUG] Found ${issues.length} open issues`);
92
93 let processedCount = 0;
94 let candidateCount = 0;
95
96 for (const issue of issues) {
97 processedCount++;
98 console.log(
99 `[DEBUG] Processing issue #${issue.number} (${processedCount}/${issues.length}): ${issue.title}`
100 );
101
102 console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
103 const comments: GitHubComment[] = await githubRequest(
104 `/repos/${owner}/${repo}/issues/${issue.number}/comments`,
105 token
106 );
107 console.log(
108 `[DEBUG] Issue #${issue.number} has ${comments.length} comments`
109 );
110
111 const dupeComments = comments.filter(
112 (comment) =>
113 comment.body.includes("Found") &&
114 comment.body.includes("possible duplicate") &&
115 comment.user.type === "Bot"
116 );
117 console.log(
118 `[DEBUG] Issue #${issue.number} has ${dupeComments.length} duplicate detection comments`
119 );
120
121 if (dupeComments.length === 0) {
122 console.log(
123 `[DEBUG] Issue #${issue.number} - no duplicate comments found, skipping`
124 );
125 continue;
126 }
127
128 const lastDupeComment = dupeComments[dupeComments.length - 1];
129 const dupeCommentDate = new Date(lastDupeComment.created_at);
130 console.log(
131 `[DEBUG] Issue #${
132 issue.number
133 } - most recent duplicate comment from: ${dupeCommentDate.toISOString()}`
134 );
135
136 if (dupeCommentDate > threeDaysAgo) {
137 console.log(
138 `[DEBUG] Issue #${issue.number} - duplicate comment is too recent, skipping`
139 );
140 continue;
141 }
142 console.log(
143 `[DEBUG] Issue #${
144 issue.number
145 } - duplicate comment is old enough (${Math.floor(
146 (Date.now() - dupeCommentDate.getTime()) / (1000 * 60 * 60 * 24)
147 )} days)`
148 );
149
150 const commentsAfterDupe = comments.filter(
151 (comment) => new Date(comment.created_at) > dupeCommentDate
152 );
153 console.log(
154 `[DEBUG] Issue #${issue.number} - ${commentsAfterDupe.length} comments after duplicate detection`
155 );
156
157 if (commentsAfterDupe.length > 0) {
158 console.log(
159 `[DEBUG] Issue #${issue.number} - has activity after duplicate comment, skipping`
160 );
161 continue;
162 }
163
164 console.log(
165 `[DEBUG] Issue #${issue.number} - checking reactions on duplicate comment...`
166 );
167 const reactions: GitHubReaction[] = await githubRequest(
168 `/repos/${owner}/${repo}/issues/comments/${lastDupeComment.id}/reactions`,
169 token
170 );
171 console.log(
172 `[DEBUG] Issue #${issue.number} - duplicate comment has ${reactions.length} reactions`
173 );
174
175 const authorThumbsDown = reactions.some(
176 (reaction) =>
177 reaction.user.id === issue.user.id && reaction.content === "-1"
178 );
179 console.log(
180 `[DEBUG] Issue #${issue.number} - author thumbs down reaction: ${authorThumbsDown}`
181 );
182
183 if (authorThumbsDown) {
184 console.log(
185 `[DEBUG] Issue #${issue.number} - author disagreed with duplicate detection, skipping`
186 );
187 continue;
188 }
189
190 candidateCount++;
191 const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
192 console.log(
193 `[DRY RUN] Would auto-close issue #${issue.number} as duplicate: ${issueUrl}`
194 );
195 }
196
197 console.log(
198 `[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates for auto-close`
199 );
200}
201
202autoCloseDuplicates().catch(console.error);
203
204// Make it a module
205export {};