-
Notifications
You must be signed in to change notification settings - Fork 7.6k
feat(workflow): Backlog management bot #11518
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,149 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
* GitHub Action script for managing issue backlog. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Behavior: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
* - Pull Requests are skipped (only opened issues are processed) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
* - Skips issues with 'to-be-discussed' label. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
* - Closes issues with label 'awaiting-response' or without assignees, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
* with a standard closure comment. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
* - Sends a Friendly Reminder comment to assigned issues without | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
* exempt labels that have been inactive for 90+ days. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
* - Avoids sending duplicate Friendly Reminder comments if one was | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
* posted within the last 7 days. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
const dedent = (strings, ...values) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const raw = typeof strings === 'string' ? [strings] : strings.raw; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let result = ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
raw.forEach((str, i) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
result += str + (values[i] || ''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const lines = result.split('\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const minIndent = Math.min(...lines.filter(l => l.trim()).map(l => l.match(/^\s*/)[0].length)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return lines.map(l => l.slice(minIndent)).join('\n').trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
async function fetchAllOpenIssues(github, owner, repo) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const issues = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let page = 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
while (true) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const response = await github.rest.issues.listForRepo({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
owner, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
repo, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
state: 'open', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
per_page: 100, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
page, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
const data = response.data || []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (data.length === 0) break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const onlyIssues = data.filter(issue => !issue.pull_request); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
issues.push(...onlyIssues); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (data.length < 100) break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
page++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return issues; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
const shouldSendReminder = (issue, exemptLabels, closeLabels) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const hasExempt = issue.labels.some(l => exemptLabels.includes(l.name)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const hasClose = issue.labels.some(l => closeLabels.includes(l.name)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return issue.assignees.length > 0 && !hasExempt && !hasClose; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
module.exports = async ({ github, context }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { owner, repo } = context.repo; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const issues = await fetchAllOpenIssues(github, owner, repo); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const now = new Date(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const thresholdDays = 90; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const exemptLabels = ['to-be-discussed']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const closeLabels = ['awaiting-response']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const sevenDays = 7 * 24 * 60 * 60 * 1000; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
let totalClosed = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let totalReminders = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
let totalSkipped = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
for (const issue of issues) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const isAssigned = issue.assignees && issue.assignees.length > 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const lastUpdate = new Date(issue.updated_at); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const daysSinceUpdate = Math.floor((now - lastUpdate) / (1000 * 60 * 60 * 24)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (daysSinceUpdate < thresholdDays) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
totalSkipped++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { data: comments } = await github.rest.issues.listComments({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
owner, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
repo, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
issue_number: issue.number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
per_page: 10, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
const recentFriendlyReminder = comments.find(comment => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
comment.user.login === 'github-actions[bot]' && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
comment.body.includes('⏰ Friendly Reminder') && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
(now - new Date(comment.created_at)) < sevenDays | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (recentFriendlyReminder) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
totalSkipped++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (issue.labels.some(label => exemptLabels.includes(label.name))) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
totalSkipped++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+92
to
+101
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The early check for recentFriendlyReminder at line 92 prevents the automatic closure path from running when a reminder was recently posted. To ensure stale issues labeled for closure still get closed, move this check inside the reminder-sending branch so that only reminder logic is gated by recent comments.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The originally proposed solution would ensure a early exit + it increments totalSkipped used for final summary at logs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @lucasssvaz wdyt? |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (issue.labels.some(label => closeLabels.includes(label.name)) || !isAssigned) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
await github.rest.issues.createComment({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
owner, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
repo, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
issue_number: issue.number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
body: '⚠️ This issue was closed automatically due to inactivity. Please reopen or open a new one if still relevant.', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
await github.rest.issues.update({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
owner, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
repo, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
issue_number: issue.number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
state: 'closed', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
totalClosed++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (shouldSendReminder(issue, exemptLabels, closeLabels)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const assignees = issue.assignees.map(u => `@${u.login}`).join(', '); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const comment = dedent` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
⏰ Friendly Reminder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
Hi ${assignees}! | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
This issue has had no activity for ${daysSinceUpdate} days. If it's still relevant: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
- Please provide a status update | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
- Add any blocking details | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
- Or label it 'awaiting-response' if you're waiting on something | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
This is just a reminder; the issue remains open for now.`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
await github.rest.issues.createComment({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
owner, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
repo, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
issue_number: issue.number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
body: comment, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
totalReminders++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
console.log(dedent` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
=== Backlog cleanup summary === | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Total issues processed: ${issues.length} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Total issues closed: ${totalClosed} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Total reminders sent: ${totalReminders} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Total skipped: ${totalSkipped}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
name: "Backlog Management Bot" | ||
|
||
on: | ||
schedule: | ||
- cron: '0 2 * * *' # Run daily at 2 AM UTC | ||
|
||
permissions: | ||
issues: write | ||
contents: read | ||
|
||
jobs: | ||
backlog-bot: | ||
name: "Check for stale issues" | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout repository | ||
uses: actions/checkout@v4 | ||
|
||
- name: Run backlog cleanup script | ||
uses: actions/github-script@v7 | ||
with: | ||
github-token: ${{ secrets.GITHUB_TOKEN }} | ||
script: | | ||
const script = require('./.github/scripts/backlog-cleanup.js'); | ||
await script({ github, context }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Limiting comment retrieval to 10 may miss recent reminders if an issue has many comments. Consider increasing
per_page
to 100 or implementing pagination so all comments within the last 7 days are checked.Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there would be more than 10 comments, it is a sign that the issue is being worked on. In that case, the issue will be skipped because the 90 days of inactivity condition will not be met.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lucasssvaz wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will review this PR in a few days. For now testing the new BLE related stuff before 3.3.0 RC1. Will review ASAP.