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

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

Co-Authored-By: Claude <noreply@anthropic.com>
inigo authored on Sep 19, 2025 parent c3a32e4 · commit d81efef32491
5 changed files+0 -645compared with c3a32e4
D.github/workflows/auto-close-duplicates.yml
D.github/workflows/backfill-duplicate-comments.yml
D.github/workflows/claude-dedupe-issues.yml
Dscripts/auto-close-duplicates.ts
Dscripts/backfill-duplicate-comments.ts
D.github/workflows/auto-close-duplicates.yml+0 -31
@@ -1,31 +1,0 @@
1-name: Auto-close duplicate issues
2-description: Auto-closes issues that are duplicates of existing issues
3-on:
4- schedule:
5- - cron: "0 9 * * *"
6- workflow_dispatch:
7-
8-jobs:
9- auto-close-duplicates:
10- runs-on: ubuntu-latest
11- timeout-minutes: 10
12- permissions:
13- contents: read
14- issues: write
15-
16- steps:
17- - name: Checkout repository
18- uses: actions/checkout@v4
19-
20- - name: Setup Bun
21- uses: oven-sh/setup-bun@v2
22- with:
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 }}
31- STATSIG_API_KEY: ${{ secrets.STATSIG_API_KEY }}
D.github/workflows/backfill-duplicate-comments.yml+0 -44
@@ -1,44 +1,0 @@
1-name: Backfill Duplicate Comments
2-description: Triggers duplicate detection for old issues that don't have duplicate comments
3-
4-on:
5- workflow_dispatch:
6- inputs:
7- days_back:
8- description: 'How many days back to look for old issues'
9- required: false
10- default: '90'
11- type: string
12- dry_run:
13- description: 'Dry run mode (true to only log what would be done)'
14- required: false
15- default: 'true'
16- type: choice
17- options:
18- - 'true'
19- - 'false'
20-
21-jobs:
22- backfill-duplicate-comments:
23- runs-on: ubuntu-latest
24- timeout-minutes: 30
25- permissions:
26- contents: read
27- issues: read
28- actions: write
29-
30- steps:
31- - name: Checkout repository
32- uses: actions/checkout@v4
33-
34- - name: Setup Bun
35- uses: oven-sh/setup-bun@v2
36- with:
37- bun-version: latest
38-
39- - name: Backfill duplicate comments
40- run: bun run scripts/backfill-duplicate-comments.ts
41- env:
42- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43- DAYS_BACK: ${{ inputs.days_back }}
44- DRY_RUN: ${{ inputs.dry_run }}
D.github/workflows/claude-dedupe-issues.yml+0 -80
@@ -1,80 +1,0 @@
1-name: Claude Issue Dedupe
2-description: Automatically dedupe GitHub issues using Claude Code
3-on:
4- issues:
5- types: [opened]
6- workflow_dispatch:
7- inputs:
8- issue_number:
9- description: 'Issue number to process for duplicate detection'
10- required: true
11- type: string
12-
13-jobs:
14- claude-dedupe-issues:
15- runs-on: ubuntu-latest
16- timeout-minutes: 10
17- permissions:
18- contents: read
19- issues: write
20-
21- steps:
22- - name: Checkout repository
23- uses: actions/checkout@v4
24-
25- - name: Run Claude Code slash command
26- uses: anthropics/claude-code-base-action@beta
27- with:
28- prompt: "/dedupe ${{ github.repository }}/issues/${{ github.event.issue.number || inputs.issue_number }}"
29- anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
30- claude_env: |
31- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32-
33- - name: Log duplicate comment event to Statsig
34- if: always()
35- env:
36- STATSIG_API_KEY: ${{ secrets.STATSIG_API_KEY }}
37- run: |
38- ISSUE_NUMBER=${{ github.event.issue.number || inputs.issue_number }}
39- REPO=${{ github.repository }}
40-
41- if [ -z "$STATSIG_API_KEY" ]; then
42- echo "STATSIG_API_KEY not found, skipping Statsig logging"
43- exit 0
44- fi
45-
46- # Prepare the event payload
47- EVENT_PAYLOAD=$(jq -n \
48- --arg issue_number "$ISSUE_NUMBER" \
49- --arg repo "$REPO" \
50- --arg triggered_by "${{ github.event_name }}" \
51- '{
52- events: [{
53- eventName: "github_duplicate_comment_added",
54- value: 1,
55- metadata: {
56- repository: $repo,
57- issue_number: ($issue_number | tonumber),
58- triggered_by: $triggered_by,
59- workflow_run_id: "${{ github.run_id }}"
60- },
61- time: (now | floor | tostring)
62- }]
63- }')
64-
65- # Send to Statsig API
66- echo "Logging duplicate comment event to Statsig for issue #${ISSUE_NUMBER}"
67-
68- RESPONSE=$(curl -s -w "\n%{http_code}" -X POST https://events.statsigapi.net/v1/log_event \
69- -H "Content-Type: application/json" \
70- -H "STATSIG-API-KEY: ${STATSIG_API_KEY}" \
71- -d "$EVENT_PAYLOAD")
72-
73- HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
74- BODY=$(echo "$RESPONSE" | head -n-1)
75-
76- if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 202 ]; then
77- echo "Successfully logged duplicate comment event for issue #${ISSUE_NUMBER}"
78- else
79- echo "Failed to log duplicate comment event for issue #${ISSUE_NUMBER}. HTTP ${HTTP_CODE}: ${BODY}"
80- fi
Dscripts/auto-close-duplicates.ts+0 -277
@@ -1,277 +1,0 @@
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- created_at: string;
14-}
15-
16-interface GitHubComment {
17- id: number;
18- body: string;
19- created_at: string;
20- user: { type: string; id: number };
21-}
22-
23-interface GitHubReaction {
24- user: { id: number };
25- content: string;
26-}
27-
28-async function githubRequest<T>(endpoint: string, token: string, method: string = 'GET', body?: any): Promise<T> {
29- const response = await fetch(`https://api.github.com${endpoint}`, {
30- method,
31- headers: {
32- Authorization: `Bearer ${token}`,
33- Accept: "application/vnd.github.v3+json",
34- "User-Agent": "auto-close-duplicates-script",
35- ...(body && { "Content-Type": "application/json" }),
36- },
37- ...(body && { body: JSON.stringify(body) }),
38- });
39-
40- if (!response.ok) {
41- throw new Error(
42- `GitHub API request failed: ${response.status} ${response.statusText}`
43- );
44- }
45-
46- return response.json();
47-}
48-
49-function extractDuplicateIssueNumber(commentBody: string): number | null {
50- // Try to match #123 format first
51- let match = commentBody.match(/#(\d+)/);
52- if (match) {
53- return parseInt(match[1], 10);
54- }
55-
56- // Try to match GitHub issue URL format: https://github.com/owner/repo/issues/123
57- match = commentBody.match(/github\.com\/[^\/]+\/[^\/]+\/issues\/(\d+)/);
58- if (match) {
59- return parseInt(match[1], 10);
60- }
61-
62- return null;
63-}
64-
65-
66-async function closeIssueAsDuplicate(
67- owner: string,
68- repo: string,
69- issueNumber: number,
70- duplicateOfNumber: number,
71- token: string
72-): Promise<void> {
73- await githubRequest(
74- `/repos/${owner}/${repo}/issues/${issueNumber}`,
75- token,
76- 'PATCH',
77- {
78- state: 'closed',
79- state_reason: 'duplicate',
80- labels: ['duplicate']
81- }
82- );
83-
84- await githubRequest(
85- `/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
86- token,
87- 'POST',
88- {
89- body: `This issue has been automatically closed as a duplicate of #${duplicateOfNumber}.
90-
91-If this is incorrect, please re-open this issue or create a new one.
92-
93-🤖 Generated with [Claude Code](https://claude.ai/code)`
94- }
95- );
96-
97-}
98-
99-async function autoCloseDuplicates(): Promise<void> {
100- console.log("[DEBUG] Starting auto-close duplicates script");
101-
102- const token = process.env.GITHUB_TOKEN;
103- if (!token) {
104- throw new Error("GITHUB_TOKEN environment variable is required");
105- }
106- console.log("[DEBUG] GitHub token found");
107-
108- const owner = process.env.GITHUB_REPOSITORY_OWNER || "anthropics";
109- const repo = process.env.GITHUB_REPOSITORY_NAME || "claude-code";
110- console.log(`[DEBUG] Repository: ${owner}/${repo}`);
111-
112- const threeDaysAgo = new Date();
113- threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
114- console.log(
115- `[DEBUG] Checking for duplicate comments older than: ${threeDaysAgo.toISOString()}`
116- );
117-
118- console.log("[DEBUG] Fetching open issues created more than 3 days ago...");
119- const allIssues: GitHubIssue[] = [];
120- let page = 1;
121- const perPage = 100;
122-
123- while (true) {
124- const pageIssues: GitHubIssue[] = await githubRequest(
125- `/repos/${owner}/${repo}/issues?state=open&per_page=${perPage}&page=${page}`,
126- token
127- );
128-
129- if (pageIssues.length === 0) break;
130-
131- // Filter for issues created more than 3 days ago
132- const oldEnoughIssues = pageIssues.filter(issue =>
133- new Date(issue.created_at) <= threeDaysAgo
134- );
135-
136- allIssues.push(...oldEnoughIssues);
137- page++;
138-
139- // Safety limit to avoid infinite loops
140- if (page > 20) break;
141- }
142-
143- const issues = allIssues;
144- console.log(`[DEBUG] Found ${issues.length} open issues`);
145-
146- let processedCount = 0;
147- let candidateCount = 0;
148-
149- for (const issue of issues) {
150- processedCount++;
151- console.log(
152- `[DEBUG] Processing issue #${issue.number} (${processedCount}/${issues.length}): ${issue.title}`
153- );
154-
155- console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
156- const comments: GitHubComment[] = await githubRequest(
157- `/repos/${owner}/${repo}/issues/${issue.number}/comments`,
158- token
159- );
160- console.log(
161- `[DEBUG] Issue #${issue.number} has ${comments.length} comments`
162- );
163-
164- const dupeComments = comments.filter(
165- (comment) =>
166- comment.body.includes("Found") &&
167- comment.body.includes("possible duplicate") &&
168- comment.user.type === "Bot"
169- );
170- console.log(
171- `[DEBUG] Issue #${issue.number} has ${dupeComments.length} duplicate detection comments`
172- );
173-
174- if (dupeComments.length === 0) {
175- console.log(
176- `[DEBUG] Issue #${issue.number} - no duplicate comments found, skipping`
177- );
178- continue;
179- }
180-
181- const lastDupeComment = dupeComments[dupeComments.length - 1];
182- const dupeCommentDate = new Date(lastDupeComment.created_at);
183- console.log(
184- `[DEBUG] Issue #${
185- issue.number
186- } - most recent duplicate comment from: ${dupeCommentDate.toISOString()}`
187- );
188-
189- if (dupeCommentDate > threeDaysAgo) {
190- console.log(
191- `[DEBUG] Issue #${issue.number} - duplicate comment is too recent, skipping`
192- );
193- continue;
194- }
195- console.log(
196- `[DEBUG] Issue #${
197- issue.number
198- } - duplicate comment is old enough (${Math.floor(
199- (Date.now() - dupeCommentDate.getTime()) / (1000 * 60 * 60 * 24)
200- )} days)`
201- );
202-
203- const commentsAfterDupe = comments.filter(
204- (comment) => new Date(comment.created_at) > dupeCommentDate
205- );
206- console.log(
207- `[DEBUG] Issue #${issue.number} - ${commentsAfterDupe.length} comments after duplicate detection`
208- );
209-
210- if (commentsAfterDupe.length > 0) {
211- console.log(
212- `[DEBUG] Issue #${issue.number} - has activity after duplicate comment, skipping`
213- );
214- continue;
215- }
216-
217- console.log(
218- `[DEBUG] Issue #${issue.number} - checking reactions on duplicate comment...`
219- );
220- const reactions: GitHubReaction[] = await githubRequest(
221- `/repos/${owner}/${repo}/issues/comments/${lastDupeComment.id}/reactions`,
222- token
223- );
224- console.log(
225- `[DEBUG] Issue #${issue.number} - duplicate comment has ${reactions.length} reactions`
226- );
227-
228- const authorThumbsDown = reactions.some(
229- (reaction) =>
230- reaction.user.id === issue.user.id && reaction.content === "-1"
231- );
232- console.log(
233- `[DEBUG] Issue #${issue.number} - author thumbs down reaction: ${authorThumbsDown}`
234- );
235-
236- if (authorThumbsDown) {
237- console.log(
238- `[DEBUG] Issue #${issue.number} - author disagreed with duplicate detection, skipping`
239- );
240- continue;
241- }
242-
243- const duplicateIssueNumber = extractDuplicateIssueNumber(lastDupeComment.body);
244- if (!duplicateIssueNumber) {
245- console.log(
246- `[DEBUG] Issue #${issue.number} - could not extract duplicate issue number from comment, skipping`
247- );
248- continue;
249- }
250-
251- candidateCount++;
252- const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
253-
254- try {
255- console.log(
256- `[INFO] Auto-closing issue #${issue.number} as duplicate of #${duplicateIssueNumber}: ${issueUrl}`
257- );
258- await closeIssueAsDuplicate(owner, repo, issue.number, duplicateIssueNumber, token);
259- console.log(
260- `[SUCCESS] Successfully closed issue #${issue.number} as duplicate of #${duplicateIssueNumber}`
261- );
262- } catch (error) {
263- console.error(
264- `[ERROR] Failed to close issue #${issue.number} as duplicate: ${error}`
265- );
266- }
267- }
268-
269- console.log(
270- `[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates for auto-close`
271- );
272-}
273-
274-autoCloseDuplicates().catch(console.error);
275-
276-// Make it a module
277-export {};
Dscripts/backfill-duplicate-comments.ts+0 -213
@@ -1,213 +1,0 @@
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- MAX_ISSUE_NUMBER - Only process issues with numbers less than this value (default: 4050)`);
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 maxIssueNumber = parseInt(process.env.MAX_ISSUE_NUMBER || "4050", 10);
93- const minIssueNumber = parseInt(process.env.MIN_ISSUE_NUMBER || "1", 10);
94-
95- console.log(`[DEBUG] Repository: ${owner}/${repo}`);
96- console.log(`[DEBUG] Dry run mode: ${dryRun}`);
97- console.log(`[DEBUG] Looking at issues between #${minIssueNumber} and #${maxIssueNumber}`);
98-
99- console.log(`[DEBUG] Fetching issues between #${minIssueNumber} and #${maxIssueNumber}...`);
100- const allIssues: GitHubIssue[] = [];
101- let page = 1;
102- const perPage = 100;
103-
104- while (true) {
105- const pageIssues: GitHubIssue[] = await githubRequest(
106- `/repos/${owner}/${repo}/issues?state=all&per_page=${perPage}&page=${page}&sort=created&direction=desc`,
107- token
108- );
109-
110- if (pageIssues.length === 0) break;
111-
112- // Filter to only include issues within the specified range
113- const filteredIssues = pageIssues.filter(issue =>
114- issue.number >= minIssueNumber && issue.number < maxIssueNumber
115- );
116- allIssues.push(...filteredIssues);
117-
118- // If the oldest issue in this page is still above our minimum, we need to continue
119- // but if the oldest issue is below our minimum, we can stop
120- const oldestIssueInPage = pageIssues[pageIssues.length - 1];
121- if (oldestIssueInPage && oldestIssueInPage.number >= maxIssueNumber) {
122- console.log(`[DEBUG] Oldest issue in page #${page} is #${oldestIssueInPage.number}, continuing...`);
123- } else if (oldestIssueInPage && oldestIssueInPage.number < minIssueNumber) {
124- console.log(`[DEBUG] Oldest issue in page #${page} is #${oldestIssueInPage.number}, below minimum, stopping`);
125- break;
126- } else if (filteredIssues.length === 0 && pageIssues.length > 0) {
127- console.log(`[DEBUG] No issues in page #${page} are in range #${minIssueNumber}-#${maxIssueNumber}, continuing...`);
128- }
129-
130- page++;
131-
132- // Safety limit to avoid infinite loops
133- if (page > 200) {
134- console.log("[DEBUG] Reached page limit, stopping pagination");
135- break;
136- }
137- }
138-
139- console.log(`[DEBUG] Found ${allIssues.length} issues between #${minIssueNumber} and #${maxIssueNumber}`);
140-
141- let processedCount = 0;
142- let candidateCount = 0;
143- let triggeredCount = 0;
144-
145- for (const issue of allIssues) {
146- processedCount++;
147- console.log(
148- `[DEBUG] Processing issue #${issue.number} (${processedCount}/${allIssues.length}): ${issue.title}`
149- );
150-
151- console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
152- const comments: GitHubComment[] = await githubRequest(
153- `/repos/${owner}/${repo}/issues/${issue.number}/comments`,
154- token
155- );
156- console.log(
157- `[DEBUG] Issue #${issue.number} has ${comments.length} comments`
158- );
159-
160- // Look for existing duplicate detection comments (from the dedupe bot)
161- const dupeDetectionComments = comments.filter(
162- (comment) =>
163- comment.body.includes("Found") &&
164- comment.body.includes("possible duplicate") &&
165- comment.user.type === "Bot"
166- );
167-
168- console.log(
169- `[DEBUG] Issue #${issue.number} has ${dupeDetectionComments.length} duplicate detection comments`
170- );
171-
172- // Skip if there's already a duplicate detection comment
173- if (dupeDetectionComments.length > 0) {
174- console.log(
175- `[DEBUG] Issue #${issue.number} already has duplicate detection comment, skipping`
176- );
177- continue;
178- }
179-
180- candidateCount++;
181- const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
182-
183- try {
184- console.log(
185- `[INFO] ${dryRun ? '[DRY RUN] ' : ''}Triggering dedupe workflow for issue #${issue.number}: ${issueUrl}`
186- );
187- await triggerDedupeWorkflow(owner, repo, issue.number, token, dryRun);
188-
189- if (!dryRun) {
190- console.log(
191- `[SUCCESS] Successfully triggered dedupe workflow for issue #${issue.number}`
192- );
193- }
194- triggeredCount++;
195- } catch (error) {
196- console.error(
197- `[ERROR] Failed to trigger workflow for issue #${issue.number}: ${error}`
198- );
199- }
200-
201- // Add a delay between workflow triggers to avoid overwhelming the system
202- await new Promise(resolve => setTimeout(resolve, 1000));
203- }
204-
205- console.log(
206- `[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates without duplicate comments, ${dryRun ? 'would trigger' : 'triggered'} ${triggeredCount} workflows`
207- );
208-}
209-
210-backfillDuplicateComments().catch(console.error);
211-
212-// Make it a module
213-export {};