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

Revert "Revert "Add stale issue management workflows" (#9304)"Browse files
This reverts commit a6e09217294754abe57c6f1549b82253c721a03e.
Ashwin Bhat authored on Dec 2, 2025 · GitHub committed on Dec 2, 2025 parent 4928f2c · commit 48331d39064a
2 changed files+199 -0compared with 4928f2c
A.github/workflows/remove-autoclose-label.yml
A.github/workflows/stale-issue-manager.yml
@@ -1,0 +1,42 @@
1+name: "Remove Autoclose Label on Activity"
2+
3+on:
4+ issue_comment:
5+ types: [created]
6+
7+permissions:
8+ issues: write
9+
10+jobs:
11+ remove-autoclose:
12+ # Only run if the issue has the autoclose label
13+ if: |
14+ github.event.issue.state == 'open' &&
15+ contains(github.event.issue.labels.*.name, 'autoclose') &&
16+ github.event.comment.user.login != 'github-actions[bot]'
17+ runs-on: ubuntu-latest
18+ steps:
19+ - name: Remove autoclose label
20+ uses: actions/github-script@v7
21+ with:
22+ script: |
23+ console.log(`Removing autoclose label from issue #${context.issue.number} due to new comment from ${context.payload.comment.user.login}`);
24+
25+ try {
26+ // Remove the autoclose label
27+ await github.rest.issues.removeLabel({
28+ owner: context.repo.owner,
29+ repo: context.repo.repo,
30+ issue_number: context.issue.number,
31+ name: 'autoclose'
32+ });
33+
34+ console.log(`Successfully removed autoclose label from issue #${context.issue.number}`);
35+ } catch (error) {
36+ // If the label was already removed or doesn't exist, that's fine
37+ if (error.status === 404) {
38+ console.log(`Autoclose label was already removed from issue #${context.issue.number}`);
39+ } else {
40+ throw error;
41+ }
42+ }
@@ -1,0 +1,157 @@
1+name: "Manage Stale Issues"
2+
3+on:
4+ schedule:
5+ # 2am Pacific = 9am UTC (10am UTC during DST)
6+ - cron: "0 10 * * *"
7+ workflow_dispatch:
8+
9+permissions:
10+ issues: write
11+
12+concurrency:
13+ group: stale-issue-manager
14+
15+jobs:
16+ manage-stale-issues:
17+ runs-on: ubuntu-latest
18+ steps:
19+ - name: Manage stale issues
20+ uses: actions/github-script@v7
21+ with:
22+ script: |
23+ const oneMonthAgo = new Date();
24+ oneMonthAgo.setDate(oneMonthAgo.getDate() - 30);
25+
26+ const twoMonthsAgo = new Date();
27+ twoMonthsAgo.setDate(twoMonthsAgo.getDate() - 60);
28+
29+ const warningComment = `This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.`;
30+
31+ const closingComment = `This issue has been automatically closed due to 60 days of inactivity. If you're still experiencing this issue, please open a new issue with updated information.`;
32+
33+ let page = 1;
34+ let hasMore = true;
35+ let totalWarned = 0;
36+ let totalClosed = 0;
37+ let totalLabeled = 0;
38+
39+ while (hasMore) {
40+ // Get open issues sorted by last updated (oldest first)
41+ const { data: issues } = await github.rest.issues.listForRepo({
42+ owner: context.repo.owner,
43+ repo: context.repo.repo,
44+ state: 'open',
45+ sort: 'updated',
46+ direction: 'asc',
47+ per_page: 100,
48+ page: page
49+ });
50+
51+ if (issues.length === 0) {
52+ hasMore = false;
53+ break;
54+ }
55+
56+ for (const issue of issues) {
57+ // Skip if already locked
58+ if (issue.locked) continue;
59+
60+ // Skip pull requests
61+ if (issue.pull_request) continue;
62+
63+ // Check if updated more recently than 30 days ago
64+ const updatedAt = new Date(issue.updated_at);
65+ if (updatedAt > oneMonthAgo) {
66+ // Since issues are sorted by updated_at ascending,
67+ // once we hit a recent issue, all remaining will be recent too
68+ hasMore = false;
69+ break;
70+ }
71+
72+ // Check if issue has autoclose label
73+ const hasAutocloseLabel = issue.labels.some(label =>
74+ typeof label === 'object' && label.name === 'autoclose'
75+ );
76+
77+ try {
78+ // Get comments to check for existing warning
79+ const { data: comments } = await github.rest.issues.listComments({
80+ owner: context.repo.owner,
81+ repo: context.repo.repo,
82+ issue_number: issue.number,
83+ per_page: 100
84+ });
85+
86+ // Find the last comment from github-actions bot
87+ const botComments = comments.filter(comment =>
88+ comment.user && comment.user.login === 'github-actions[bot]' &&
89+ comment.body && comment.body.includes('inactive for 30 days')
90+ );
91+
92+ const lastBotComment = botComments[botComments.length - 1];
93+
94+ if (lastBotComment) {
95+ // Check if the bot comment is older than 30 days (total 60 days of inactivity)
96+ const botCommentDate = new Date(lastBotComment.created_at);
97+ if (botCommentDate < oneMonthAgo) {
98+ // Close the issue - it's been stale for 60+ days
99+ console.log(`Closing issue #${issue.number} (stale for 60+ days): ${issue.title}`);
100+
101+ // Post closing comment
102+ await github.rest.issues.createComment({
103+ owner: context.repo.owner,
104+ repo: context.repo.repo,
105+ issue_number: issue.number,
106+ body: closingComment
107+ });
108+
109+ // Close the issue
110+ await github.rest.issues.update({
111+ owner: context.repo.owner,
112+ repo: context.repo.repo,
113+ issue_number: issue.number,
114+ state: 'closed',
115+ state_reason: 'not_planned'
116+ });
117+
118+ totalClosed++;
119+ }
120+ // If bot comment exists but is recent, issue already has warning
121+ } else if (updatedAt < oneMonthAgo) {
122+ // No bot warning yet, issue is 30+ days old
123+ console.log(`Warning issue #${issue.number} (stale for 30+ days): ${issue.title}`);
124+
125+ // Post warning comment
126+ await github.rest.issues.createComment({
127+ owner: context.repo.owner,
128+ repo: context.repo.repo,
129+ issue_number: issue.number,
130+ body: warningComment
131+ });
132+
133+ totalWarned++;
134+
135+ // Add autoclose label if not present
136+ if (!hasAutocloseLabel) {
137+ await github.rest.issues.addLabels({
138+ owner: context.repo.owner,
139+ repo: context.repo.repo,
140+ issue_number: issue.number,
141+ labels: ['autoclose']
142+ });
143+ totalLabeled++;
144+ }
145+ }
146+ } catch (error) {
147+ console.error(`Failed to process issue #${issue.number}: ${error.message}`);
148+ }
149+ }
150+
151+ page++;
152+ }
153+
154+ console.log(`Summary:`);
155+ console.log(`- Issues warned (30 days stale): ${totalWarned}`);
156+ console.log(`- Issues labeled with autoclose: ${totalLabeled}`);
157+ console.log(`- Issues closed (60 days stale): ${totalClosed}`);