Skip to content

Commit c136a86

Browse files
Samuel FialkaSamuel Fialka
authored andcommitted
feat(ci): migrate issue to discusion
1 parent b760d45 commit c136a86

File tree

2 files changed

+130
-54
lines changed

2 files changed

+130
-54
lines changed

.github/scripts/backlog-cleanup.js

Lines changed: 126 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* exempt labels that have been inactive for 90+ days.
1111
* - Avoids sending duplicate Friendly Reminder comments if one was
1212
* posted within the last 7 days.
13+
* - Moves issues labeled 'questions' to GitHub Discussions
1314
*/
1415

1516
const dedent = (strings, ...values) => {
@@ -28,68 +29,112 @@ async function fetchAllOpenIssues(github, owner, repo) {
2829
let page = 1;
2930

3031
while (true) {
31-
const response = await github.rest.issues.listForRepo({
32+
try {
33+
const response = await github.rest.issues.listForRepo({
34+
owner,
35+
repo,
36+
state: 'open',
37+
per_page: 100,
38+
page,
39+
});
40+
const data = response.data || [];
41+
if (data.length === 0) break;
42+
const onlyIssues = data.filter(issue => !issue.pull_request);
43+
issues.push(...onlyIssues);
44+
if (data.length < 100) break;
45+
page++;
46+
} catch (err) {
47+
console.error('Error fetching issues:', err);
48+
break;
49+
}
50+
}
51+
return issues;
52+
}
53+
54+
55+
async function migrateToDiscussion(github, owner, repo, issue, categories) {
56+
const discussionCategory = 'Q&A';
57+
try {
58+
const category = categories.find(cat =>
59+
cat.name.toLowerCase() === discussionCategory.toLowerCase()
60+
);
61+
if (!category) {
62+
throw new Error(`Discussion category '${discussionCategory}' not found.`);
63+
}
64+
const { data: discussion } = await github.rest.discussions.create({
3265
owner,
3366
repo,
34-
state: 'open',
35-
per_page: 100,
36-
page,
67+
title: issue.title,
68+
body: `Originally created by @${issue.user.login} in #${issue.number}\n\n---\n\n${issue.body || ''}`,
69+
category_id: category.id,
3770
});
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++;
71+
await github.rest.issues.createComment({
72+
owner,
73+
repo,
74+
issue_number: issue.number,
75+
body: `💬 This issue was moved to [Discussions](${discussion.html_url}) for better visibility.`,
76+
});
77+
await github.rest.issues.update({
78+
owner,
79+
repo,
80+
issue_number: issue.number,
81+
state: 'closed',
82+
});
83+
return discussion.html_url;
84+
} catch (err) {
85+
console.error(`Error migrating issue #${issue.number} to discussion:`, err);
86+
return null;
4687
}
47-
return issues;
4888
}
4989

90+
5091
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;
92+
const hasExempt = issue.labels.some(l => exemptLabels.includes(l.name));
93+
const hasClose = issue.labels.some(l => closeLabels.includes(l.name));
94+
return issue.assignees.length > 0 && !hasExempt && !hasClose;
5495
};
5596

5697

5798
module.exports = async ({ github, context }) => {
99+
let categories = [];
100+
try {
101+
const { data } = await github.rest.discussions.listCategories({ owner, repo });
102+
categories = data;
103+
} catch (err) {
104+
console.error('Error fetching discussion categories:', err);
105+
}
58106
const { owner, repo } = context.repo;
59-
const issues = await fetchAllOpenIssues(github, owner, repo);
107+
let issues = [];
108+
try {
109+
issues = await fetchAllOpenIssues(github, owner, repo);
110+
} catch (err) {
111+
console.error('Failed to fetch issues:', err);
112+
return;
113+
}
60114
const now = new Date();
61115
const thresholdDays = 90;
62-
const exemptLabels = ['to-be-discussed'];
63-
const closeLabels = ['awaiting-response'];
116+
const exemptLabels = ['Status: Community help needed', 'Status: Needs investigation'];
117+
const closeLabels = ['Status: Awaiting Response'];
118+
const discussionLabel = 'Type: Question';
64119
const sevenDays = 7 * 24 * 60 * 60 * 1000;
65120

66121
let totalClosed = 0;
67122
let totalReminders = 0;
68123
let totalSkipped = 0;
124+
let totalMigrated = 0;
69125

70126
for (const issue of issues) {
71127
const isAssigned = issue.assignees && issue.assignees.length > 0;
72128
const lastUpdate = new Date(issue.updated_at);
73129
const daysSinceUpdate = Math.floor((now - lastUpdate) / (1000 * 60 * 60 * 24));
74130

75-
if (daysSinceUpdate < thresholdDays) {
76-
totalSkipped++;
131+
if (issue.labels.some(label => label.name === discussionLabel)) {
132+
const migrated = await migrateToDiscussion(github, owner, repo, issue, categories);
133+
if (migrated) totalMigrated++;
77134
continue;
78135
}
79136

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) {
137+
if (daysSinceUpdate < thresholdDays) {
93138
totalSkipped++;
94139
continue;
95140
}
@@ -100,19 +145,45 @@ module.exports = async ({ github, context }) => {
100145
}
101146

102147
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({
148+
try {
149+
await github.rest.issues.createComment({
150+
owner,
151+
repo,
152+
issue_number: issue.number,
153+
body: '⚠️ This issue was closed automatically due to inactivity. Please reopen or open a new one if still relevant.',
154+
});
155+
await github.rest.issues.update({
156+
owner,
157+
repo,
158+
issue_number: issue.number,
159+
state: 'closed',
160+
});
161+
totalClosed++;
162+
} catch (err) {
163+
console.error(`Error closing issue #${issue.number}:`, err);
164+
}
165+
continue;
166+
}
167+
168+
let comments = [];
169+
try {
170+
const { data } = await github.rest.issues.listComments({
110171
owner,
111172
repo,
112173
issue_number: issue.number,
113-
state: 'closed',
174+
per_page: 50,
114175
});
115-
totalClosed++;
176+
comments = data;
177+
} catch (err) {
178+
console.error(`Error fetching comments for issue #${issue.number}:`, err);
179+
}
180+
181+
const recentFriendlyReminder = comments.find(comment =>
182+
comment.user.login === 'github-actions[bot]' &&
183+
comment.body.includes('⏰ Friendly Reminder')
184+
);
185+
if (recentFriendlyReminder) {
186+
totalSkipped++;
116187
continue;
117188
}
118189

@@ -129,14 +200,17 @@ module.exports = async ({ github, context }) => {
129200
- Or label it 'awaiting-response' if you're waiting on something
130201
131202
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++;
203+
try {
204+
await github.rest.issues.createComment({
205+
owner,
206+
repo,
207+
issue_number: issue.number,
208+
body: comment,
209+
});
210+
totalReminders++;
211+
} catch (err) {
212+
console.error(`Error sending reminder for issue #${issue.number}:`, err);
213+
}
140214
}
141215
}
142216

@@ -145,5 +219,6 @@ module.exports = async ({ github, context }) => {
145219
Total issues processed: ${issues.length}
146220
Total issues closed: ${totalClosed}
147221
Total reminders sent: ${totalReminders}
222+
Total migrated to discussions: ${totalMigrated}
148223
Total skipped: ${totalSkipped}`);
149224
};

.github/workflows/backlog-bot.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ name: "Backlog Management Bot"
22

33
on:
44
schedule:
5-
- cron: '0 2 * * *' # Run daily at 2 AM UTC
5+
- cron: '0 4 * * *' # Run daily at 4 AM UTC
66

77
permissions:
88
issues: write
9+
discussions: write
910
contents: read
1011

1112
jobs:
@@ -14,10 +15,10 @@ jobs:
1415
runs-on: ubuntu-latest
1516
steps:
1617
- name: Checkout repository
17-
uses: actions/checkout@v4
18+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
1819

1920
- name: Run backlog cleanup script
20-
uses: actions/github-script@v7
21+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
2122
with:
2223
github-token: ${{ secrets.GITHUB_TOKEN }}
2324
script: |

0 commit comments

Comments
 (0)