Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitb8e3c9b

Browse files
feat(ci): add a backlog clean up bot
1 parentb859bdf commitb8e3c9b

File tree

2 files changed

+277
-0
lines changed

2 files changed

+277
-0
lines changed

‎.github/scripts/backlog-cleanup.js‎

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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+
* - Marks issues labeled 'questions' by adding the 'Move to Discussion' label.
14+
* (Actual migration to Discussions must be handled manually.)
15+
*/
16+
17+
constdedent=(strings, ...values)=>{
18+
constraw=typeofstrings==='string' ?[strings] :strings.raw;
19+
letresult='';
20+
raw.forEach((str,i)=>{
21+
result+=str+(values[i]||'');
22+
});
23+
constlines=result.split('\n');
24+
constminIndent=Math.min(...lines.filter(l=>l.trim()).map(l=>l.match(/^\s*/)[0].length));
25+
returnlines.map(l=>l.slice(minIndent)).join('\n').trim();
26+
};
27+
28+
29+
asyncfunctionaddMoveToDiscussionLabel(github,owner,repo,issue,isDryRun){
30+
consttargetLabel="Move to Discussion";
31+
32+
consthasLabel=issue.labels.some(
33+
l=>l.name.toLowerCase()===targetLabel.toLowerCase()
34+
);
35+
36+
if(hasLabel)returnfalse;
37+
38+
if(isDryRun){
39+
console.log(`[DRY-RUN] Would add '${targetLabel}' to issue #${issue.number}`);
40+
returntrue;
41+
}
42+
43+
try{
44+
awaitgithub.rest.issues.addLabels({
45+
owner,
46+
repo,
47+
issue_number:issue.number,
48+
labels:[targetLabel],
49+
});
50+
returntrue;
51+
52+
}catch(err){
53+
console.error(`Failed to add label to #${issue.number}`,err);
54+
returnfalse;
55+
}
56+
}
57+
58+
59+
asyncfunctionfetchAllOpenIssues(github,owner,repo){
60+
constissues=[];
61+
letpage=1;
62+
63+
while(true){
64+
try{
65+
constresponse=awaitgithub.rest.issues.listForRepo({
66+
owner,
67+
repo,
68+
state:'open',
69+
per_page:100,
70+
page,
71+
});
72+
constdata=response.data||[];
73+
if(data.length===0)break;
74+
constonlyIssues=data.filter(issue=>!issue.pull_request);
75+
issues.push(...onlyIssues);
76+
if(data.length<100)break;
77+
page++;
78+
}catch(err){
79+
console.error('Error fetching issues:',err);
80+
break;
81+
}
82+
}
83+
returnissues;
84+
}
85+
86+
87+
asyncfunctionhasRecentFriendlyReminder(github,owner,repo,issueNumber,maxAgeMs){
88+
letcomments=[];
89+
letpage=1;
90+
91+
while(true){
92+
const{ data}=awaitgithub.rest.issues.listComments({
93+
owner,
94+
repo,
95+
issue_number:issueNumber,
96+
per_page:100,
97+
page,
98+
});
99+
100+
if(!data||data.length===0)break;
101+
comments.push(...data);
102+
if(data.length<100)break;
103+
page++;
104+
}
105+
106+
constreminders=comments
107+
.filter(c=>
108+
c.user.login==='github-actions[bot]'&&
109+
c.body.includes('⏰ Friendly Reminder')
110+
)
111+
.sort((a,b)=>newDate(b.created_at)-newDate(a.created_at));
112+
113+
if(reminders.length===0)returnfalse;
114+
115+
constmostRecent=newDate(reminders[0].created_at);
116+
return(Date.now()-mostRecent.getTime())<maxAgeMs;
117+
}
118+
119+
120+
module.exports=async({ github, context, dryRun})=>{
121+
constnow=newDate();
122+
constthresholdDays=90;
123+
constexemptLabels=['Status: Community help needed','Status: Needs investigation'];
124+
constcloseLabels=['Status: Awaiting Response'];
125+
constquestionLabel='Type: Question';
126+
const{ owner, repo}=context.repo;
127+
constsevenDaysMs=7*24*60*60*1000;
128+
129+
constisDryRun=dryRun==="1";
130+
if(isDryRun){
131+
console.log("DRY-RUN mode enabled — no changes will be made.");
132+
}
133+
134+
lettotalClosed=0;
135+
lettotalReminders=0;
136+
lettotalSkipped=0;
137+
lettotalMarkedToMigrate=0;
138+
139+
letissues=[];
140+
141+
try{
142+
issues=awaitfetchAllOpenIssues(github,owner,repo);
143+
}catch(err){
144+
console.error('Failed to fetch issues:',err);
145+
return;
146+
}
147+
148+
for(constissueofissues){
149+
constisAssigned=issue.assignees&&issue.assignees.length>0;
150+
constlastUpdate=newDate(issue.updated_at);
151+
constdaysSinceUpdate=Math.floor((now-lastUpdate)/(1000*60*60*24));
152+
153+
if(issue.labels.some(label=>exemptLabels.includes(label.name))){
154+
totalSkipped++;
155+
continue;
156+
}
157+
158+
if(issue.labels.some(label=>label.name===questionLabel)){
159+
constmarked=awaitaddMoveToDiscussionLabel(github,owner,repo,issue,isDryRun);
160+
if(marked)totalMarkedToMigrate++;
161+
continue;// Do not apply reminder logic
162+
}
163+
164+
if(daysSinceUpdate<thresholdDays){
165+
totalSkipped++;
166+
continue;
167+
}
168+
169+
if(issue.labels.some(label=>closeLabels.includes(label.name))||!isAssigned){
170+
171+
if(isDryRun){
172+
console.log(`[DRY-RUN] Would close issue #${issue.number}`);
173+
totalClosed++;
174+
continue;
175+
}
176+
177+
try{
178+
awaitgithub.rest.issues.createComment({
179+
owner,
180+
repo,
181+
issue_number:issue.number,
182+
body:'⚠️ This issue was closed automatically due to inactivity. Please reopen or open a new one if still relevant.',
183+
});
184+
awaitgithub.rest.issues.update({
185+
owner,
186+
repo,
187+
issue_number:issue.number,
188+
state:'closed',
189+
});
190+
totalClosed++;
191+
}catch(err){
192+
console.error(`Error closing issue #${issue.number}:`,err);
193+
}
194+
continue;
195+
}
196+
197+
if(isAssigned){
198+
199+
if(awaithasRecentFriendlyReminder(github,owner,repo,issue.number,sevenDaysMs)){
200+
totalSkipped++;
201+
continue;
202+
}
203+
204+
constassignees=issue.assignees.map(u=>`@${u.login}`).join(', ');
205+
constcomment=dedent`
206+
⏰ Friendly Reminder
207+
208+
Hi${assignees}!
209+
210+
This issue has had no activity for${daysSinceUpdate} days. If it's still relevant:
211+
- Please provide a status update
212+
- Add any blocking details
213+
- Or label it 'Status: Awaiting Response' if you're waiting on something
214+
215+
This is just a reminder; the issue remains open for now.`;
216+
217+
if(isDryRun){
218+
console.log(`[DRY-RUN] Would post reminder on #${issue.number}`);
219+
totalReminders++;
220+
continue;
221+
}
222+
223+
try{
224+
awaitgithub.rest.issues.createComment({
225+
owner,
226+
repo,
227+
issue_number:issue.number,
228+
body:comment,
229+
});
230+
totalReminders++;
231+
}catch(err){
232+
console.error(`Error sending reminder for issue #${issue.number}:`,err);
233+
}
234+
}
235+
}
236+
237+
console.log(dedent`
238+
=== Backlog cleanup summary ===
239+
Total issues processed:${issues.length}
240+
Total issues closed:${totalClosed}
241+
Total reminders sent:${totalReminders}
242+
Total marked to migrate to discussions:${totalMarkedToMigrate}
243+
Total skipped:${totalSkipped}`);
244+
};

‎.github/workflows/backlog-bot.yml‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name:"Backlog Management Bot"
2+
3+
on:
4+
schedule:
5+
-cron:'0 4 * * *'# Run daily at 4 AM UTC
6+
workflow_dispatch:
7+
inputs:
8+
dry-run:
9+
description:"Run without modifying issues"
10+
required:false
11+
default:"0"
12+
13+
permissions:
14+
issues:write
15+
discussions:write
16+
contents:read
17+
18+
jobs:
19+
backlog-bot:
20+
name:"Check issues"
21+
runs-on:ubuntu-latest
22+
steps:
23+
-name:Checkout repository
24+
uses:actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683# v4.2.2
25+
26+
-name:Run backlog cleanup script
27+
uses:actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea# v7.0.1
28+
with:
29+
github-token:${{ secrets.GITHUB_TOKEN }}
30+
script:|
31+
const script = require('./.github/scripts/backlog-cleanup.js');
32+
const dryRun = "${{ github.event.inputs.dry-run }}";
33+
await script({ github, context, dryRun });

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp