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 { } ;
0 commit comments