From 4768f8456fef1ac612adf5b5acb5787fa00c1f71 Mon Sep 17 00:00:00 2001 From: Samuel Fialka Date: Thu, 19 Jun 2025 14:57:01 +0200 Subject: [PATCH 1/2] feat(ci): add backlog management bot --- .github/workflows/backlog-management.yml | 97 ++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 .github/workflows/backlog-management.yml diff --git a/.github/workflows/backlog-management.yml b/.github/workflows/backlog-management.yml new file mode 100644 index 00000000000..42057d6961e --- /dev/null +++ b/.github/workflows/backlog-management.yml @@ -0,0 +1,97 @@ +name: 'Backlog Management Bot' +on: + schedule: + - cron: '0 2 * * *' # Run daily at 2 AM UTC + workflow_dispatch: + pull_request: + types: [opened, reopened, synchronize, labeled] + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + name: 'Stale Issue Management' + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ github.token }} + stale-issue-message: '' + close-issue-message: | + [TEST MODE] This issue has been automatically closed due to inactivity. If this issue is still relevant, please reopen it or create a new issue with updated information. + days-before-issue-stale: 0 + days-before-issue-close: 0 + exempt-issue-labels: 'to-be-discussed' + exempt-issue-assignees: true + exempt-all-issue-assignees: true + operations-per-run: 100 + assignee-reminder: + name: 'Assignee Reminder Bot' + runs-on: ubuntu-latest + steps: + - name: Send Reminders for Assigned Issues + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + // Get all open issues that are assigned + const { data: issues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + per_page: 100 + }); + + const now = new Date(); + const reminderThreshold = 0; // days + + for (const issue of issues) { + if (!issue.assignees || issue.assignees.length === 0) continue; + if (issue.pull_request) continue; + // if (!issue.title.includes('[TEST]')) continue; + + const labels = issue.labels.map(label => label.name); + const exemptLabels = ['stale', 'to-be-discussed']; + if (labels.some(label => exemptLabels.includes(label))) continue; + + // Check last activity + const lastUpdate = new Date(issue.updated_at); + const daysSinceUpdate = Math.floor((now - lastUpdate) / (1000 * 60 * 60 * 24)); + + if (daysSinceUpdate >= reminderThreshold) { + // Check if we've already sent a reminder recently + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + per_page: 10 + }); + + const recentBotComment = comments.find(comment => + comment.user.login === 'github-actions[bot]' && + comment.body.includes('⏰ Friendly Reminder') && + (now - new Date(comment.created_at)) < (7 * 24 * 60 * 60 * 1000) // 7 days + ); + + if (!recentBotComment) { + const assigneeNames = issue.assignees.map(assignee => `@${assignee.login}`).join(', '); + // console.log(`[TEST] Would send reminder to: ${assigneeNames} for issue #${issue.number} (${issue.title})`); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: + `## ⏰ Friendly Reminder + + Hello ${assigneeNames}! + This issue has been assigned to you and hasn't had any activity for ${daysSinceUpdate} days. Please provide a status update or progress comment + + --- + *This is an automated reminder from the Backlog Management Bot.*` + }); + } + } + } From 8a17a916f82228fbbd83dc729472703e97acca3b Mon Sep 17 00:00:00 2001 From: Samuel Fialka Date: Tue, 24 Jun 2025 10:35:42 +0200 Subject: [PATCH 2/2] feat(ci): split script and action to separate files --- .github/scripts/backlog-cleanup.js | 149 +++++++++++++++++++++++ .github/workflows/backlog-bot.yml | 25 ++++ .github/workflows/backlog-management.yml | 97 --------------- 3 files changed, 174 insertions(+), 97 deletions(-) create mode 100644 .github/scripts/backlog-cleanup.js create mode 100644 .github/workflows/backlog-bot.yml delete mode 100644 .github/workflows/backlog-management.yml diff --git a/.github/scripts/backlog-cleanup.js b/.github/scripts/backlog-cleanup.js new file mode 100644 index 00000000000..0869c59d4b1 --- /dev/null +++ b/.github/scripts/backlog-cleanup.js @@ -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; + } + + 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}`); +}; diff --git a/.github/workflows/backlog-bot.yml b/.github/workflows/backlog-bot.yml new file mode 100644 index 00000000000..24228001139 --- /dev/null +++ b/.github/workflows/backlog-bot.yml @@ -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 }); diff --git a/.github/workflows/backlog-management.yml b/.github/workflows/backlog-management.yml deleted file mode 100644 index 42057d6961e..00000000000 --- a/.github/workflows/backlog-management.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: 'Backlog Management Bot' -on: - schedule: - - cron: '0 2 * * *' # Run daily at 2 AM UTC - workflow_dispatch: - pull_request: - types: [opened, reopened, synchronize, labeled] - -permissions: - issues: write - pull-requests: write - -jobs: - stale: - name: 'Stale Issue Management' - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v9 - with: - repo-token: ${{ github.token }} - stale-issue-message: '' - close-issue-message: | - [TEST MODE] This issue has been automatically closed due to inactivity. If this issue is still relevant, please reopen it or create a new issue with updated information. - days-before-issue-stale: 0 - days-before-issue-close: 0 - exempt-issue-labels: 'to-be-discussed' - exempt-issue-assignees: true - exempt-all-issue-assignees: true - operations-per-run: 100 - assignee-reminder: - name: 'Assignee Reminder Bot' - runs-on: ubuntu-latest - steps: - - name: Send Reminders for Assigned Issues - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - // Get all open issues that are assigned - const { data: issues } = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - per_page: 100 - }); - - const now = new Date(); - const reminderThreshold = 0; // days - - for (const issue of issues) { - if (!issue.assignees || issue.assignees.length === 0) continue; - if (issue.pull_request) continue; - // if (!issue.title.includes('[TEST]')) continue; - - const labels = issue.labels.map(label => label.name); - const exemptLabels = ['stale', 'to-be-discussed']; - if (labels.some(label => exemptLabels.includes(label))) continue; - - // Check last activity - const lastUpdate = new Date(issue.updated_at); - const daysSinceUpdate = Math.floor((now - lastUpdate) / (1000 * 60 * 60 * 24)); - - if (daysSinceUpdate >= reminderThreshold) { - // Check if we've already sent a reminder recently - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - per_page: 10 - }); - - const recentBotComment = comments.find(comment => - comment.user.login === 'github-actions[bot]' && - comment.body.includes('⏰ Friendly Reminder') && - (now - new Date(comment.created_at)) < (7 * 24 * 60 * 60 * 1000) // 7 days - ); - - if (!recentBotComment) { - const assigneeNames = issue.assignees.map(assignee => `@${assignee.login}`).join(', '); - // console.log(`[TEST] Would send reminder to: ${assigneeNames} for issue #${issue.number} (${issue.title})`); - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: - `## ⏰ Friendly Reminder - - Hello ${assigneeNames}! - This issue has been assigned to you and hasn't had any activity for ${daysSinceUpdate} days. Please provide a status update or progress comment - - --- - *This is an automated reminder from the Backlog Management Bot.*` - }); - } - } - }