Skip to content

Commit 170bd2f

Browse files
committed
ci: borrow issue-lifecycle scripts from anthropics/claude-code
Four workflows were copied from anthropics/claude-code without their backing scripts, so they have been failing on every run since they landed: - sweep.yml -> scripts/sweep.ts - auto-close-duplicates.yml -> scripts/auto-close-duplicates.ts - backfill-duplicate-comments.yml -> scripts/backfill-duplicate-comments.ts - issue-lifecycle-comment.yml -> scripts/lifecycle-comment.ts This PR adds the missing scripts plus their two helpers (issue-lifecycle.ts, gh.sh) borrowed verbatim from github.com/anthropics/claude-code/tree/main/scripts and adapted for this repo where appropriate. Localizations applied: - issue-lifecycle.ts: 'Claude Code' -> 'Deepgram CLI'; support URL repointed to developers.deepgram.com; 'claude --version' replaced by 'dg --version' in the needs-info nudge. - sweep.ts: NEW_ISSUE points at deepgram/cli/issues/new/choose (everything else verbatim). - auto-close-duplicates.ts: default owner/repo fall back to deepgram/cli when GITHUB_REPOSITORY_OWNER/_NAME are missing (claude-code's defaults). Removed the 'Generated with Claude Code' attribution from the auto-close comment since this is a workflow, not a model-generated message. - backfill-duplicate-comments.ts: replaced the upstream's hardcoded owner='anthropics' / repo='claude-code' with parsing from the GITHUB_REPOSITORY env var (auto-set by GitHub Actions). This is a real fix for re-use; upstream was implicitly assuming the script only ever runs in claude-code. - gh.sh: error message example updated to reference deepgram/cli. Workflow drift check (per the CTO's request): All four workflow YAMLs are byte-identical to upstream EXCEPT for intentional security hardening — actions are pinned to specific commit SHAs (e.g. actions/checkout@34e1148... # v4) instead of version tags. No other drift. Workflows themselves do not need to change as part of this PR. Verification: - 'bun build --target=bun' compiles all 5 .ts files cleanly - 'bash -n scripts/gh.sh' passes - gh.sh marked +x Note: this is unrelated to the open homebrew-tap/cli rust-fix PRs, which can merge independently of this.
1 parent 82dd473 commit 170bd2f

6 files changed

Lines changed: 877 additions & 0 deletions

File tree

scripts/auto-close-duplicates.ts

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
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>(
29+
endpoint: string,
30+
token: string,
31+
method: string = "GET",
32+
body?: any,
33+
): Promise<T> {
34+
const response = await fetch(`https://api.github.com${endpoint}`, {
35+
method,
36+
headers: {
37+
Authorization: `Bearer ${token}`,
38+
Accept: "application/vnd.github.v3+json",
39+
"User-Agent": "auto-close-duplicates-script",
40+
...(body && { "Content-Type": "application/json" }),
41+
},
42+
...(body && { body: JSON.stringify(body) }),
43+
});
44+
45+
if (!response.ok) {
46+
throw new Error(
47+
`GitHub API request failed: ${response.status} ${response.statusText}`,
48+
);
49+
}
50+
51+
return response.json();
52+
}
53+
54+
function extractDuplicateIssueNumber(commentBody: string): number | null {
55+
let match = commentBody.match(/#(\d+)/);
56+
if (match) {
57+
return parseInt(match[1], 10);
58+
}
59+
60+
match = commentBody.match(/github\.com\/[^\/]+\/[^\/]+\/issues\/(\d+)/);
61+
if (match) {
62+
return parseInt(match[1], 10);
63+
}
64+
65+
return null;
66+
}
67+
68+
async function closeIssueAsDuplicate(
69+
owner: string,
70+
repo: string,
71+
issueNumber: number,
72+
duplicateOfNumber: number,
73+
token: string,
74+
): Promise<void> {
75+
await githubRequest(
76+
`/repos/${owner}/${repo}/issues/${issueNumber}`,
77+
token,
78+
"PATCH",
79+
{
80+
state: "closed",
81+
state_reason: "duplicate",
82+
labels: ["duplicate"],
83+
},
84+
);
85+
86+
await githubRequest(
87+
`/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
88+
token,
89+
"POST",
90+
{
91+
body: `This issue has been automatically closed as a duplicate of #${duplicateOfNumber}.
92+
93+
If this is incorrect, please re-open this issue or create a new one.`,
94+
},
95+
);
96+
}
97+
98+
async function autoCloseDuplicates(): Promise<void> {
99+
console.log("[DEBUG] Starting auto-close duplicates script");
100+
101+
const token = process.env.GITHUB_TOKEN;
102+
if (!token) {
103+
throw new Error("GITHUB_TOKEN environment variable is required");
104+
}
105+
console.log("[DEBUG] GitHub token found");
106+
107+
const owner = process.env.GITHUB_REPOSITORY_OWNER || "deepgram";
108+
const repo = process.env.GITHUB_REPOSITORY_NAME || "cli";
109+
console.log(`[DEBUG] Repository: ${owner}/${repo}`);
110+
111+
const threeDaysAgo = new Date();
112+
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
113+
console.log(
114+
`[DEBUG] Checking for duplicate comments older than: ${threeDaysAgo.toISOString()}`,
115+
);
116+
117+
console.log("[DEBUG] Fetching open issues created more than 3 days ago...");
118+
const allIssues: GitHubIssue[] = [];
119+
let page = 1;
120+
const perPage = 100;
121+
122+
while (true) {
123+
const pageIssues: GitHubIssue[] = await githubRequest(
124+
`/repos/${owner}/${repo}/issues?state=open&per_page=${perPage}&page=${page}`,
125+
token,
126+
);
127+
128+
if (pageIssues.length === 0) break;
129+
130+
const oldEnoughIssues = pageIssues.filter(
131+
(issue) => new Date(issue.created_at) <= threeDaysAgo,
132+
);
133+
134+
allIssues.push(...oldEnoughIssues);
135+
page++;
136+
137+
if (page > 20) break;
138+
}
139+
140+
const issues = allIssues;
141+
console.log(`[DEBUG] Found ${issues.length} open issues`);
142+
143+
let processedCount = 0;
144+
let candidateCount = 0;
145+
146+
for (const issue of issues) {
147+
processedCount++;
148+
console.log(
149+
`[DEBUG] Processing issue #${issue.number} (${processedCount}/${issues.length}): ${issue.title}`,
150+
);
151+
152+
console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
153+
const comments: GitHubComment[] = await githubRequest(
154+
`/repos/${owner}/${repo}/issues/${issue.number}/comments`,
155+
token,
156+
);
157+
console.log(
158+
`[DEBUG] Issue #${issue.number} has ${comments.length} comments`,
159+
);
160+
161+
const dupeComments = comments.filter(
162+
(comment) =>
163+
comment.body.includes("Found") &&
164+
comment.body.includes("possible duplicate") &&
165+
comment.user.type === "Bot",
166+
);
167+
console.log(
168+
`[DEBUG] Issue #${issue.number} has ${dupeComments.length} duplicate detection comments`,
169+
);
170+
171+
if (dupeComments.length === 0) {
172+
console.log(
173+
`[DEBUG] Issue #${issue.number} - no duplicate comments found, skipping`,
174+
);
175+
continue;
176+
}
177+
178+
const lastDupeComment = dupeComments[dupeComments.length - 1];
179+
const dupeCommentDate = new Date(lastDupeComment.created_at);
180+
console.log(
181+
`[DEBUG] Issue #${issue.number} - most recent duplicate comment from: ${dupeCommentDate.toISOString()}`,
182+
);
183+
184+
if (dupeCommentDate > threeDaysAgo) {
185+
console.log(
186+
`[DEBUG] Issue #${issue.number} - duplicate comment is too recent, skipping`,
187+
);
188+
continue;
189+
}
190+
console.log(
191+
`[DEBUG] Issue #${issue.number} - duplicate comment is old enough (${Math.floor(
192+
(Date.now() - dupeCommentDate.getTime()) / (1000 * 60 * 60 * 24),
193+
)} days)`,
194+
);
195+
196+
const commentsAfterDupe = comments.filter(
197+
(comment) => new Date(comment.created_at) > dupeCommentDate,
198+
);
199+
console.log(
200+
`[DEBUG] Issue #${issue.number} - ${commentsAfterDupe.length} comments after duplicate detection`,
201+
);
202+
203+
if (commentsAfterDupe.length > 0) {
204+
console.log(
205+
`[DEBUG] Issue #${issue.number} - has activity after duplicate comment, skipping`,
206+
);
207+
continue;
208+
}
209+
210+
console.log(
211+
`[DEBUG] Issue #${issue.number} - checking reactions on duplicate comment...`,
212+
);
213+
const reactions: GitHubReaction[] = await githubRequest(
214+
`/repos/${owner}/${repo}/issues/comments/${lastDupeComment.id}/reactions`,
215+
token,
216+
);
217+
console.log(
218+
`[DEBUG] Issue #${issue.number} - duplicate comment has ${reactions.length} reactions`,
219+
);
220+
221+
const authorThumbsDown = reactions.some(
222+
(reaction) =>
223+
reaction.user.id === issue.user.id && reaction.content === "-1",
224+
);
225+
console.log(
226+
`[DEBUG] Issue #${issue.number} - author thumbs down reaction: ${authorThumbsDown}`,
227+
);
228+
229+
if (authorThumbsDown) {
230+
console.log(
231+
`[DEBUG] Issue #${issue.number} - author disagreed with duplicate detection, skipping`,
232+
);
233+
continue;
234+
}
235+
236+
const duplicateIssueNumber = extractDuplicateIssueNumber(
237+
lastDupeComment.body,
238+
);
239+
if (!duplicateIssueNumber) {
240+
console.log(
241+
`[DEBUG] Issue #${issue.number} - could not extract duplicate issue number from comment, skipping`,
242+
);
243+
continue;
244+
}
245+
246+
candidateCount++;
247+
const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
248+
249+
try {
250+
console.log(
251+
`[INFO] Auto-closing issue #${issue.number} as duplicate of #${duplicateIssueNumber}: ${issueUrl}`,
252+
);
253+
await closeIssueAsDuplicate(
254+
owner,
255+
repo,
256+
issue.number,
257+
duplicateIssueNumber,
258+
token,
259+
);
260+
console.log(
261+
`[SUCCESS] Successfully closed issue #${issue.number} as duplicate of #${duplicateIssueNumber}`,
262+
);
263+
} catch (error) {
264+
console.error(
265+
`[ERROR] Failed to close issue #${issue.number} as duplicate: ${error}`,
266+
);
267+
}
268+
}
269+
270+
console.log(
271+
`[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates for auto-close`,
272+
);
273+
}
274+
275+
autoCloseDuplicates().catch(console.error);
276+
277+
export {};

0 commit comments

Comments
 (0)