diff --git a/.github/workflows/inactivity_reminder.yaml b/.github/workflows/inactivity_reminder.yaml new file mode 100644 index 0000000000..adc2f0c657 --- /dev/null +++ b/.github/workflows/inactivity_reminder.yaml @@ -0,0 +1,110 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Inactivity Reminder with Different Times + +on: + schedule: + - cron: '0 9 * * *' # Runs daily at 09:00 UTC + +jobs: + remind: + runs-on: ubuntu-latest + steps: + - name: Remind inactive issues and PRs + uses: actions/github-script@v6 + with: + script: | + const MS_IN_DAY = 24 * 60 * 60 * 1000; + const now = new Date(); + + // Thresholds + const ISSUE_INACTIVITY_DAYS = 21; // 3 weeks + const PR_INACTIVITY_DAYS = 7; // 1 week + + // Always notify this user + const defaultNotify = '@anandhkb'; + + function isInactive(updatedAt, thresholdDays) { + const updatedDate = new Date(updatedAt); + return (now - updatedDate) > thresholdDays * MS_IN_DAY; + } + + // Fetch all open issues + PRs (paginate if >100) + const issuesAndPRs = await github.paginate( + github.rest.issues.listForRepo, + { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + per_page: 100, + } + ); + + for (const item of issuesAndPRs) { + // Skip if issue/PR has the skip inactivity reminder label + if (item.labels && item.labels.some(label => label.name === 'skip inactivity reminder')) { + console.log(`Skipping #${item.number} due to skip inactivity reminder label`); + continue; + } + + const isPR = !!item.pull_request; + + // Skip if PR is in Draft mode + if (isPR && item.draft) { + console.log(`Skipping #${item.number} due to Draft PR status`); + continue; + } + + // Skip if issue (not PR) is an EPIC + if (!isPR && item.labels && item.labels.some(label => label.name === 'epic')) { + console.log(`Skipping #${item.number} due to EPIC label (issue only)`); + continue; + } + + const thresholdDays = isPR ? PR_INACTIVITY_DAYS : ISSUE_INACTIVITY_DAYS; + + if (isInactive(item.updated_at, thresholdDays)) { + // For issues, only send reminder if they have "awaiting response" label + if (!isPR && (!item.labels || !item.labels.some(label => label.name === 'awaiting response'))) { + console.log(`Skipping #${item.number} (issue) - no awaiting response label`); + continue; + } + + const assigneeMentions = item.assignees && item.assignees.length > 0 + ? item.assignees.map(a => `@${a.login}`).join(' ') + : ''; + const mentions = assigneeMentions + ? `${defaultNotify} ${assigneeMentions}`.trim() + : defaultNotify; + const issueNumber = item.number; + const type = isPR ? "pull request" : "issue"; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: [ + `🔔 Hi ${mentions}, this ${type} has had no activity for ${thresholdDays} days. Please update or let us know if it can be closed. Thank you!`, + '', + 'If this is an "epic" issue, then please add the "epic" label to this issue.', + 'If it is a PR and not ready for review, then please convert this to draft.', + 'If you just want to switch off this notification, then use the "skip inactivity reminder" label.' + ].join('\n'), + }); + + console.log(`Posted reminder on #${issueNumber} (${type}) to ${mentions}`); + } + } diff --git a/.github/workflows/issue_automation.yaml b/.github/workflows/issue_automation.yaml new file mode 100644 index 0000000000..e57462c046 --- /dev/null +++ b/.github/workflows/issue_automation.yaml @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Auto-label and Round-Robin Assign Issues + +on: + issues: + types: [opened] + +jobs: + auto-label: + runs-on: ubuntu-latest + steps: + - name: Add awaiting response label to new issues + uses: actions/github-script@v6 + with: + script: | + // Only process issues (not PRs) + if (context.payload.issue && !context.payload.issue.pull_request) { + const issue = context.payload.issue; + const issueNumber = issue.number; + + console.log(`Adding 'awaiting response' label to issue #${issueNumber}`); + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: ['awaiting response'] + }); + + console.log(`Successfully added 'awaiting response' label to issue #${issueNumber}`); + } else { + console.log('Skipping - this is a pull request, not an issue'); + } + + round-robin-assign: + runs-on: ubuntu-latest + steps: + - name: Assign issue round-robin only if unassigned + uses: actions/github-script@v6 + with: + script: | + // Only process issues (not PRs) + if (context.payload.issue && !context.payload.issue.pull_request) { + const issue = context.payload.issue; + const issueNumber = issue.number; + + // Round-robin assignment logic + const assignees = [ + 'kaatish', + 'rg20', + 'akifcorduk', + 'hlinsen', + 'Kh4ster', + 'aliceb-nv', + 'chris-maes', + 'rgsl888prabhu', + 'Iroy30', + 'tmckayus' + ]; + + // Only assign if no one is assigned yet + if (!issue.assignees || issue.assignees.length === 0) { + const index = (issueNumber - 1) % assignees.length; + const assignee = assignees[index]; + + console.log(`Assigning issue #${issueNumber} to @${assignee}`); + + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + assignees: [assignee], + }); + + console.log(`Successfully assigned issue #${issueNumber} to @${assignee}`); + } else { + console.log(`Issue #${issueNumber} already has assignees, skipping assignment.`); + } + } else { + console.log('Skipping - this is a pull request, not an issue'); + }