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

claude-code / .github / workflows / stale-issue-manager.yml
157 lines · 6.3 KBRaw
1name: "Manage Stale Issues"
2
3on:
4 schedule:
5 # 2am Pacific = 9am UTC (10am UTC during DST)
6 - cron: "0 10 * * *"
7 workflow_dispatch:
8
9permissions:
10 issues: write
11
12concurrency:
13 group: stale-issue-manager
14
15jobs:
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}`);