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