Skip to content

Commit 8a17a91

Browse files
author
Samuel Fialka
committed
feat(ci): split script and action to separate files
1 parent 4768f84 commit 8a17a91

File tree

3 files changed

+174
-97
lines changed

3 files changed

+174
-97
lines changed

.github/scripts/backlog-cleanup.js

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/**
2+
* GitHub Action script for managing issue backlog.
3+
*
4+
* Behavior:
5+
* - Pull Requests are skipped (only opened issues are processed)
6+
* - Skips issues with 'to-be-discussed' label.
7+
* - Closes issues with label 'awaiting-response' or without assignees,
8+
* with a standard closure comment.
9+
* - Sends a Friendly Reminder comment to assigned issues without
10+
* exempt labels that have been inactive for 90+ days.
11+
* - Avoids sending duplicate Friendly Reminder comments if one was
12+
* posted within the last 7 days.
13+
*/
14+
15+
const dedent = (strings, ...values) => {
16+
const raw = typeof strings === 'string' ? [strings] : strings.raw;
17+
let result = '';
18+
raw.forEach((str, i) => {
19+
result += str + (values[i] || '');
20+
});
21+
const lines = result.split('\n');
22+
const minIndent = Math.min(...lines.filter(l => l.trim()).map(l => l.match(/^\s*/)[0].length));
23+
return lines.map(l => l.slice(minIndent)).join('\n').trim();
24+
};
25+
26+
async function fetchAllOpenIssues(github, owner, repo) {
27+
const issues = [];
28+
let page = 1;
29+
30+
while (true) {
31+
const response = await github.rest.issues.listForRepo({
32+
owner,
33+
repo,
34+
state: 'open',
35+
per_page: 100,
36+
page,
37+
});
38+
39+
const data = response.data || [];
40+
if (data.length === 0) break;
41+
const onlyIssues = data.filter(issue => !issue.pull_request);
42+
issues.push(...onlyIssues);
43+
44+
if (data.length < 100) break;
45+
page++;
46+
}
47+
return issues;
48+
}
49+
50+
const shouldSendReminder = (issue, exemptLabels, closeLabels) => {
51+
const hasExempt = issue.labels.some(l => exemptLabels.includes(l.name));
52+
const hasClose = issue.labels.some(l => closeLabels.includes(l.name));
53+
return issue.assignees.length > 0 && !hasExempt && !hasClose;
54+
};
55+
56+
57+
module.exports = async ({ github, context }) => {
58+
const { owner, repo } = context.repo;
59+
const issues = await fetchAllOpenIssues(github, owner, repo);
60+
const now = new Date();
61+
const thresholdDays = 90;
62+
const exemptLabels = ['to-be-discussed'];
63+
const closeLabels = ['awaiting-response'];
64+
const sevenDays = 7 * 24 * 60 * 60 * 1000;
65+
66+
let totalClosed = 0;
67+
let totalReminders = 0;
68+
let totalSkipped = 0;
69+
70+
for (const issue of issues) {
71+
const isAssigned = issue.assignees && issue.assignees.length > 0;
72+
const lastUpdate = new Date(issue.updated_at);
73+
const daysSinceUpdate = Math.floor((now - lastUpdate) / (1000 * 60 * 60 * 24));
74+
75+
if (daysSinceUpdate < thresholdDays) {
76+
totalSkipped++;
77+
continue;
78+
}
79+
80+
const { data: comments } = await github.rest.issues.listComments({
81+
owner,
82+
repo,
83+
issue_number: issue.number,
84+
per_page: 10,
85+
});
86+
87+
const recentFriendlyReminder = comments.find(comment =>
88+
comment.user.login === 'github-actions[bot]' &&
89+
comment.body.includes('⏰ Friendly Reminder') &&
90+
(now - new Date(comment.created_at)) < sevenDays
91+
);
92+
if (recentFriendlyReminder) {
93+
totalSkipped++;
94+
continue;
95+
}
96+
97+
if (issue.labels.some(label => exemptLabels.includes(label.name))) {
98+
totalSkipped++;
99+
continue;
100+
}
101+
102+
if (issue.labels.some(label => closeLabels.includes(label.name)) || !isAssigned) {
103+
await github.rest.issues.createComment({
104+
owner,
105+
repo,
106+
issue_number: issue.number,
107+
body: '⚠️ This issue was closed automatically due to inactivity. Please reopen or open a new one if still relevant.',
108+
});
109+
await github.rest.issues.update({
110+
owner,
111+
repo,
112+
issue_number: issue.number,
113+
state: 'closed',
114+
});
115+
totalClosed++;
116+
continue;
117+
}
118+
119+
if (shouldSendReminder(issue, exemptLabels, closeLabels)) {
120+
const assignees = issue.assignees.map(u => `@${u.login}`).join(', ');
121+
const comment = dedent`
122+
⏰ Friendly Reminder
123+
124+
Hi ${assignees}!
125+
126+
This issue has had no activity for ${daysSinceUpdate} days. If it's still relevant:
127+
- Please provide a status update
128+
- Add any blocking details
129+
- Or label it 'awaiting-response' if you're waiting on something
130+
131+
This is just a reminder; the issue remains open for now.`;
132+
133+
await github.rest.issues.createComment({
134+
owner,
135+
repo,
136+
issue_number: issue.number,
137+
body: comment,
138+
});
139+
totalReminders++;
140+
}
141+
}
142+
143+
console.log(dedent`
144+
=== Backlog cleanup summary ===
145+
Total issues processed: ${issues.length}
146+
Total issues closed: ${totalClosed}
147+
Total reminders sent: ${totalReminders}
148+
Total skipped: ${totalSkipped}`);
149+
};

.github/workflows/backlog-bot.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: "Backlog Management Bot"
2+
3+
on:
4+
schedule:
5+
- cron: '0 2 * * *' # Run daily at 2 AM UTC
6+
7+
permissions:
8+
issues: write
9+
contents: read
10+
11+
jobs:
12+
backlog-bot:
13+
name: "Check for stale issues"
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout repository
17+
uses: actions/checkout@v4
18+
19+
- name: Run backlog cleanup script
20+
uses: actions/github-script@v7
21+
with:
22+
github-token: ${{ secrets.GITHUB_TOKEN }}
23+
script: |
24+
const script = require('./.github/scripts/backlog-cleanup.js');
25+
await script({ github, context });

.github/workflows/backlog-management.yml

Lines changed: 0 additions & 97 deletions
This file was deleted.

0 commit comments

Comments
 (0)