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
| 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 | state: string; |
| 13 | state_reason?: string; |
| 14 | user: { id: number }; |
| 15 | created_at: string; |
| 16 | closed_at?: string; |
| 17 | } |
| 18 | |
| 19 | interface GitHubComment { |
| 20 | id: number; |
| 21 | body: string; |
| 22 | created_at: string; |
| 23 | user: { type: string; id: number }; |
| 24 | } |
| 25 | |
| 26 | async 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 | |
| 47 | async 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 | |
| 72 | async 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 | |
| 79 | Usage: |
| 80 | GITHUB_TOKEN=your_token bun run scripts/backfill-duplicate-comments.ts |
| 81 | |
| 82 | Environment 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 | DAYS_BACK - How many days back to look for old issues (default: 90)`); |
| 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 daysBack = parseInt(process.env.DAYS_BACK || "90", 10); |
| 93 | |
| 94 | console.log(`[DEBUG] Repository: ${owner}/${repo}`); |
| 95 | console.log(`[DEBUG] Dry run mode: ${dryRun}`); |
| 96 | console.log(`[DEBUG] Looking back ${daysBack} days`); |
| 97 | |
| 98 | const cutoffDate = new Date(); |
| 99 | cutoffDate.setDate(cutoffDate.getDate() - daysBack); |
| 100 | |
| 101 | console.log(`[DEBUG] Fetching issues created since ${cutoffDate.toISOString()}...`); |
| 102 | const allIssues: GitHubIssue[] = []; |
| 103 | let page = 1; |
| 104 | const perPage = 100; |
| 105 | |
| 106 | while (true) { |
| 107 | const pageIssues: GitHubIssue[] = await githubRequest( |
| 108 | `/repos/${owner}/${repo}/issues?state=all&per_page=${perPage}&page=${page}&since=${cutoffDate.toISOString()}`, |
| 109 | token |
| 110 | ); |
| 111 | |
| 112 | if (pageIssues.length === 0) break; |
| 113 | |
| 114 | allIssues.push(...pageIssues); |
| 115 | page++; |
| 116 | |
| 117 | // Safety limit to avoid infinite loops |
| 118 | if (page > 100) { |
| 119 | console.log("[DEBUG] Reached page limit, stopping pagination"); |
| 120 | break; |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | console.log(`[DEBUG] Found ${allIssues.length} issues from the last ${daysBack} days`); |
| 125 | |
| 126 | let processedCount = 0; |
| 127 | let candidateCount = 0; |
| 128 | let triggeredCount = 0; |
| 129 | |
| 130 | for (const issue of allIssues) { |
| 131 | processedCount++; |
| 132 | console.log( |
| 133 | `[DEBUG] Processing issue #${issue.number} (${processedCount}/${allIssues.length}): ${issue.title}` |
| 134 | ); |
| 135 | |
| 136 | console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`); |
| 137 | const comments: GitHubComment[] = await githubRequest( |
| 138 | `/repos/${owner}/${repo}/issues/${issue.number}/comments`, |
| 139 | token |
| 140 | ); |
| 141 | console.log( |
| 142 | `[DEBUG] Issue #${issue.number} has ${comments.length} comments` |
| 143 | ); |
| 144 | |
| 145 | // Look for existing duplicate detection comments (from the dedupe bot) |
| 146 | const dupeDetectionComments = comments.filter( |
| 147 | (comment) => |
| 148 | comment.body.includes("Found") && |
| 149 | comment.body.includes("possible duplicate") && |
| 150 | comment.user.type === "Bot" |
| 151 | ); |
| 152 | |
| 153 | console.log( |
| 154 | `[DEBUG] Issue #${issue.number} has ${dupeDetectionComments.length} duplicate detection comments` |
| 155 | ); |
| 156 | |
| 157 | // Skip if there's already a duplicate detection comment |
| 158 | if (dupeDetectionComments.length > 0) { |
| 159 | console.log( |
| 160 | `[DEBUG] Issue #${issue.number} already has duplicate detection comment, skipping` |
| 161 | ); |
| 162 | continue; |
| 163 | } |
| 164 | |
| 165 | candidateCount++; |
| 166 | const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`; |
| 167 | |
| 168 | try { |
| 169 | console.log( |
| 170 | `[INFO] ${dryRun ? '[DRY RUN] ' : ''}Triggering dedupe workflow for issue #${issue.number}: ${issueUrl}` |
| 171 | ); |
| 172 | await triggerDedupeWorkflow(owner, repo, issue.number, token, dryRun); |
| 173 | |
| 174 | if (!dryRun) { |
| 175 | console.log( |
| 176 | `[SUCCESS] Successfully triggered dedupe workflow for issue #${issue.number}` |
| 177 | ); |
| 178 | } |
| 179 | triggeredCount++; |
| 180 | } catch (error) { |
| 181 | console.error( |
| 182 | `[ERROR] Failed to trigger workflow for issue #${issue.number}: ${error}` |
| 183 | ); |
| 184 | } |
| 185 | |
| 186 | // Add a delay between workflow triggers to avoid overwhelming the system |
| 187 | await new Promise(resolve => setTimeout(resolve, 1000)); |
| 188 | } |
| 189 | |
| 190 | console.log( |
| 191 | `[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates without duplicate comments, ${dryRun ? 'would trigger' : 'triggered'} ${triggeredCount} workflows` |
| 192 | ); |
| 193 | } |
| 194 | |
| 195 | backfillDuplicateComments().catch(console.error); |
| 196 | |
| 197 | // Make it a module |
| 198 | export {}; |