Skip to content

Commit 939cb67

Browse files
authored
feat: add scheduled workflow to close stale issues (#14404)
1 parent 00705b1 commit 939cb67

File tree

1 file changed

+146
-0
lines changed

1 file changed

+146
-0
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
name: '🔒 Gemini Scheduled Stale Issue Closer'
2+
3+
on:
4+
schedule:
5+
- cron: '0 0 * * 0' # Every Sunday at midnight UTC
6+
workflow_dispatch:
7+
inputs:
8+
dry_run:
9+
description: 'Run in dry-run mode (no changes applied)'
10+
required: false
11+
default: false
12+
type: 'boolean'
13+
14+
concurrency:
15+
group: '${{ github.workflow }}'
16+
cancel-in-progress: true
17+
18+
defaults:
19+
run:
20+
shell: 'bash'
21+
22+
jobs:
23+
close-stale-issues:
24+
runs-on: 'ubuntu-latest'
25+
permissions:
26+
issues: 'write'
27+
steps:
28+
- name: 'Generate GitHub App Token'
29+
id: 'generate_token'
30+
uses: 'actions/create-github-app-token@v1'
31+
with:
32+
app-id: '${{ secrets.APP_ID }}'
33+
private-key: '${{ secrets.PRIVATE_KEY }}'
34+
permission-issues: 'write'
35+
36+
- name: 'Process Stale Issues'
37+
uses: 'actions/github-script@v7'
38+
env:
39+
DRY_RUN: '${{ inputs.dry_run }}'
40+
with:
41+
github-token: '${{ steps.generate_token.outputs.token }}'
42+
script: |
43+
const dryRun = process.env.DRY_RUN === 'true';
44+
if (dryRun) {
45+
core.info('DRY RUN MODE ENABLED: No changes will be applied.');
46+
}
47+
const batchLabel = 'Stale';
48+
49+
const threeMonthsAgo = new Date();
50+
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
51+
52+
const tenDaysAgo = new Date();
53+
tenDaysAgo.setDate(tenDaysAgo.getDate() - 10);
54+
55+
core.info(`Cutoff date for creation: ${threeMonthsAgo.toISOString()}`);
56+
core.info(`Cutoff date for updates: ${tenDaysAgo.toISOString()}`);
57+
58+
const query = `repo:${context.repo.owner}/${context.repo.repo} is:issue is:open created:<${threeMonthsAgo.toISOString()}`;
59+
core.info(`Searching with query: ${query}`);
60+
61+
const itemsToCheck = await github.paginate(github.rest.search.issuesAndPullRequests, {
62+
q: query,
63+
sort: 'created',
64+
order: 'asc',
65+
per_page: 100
66+
});
67+
68+
core.info(`Found ${itemsToCheck.length} open issues to check.`);
69+
70+
let processedCount = 0;
71+
72+
for (const issue of itemsToCheck) {
73+
const createdAt = new Date(issue.created_at);
74+
const updatedAt = new Date(issue.updated_at);
75+
const reactionCount = issue.reactions.total_count;
76+
77+
// Basic thresholds
78+
if (reactionCount >= 5) {
79+
continue;
80+
}
81+
82+
let isStale = updatedAt < tenDaysAgo;
83+
84+
// If apparently active, check if it's only bot activity
85+
if (!isStale) {
86+
try {
87+
const comments = await github.rest.issues.listComments({
88+
owner: context.repo.owner,
89+
repo: context.repo.repo,
90+
issue_number: issue.number,
91+
per_page: 100,
92+
sort: 'created',
93+
direction: 'desc'
94+
});
95+
96+
const lastHumanComment = comments.data.find(comment => comment.user.type !== 'Bot');
97+
if (lastHumanComment) {
98+
isStale = new Date(lastHumanComment.created_at) < tenDaysAgo;
99+
} else {
100+
// No human comments. Check if creator is human.
101+
if (issue.user.type !== 'Bot') {
102+
isStale = createdAt < tenDaysAgo;
103+
} else {
104+
isStale = true; // Bot created, only bot comments
105+
}
106+
}
107+
} catch (error) {
108+
core.warning(`Failed to fetch comments for issue #${issue.number}: ${error.message}`);
109+
continue;
110+
}
111+
}
112+
113+
if (isStale) {
114+
processedCount++;
115+
const message = `Closing stale issue #${issue.number}: "${issue.title}" (${issue.html_url})`;
116+
core.info(message);
117+
118+
if (!dryRun) {
119+
// Add label
120+
await github.rest.issues.addLabels({
121+
owner: context.repo.owner,
122+
repo: context.repo.repo,
123+
issue_number: issue.number,
124+
labels: [batchLabel]
125+
});
126+
127+
// Add comment
128+
await github.rest.issues.createComment({
129+
owner: context.repo.owner,
130+
repo: context.repo.repo,
131+
issue_number: issue.number,
132+
body: 'Hello! As part of our effort to keep our backlog manageable and focus on the most active issues, we are tidying up older reports.\n\nIt looks like this issue hasn\'t been active for a while, so we are closing it for now. However, if you are still experiencing this bug on the latest stable build, please feel free to comment on this issue or create a new one with updated details.\n\nThank you for your contribution!'
133+
});
134+
135+
// Close issue
136+
await github.rest.issues.update({
137+
owner: context.repo.owner,
138+
repo: context.repo.repo,
139+
issue_number: issue.number,
140+
state: 'closed'
141+
});
142+
}
143+
}
144+
}
145+
146+
core.info(`\nTotal issues processed: ${processedCount}`);

0 commit comments

Comments
 (0)