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
- Remove dry run mode and implement actual issue closing - Extract duplicate issue number from bot comments - Close issues via GitHub API with proper state and comments - Add error handling for API failures - Use Claude Code comment format with reopening instructions ๐ค Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
| @@ -28,9 +28,10 @@ | ||
| 28 | 28 | 2. <link to issue> |
| 29 | 29 | 3. <link to issue> |
| 30 | 30 | |
| 31 | -If your issue is a duplicate, please close it and ๐ the existing issue instead. | |
| 31 | +This issue will be automatically closed as a duplicate in 3 days. | |
| 32 | 32 | |
| 33 | -<sub>This issue will be automatically closed as a duplicate in 3 days if there are no additional comments. To prevent auto-closure, please ๐ this comment.</sub> | |
| 33 | +- If your issue is a duplicate, please close it and ๐ the existing issue instead | |
| 34 | +- To prevent auto-closure, add a comment or ๐ this comment | |
| 34 | 35 | |
| 35 | 36 | ๐ค Generated with [Claude Code](https://claude.ai/code) |
| 36 | 37 |
| @@ -25,13 +25,16 @@ | ||
| 25 | 25 | content: string; |
| 26 | 26 | } |
| 27 | 27 | |
| 28 | -async function githubRequest<T>(endpoint: string, token: string): Promise<T> { | |
| 28 | +async function githubRequest<T>(endpoint: string, token: string, method: string = 'GET', body?: any): Promise<T> { | |
| 29 | 29 | const response = await fetch(`https://api.github.com${endpoint}`, { |
| 30 | + method, | |
| 30 | 31 | headers: { |
| 31 | 32 | Authorization: `Bearer ${token}`, |
| 32 | 33 | Accept: "application/vnd.github.v3+json", |
| 33 | 34 | "User-Agent": "auto-close-duplicates-script", |
| 35 | + ...(body && { "Content-Type": "application/json" }), | |
| 34 | 36 | }, |
| 37 | + ...(body && { body: JSON.stringify(body) }), | |
| 35 | 38 | }); |
| 36 | 39 | |
| 37 | 40 | if (!response.ok) { |
| @@ -43,6 +46,42 @@ | ||
| 43 | 46 | return response.json(); |
| 44 | 47 | } |
| 45 | 48 | |
| 49 | +function extractDuplicateIssueNumber(commentBody: string): number | null { | |
| 50 | + const match = commentBody.match(/#(\d+)/); | |
| 51 | + return match ? parseInt(match[1], 10) : null; | |
| 52 | +} | |
| 53 | + | |
| 54 | +async function closeIssueAsDuplicate( | |
| 55 | + owner: string, | |
| 56 | + repo: string, | |
| 57 | + issueNumber: number, | |
| 58 | + duplicateOfNumber: number, | |
| 59 | + token: string | |
| 60 | +): Promise<void> { | |
| 61 | + await githubRequest( | |
| 62 | + `/repos/${owner}/${repo}/issues/${issueNumber}`, | |
| 63 | + token, | |
| 64 | + 'PATCH', | |
| 65 | + { | |
| 66 | + state: 'closed', | |
| 67 | + state_reason: 'not_planned' | |
| 68 | + } | |
| 69 | + ); | |
| 70 | + | |
| 71 | + await githubRequest( | |
| 72 | + `/repos/${owner}/${repo}/issues/${issueNumber}/comments`, | |
| 73 | + token, | |
| 74 | + 'POST', | |
| 75 | + { | |
| 76 | + body: `This issue has been automatically closed as a duplicate of #${duplicateOfNumber}. | |
| 77 | + | |
| 78 | +If this is incorrect, please re-open this issue or create a new one. | |
| 79 | + | |
| 80 | +๐ค Generated with [Claude Code](https://claude.ai/code)` | |
| 81 | + } | |
| 82 | + ); | |
| 83 | +} | |
| 84 | + | |
| 46 | 85 | async function autoCloseDuplicates(): Promise<void> { |
| 47 | 86 | console.log("[DEBUG] Starting auto-close duplicates script"); |
| 48 | 87 | |
| @@ -187,11 +226,30 @@ | ||
| 187 | 226 | continue; |
| 188 | 227 | } |
| 189 | 228 | |
| 229 | + const duplicateIssueNumber = extractDuplicateIssueNumber(lastDupeComment.body); | |
| 230 | + if (!duplicateIssueNumber) { | |
| 231 | + console.log( | |
| 232 | + `[DEBUG] Issue #${issue.number} - could not extract duplicate issue number from comment, skipping` | |
| 233 | + ); | |
| 234 | + continue; | |
| 235 | + } | |
| 236 | + | |
| 190 | 237 | candidateCount++; |
| 191 | 238 | 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 | - ); | |
| 239 | + | |
| 240 | + try { | |
| 241 | + console.log( | |
| 242 | + `[INFO] Auto-closing issue #${issue.number} as duplicate of #${duplicateIssueNumber}: ${issueUrl}` | |
| 243 | + ); | |
| 244 | + await closeIssueAsDuplicate(owner, repo, issue.number, duplicateIssueNumber, token); | |
| 245 | + console.log( | |
| 246 | + `[SUCCESS] Successfully closed issue #${issue.number} as duplicate of #${duplicateIssueNumber}` | |
| 247 | + ); | |
| 248 | + } catch (error) { | |
| 249 | + console.error( | |
| 250 | + `[ERROR] Failed to close issue #${issue.number} as duplicate: ${error}` | |
| 251 | + ); | |
| 252 | + } | |
| 195 | 253 | } |
| 196 | 254 | |
| 197 | 255 | console.log( |