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

Merge pull request #25210 from anthropics/chrislloyd/sweep-lifecycle-labelsBrowse files
Add daily sweep to enforce issue lifecycle label timeouts
Chris Lloyd authored on Feb 12, 2026 · GitHub committed on Feb 12, 2026 parents 77df0af + a170402 · commit bac22cb31690
2 changed files+137 -0compared with 77df0af
A.github/workflows/sweep.yml
Ascripts/sweep.ts
@@ -1,0 +1,31 @@
1+name: "Daily Issue Sweep"
2+
3+on:
4+ schedule:
5+ - cron: "0 10 * * *" # 2am Pacific
6+ workflow_dispatch:
7+
8+permissions:
9+ issues: write
10+
11+concurrency:
12+ group: daily-issue-sweep
13+
14+jobs:
15+ sweep:
16+ runs-on: ubuntu-latest
17+ steps:
18+ - name: Checkout repository
19+ uses: actions/checkout@v4
20+
21+ - name: Setup Bun
22+ uses: oven-sh/setup-bun@v2
23+ with:
24+ bun-version: latest
25+
26+ - name: Enforce lifecycle timeouts
27+ run: bun run scripts/sweep.ts
28+ env:
29+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30+ GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
31+ GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}
@@ -1,0 +1,106 @@
1+#!/usr/bin/env bun
2+
3+// --
4+
5+const NEW_ISSUE = "https://github.com/anthropics/claude-code/issues/new/choose";
6+const DRY_RUN = process.argv.includes("--dry-run");
7+
8+const lifecycle = [
9+ { label: "needs-repro", days: 7 },
10+ { label: "needs-info", days: 7 },
11+ { label: "needs-votes", days: 30 },
12+ { label: "stale", days: 30 },
13+];
14+
15+const closeMessages: Record<string, string> = {
16+ "needs-repro": `Closing — we weren't able to get the reproduction steps needed to investigate.\n\nIf this is still a problem, please [open a new issue](${NEW_ISSUE}) with steps to reproduce.`,
17+ "needs-info": `Closing — we didn't receive the information needed to move forward.\n\nIf this is still a problem, please [open a new issue](${NEW_ISSUE}) with the requested details.`,
18+ "needs-votes": `Closing this feature request — it didn't get enough community support to prioritize.\n\nIf you'd still like to see this, please [open a new feature request](${NEW_ISSUE}) with more context about the use case.`,
19+ stale: `Closing due to inactivity.\n\nIf this is still a problem, please [open a new issue](${NEW_ISSUE}) with up-to-date information.`,
20+};
21+
22+// --
23+
24+async function githubRequest<T>(
25+ endpoint: string,
26+ method = "GET",
27+ body?: unknown
28+): Promise<T> {
29+ const token = process.env.GITHUB_TOKEN;
30+ if (!token) throw new Error("GITHUB_TOKEN required");
31+
32+ const response = await fetch(`https://api.github.com${endpoint}`, {
33+ method,
34+ headers: {
35+ Authorization: `Bearer ${token}`,
36+ Accept: "application/vnd.github.v3+json",
37+ "User-Agent": "sweep",
38+ ...(body && { "Content-Type": "application/json" }),
39+ },
40+ ...(body && { body: JSON.stringify(body) }),
41+ });
42+
43+ if (!response.ok) {
44+ if (response.status === 404) return {} as T;
45+ const text = await response.text();
46+ throw new Error(`GitHub API ${response.status}: ${text}`);
47+ }
48+
49+ return response.json();
50+}
51+
52+// --
53+
54+async function main() {
55+ const owner = process.env.GITHUB_REPOSITORY_OWNER;
56+ const repo = process.env.GITHUB_REPOSITORY_NAME;
57+ if (!owner || !repo)
58+ throw new Error("GITHUB_REPOSITORY_OWNER and GITHUB_REPOSITORY_NAME required");
59+
60+ if (DRY_RUN) console.log("DRY RUN — no issues will be closed\n");
61+
62+ let closed = 0;
63+
64+ for (const { label, days } of lifecycle) {
65+ const cutoff = new Date();
66+ cutoff.setDate(cutoff.getDate() - days);
67+ console.log(`\n=== ${label} (${days}d timeout) ===`);
68+
69+ for (let page = 1; page <= 10; page++) {
70+ const issues = await githubRequest<any[]>(
71+ `/repos/${owner}/${repo}/issues?state=open&labels=${label}&sort=updated&direction=asc&per_page=100&page=${page}`
72+ );
73+ if (issues.length === 0) break;
74+
75+ for (const issue of issues) {
76+ if (issue.pull_request) continue;
77+ const base = `/repos/${owner}/${repo}/issues/${issue.number}`;
78+
79+ const events = await githubRequest<any[]>(`${base}/events?per_page=100`);
80+
81+ const labeledAt = events
82+ .filter((e) => e.event === "labeled" && e.label?.name === label)
83+ .map((e) => new Date(e.created_at))
84+ .pop();
85+
86+ if (!labeledAt || labeledAt > cutoff) continue;
87+
88+ if (DRY_RUN) {
89+ const age = Math.floor((Date.now() - labeledAt.getTime()) / 86400000);
90+ console.log(`#${issue.number}: would close (${label}, ${age}d old) — ${issue.title}`);
91+ } else {
92+ await githubRequest(`${base}/comments`, "POST", { body: closeMessages[label] });
93+ await githubRequest(base, "PATCH", { state: "closed", state_reason: "not_planned" });
94+ console.log(`#${issue.number}: closed (${label})`);
95+ }
96+ closed++;
97+ }
98+ }
99+ }
100+
101+ console.log(`\nDone: ${closed} ${DRY_RUN ? "would be closed" : "closed"}`);
102+}
103+
104+main().catch(console.error);
105+
106+export {};