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

Extract auto-close duplicates script to TypeScript with BunBrowse files
- Move GitHub workflow script logic to scripts/auto-close-duplicates.ts
- Convert from inline JavaScript to standalone TypeScript module
- Update workflow to use Bun instead of Node.js for better performance
- Add proper TypeScript types and ES module syntax

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Boris Cherny authored on Aug 7, 2025 parent 3262b67 · commit b7116c78d8a4
2 changed files+199 -64compared with 3262b67
M.github/workflows/auto-close-duplicates.yml
Ascripts/auto-close-duplicates.ts
@@ -2,7 +2,8 @@
22 description: Dry run - logs issues that would be auto-closed as duplicates after 3 days if no response
33 on:
44 schedule:
5- - cron: '0 9 * * *'
5+ - cron: "0 9 * * *"
6+ workflow_dispatch:
67
78 jobs:
89 auto-close-duplicates:
@@ -10,69 +11,20 @@
1011 timeout-minutes: 10
1112 permissions:
1213 contents: read
13- issues: write
14+ issues: read
1415
1516 steps:
16- - name: Auto-close duplicate issues
17- uses: actions/github-script@v7
17+ - name: Checkout repository
18+ uses: actions/checkout@v4
19+
20+ - name: Setup Bun
21+ uses: oven-sh/setup-bun@v1
1822 with:
19- script: |
20- const threeDaysAgo = new Date();
21- threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
22-
23- // Get all open issues
24- const { data: issues } = await github.rest.issues.listForRepo({
25- owner: context.repo.owner,
26- repo: context.repo.repo,
27- state: 'open',
28- per_page: 100
29- });
30-
31- for (const issue of issues) {
32- // Get comments for this issue
33- const { data: comments } = await github.rest.issues.listComments({
34- owner: context.repo.owner,
35- repo: context.repo.repo,
36- issue_number: issue.number
37- });
38-
39- // Find duplicate comments (look for "Found 1 possible duplicate" or "Found X possible duplicate")
40- const dupeComments = comments.filter(comment =>
41- comment.body.includes('Found') &&
42- comment.body.includes('possible duplicate') &&
43- comment.user.type === 'Bot'
44- );
45-
46- if (dupeComments.length === 0) continue;
47-
48- // Get the most recent duplicate comment
49- const lastDupeComment = dupeComments[dupeComments.length - 1];
50- const dupeCommentDate = new Date(lastDupeComment.created_at);
51-
52- // Check if the duplicate comment is 3+ days old
53- if (dupeCommentDate > threeDaysAgo) continue;
54-
55- // Check if there are any comments after the duplicate comment
56- const commentsAfterDupe = comments.filter(comment =>
57- new Date(comment.created_at) > dupeCommentDate
58- );
59-
60- if (commentsAfterDupe.length > 0) continue;
61-
62- // Check if issue author reacted with thumbs down to the duplicate comment
63- const { data: reactions } = await github.rest.reactions.listForIssueComment({
64- owner: context.repo.owner,
65- repo: context.repo.repo,
66- comment_id: lastDupeComment.id
67- });
68-
69- const authorThumbsDown = reactions.some(reaction =>
70- reaction.user.id === issue.user.id && reaction.content === '-1'
71- );
72-
73- if (authorThumbsDown) continue;
74-
75- // DRY RUN: Log the issue that would be auto-closed
76- const issueUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/issues/${issue.number}`;
77- console.log(`[DRY RUN] Would auto-close issue #${issue.number} as duplicate: ${issueUrl}`);
78- }
23+ bun-version: latest
24+
25+ - name: Auto-close duplicate issues
26+ run: bun run scripts/auto-close-duplicates.ts
27+ env:
28+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29+ GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
30+ GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}
@@ -1,0 +1,183 @@
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+ user: { id: number };
13+}
14+
15+interface GitHubComment {
16+ id: number;
17+ body: string;
18+ created_at: string;
19+ user: { type: string; id: number };
20+}
21+
22+interface GitHubReaction {
23+ user: { id: number };
24+ content: string;
25+}
26+
27+async 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+
45+async 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+
180+autoCloseDuplicates().catch(console.error);
181+
182+// Make it a module
183+export {};