Info Needed Closer #445
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Info Needed Closer | |
| on: | |
| schedule: | |
| - cron: 30 5 * * * # 10:30pm PT | |
| workflow_dispatch: | |
| workflow_call: | |
| inputs: | |
| days: | |
| description: "Number of days without activity before closing (default: 14)" | |
| required: false | |
| type: string | |
| default: "14" | |
| permissions: | |
| issues: write | |
| contents: read | |
| jobs: | |
| main: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Close Info Needed Issues | |
| uses: actions/github-script@v8 | |
| env: | |
| DAYS: ${{ inputs.days || '14' }} | |
| with: | |
| script: | | |
| const days = parseInt(process.env.DAYS); | |
| const cutoffDate = new Date(); | |
| cutoffDate.setDate(cutoffDate.getDate() - days); | |
| core.info(`Closing issues with 'info-needed' label inactive for more than ${days} days (cutoff: ${cutoffDate.toISOString()})`); | |
| // Cache of permission lookups to avoid redundant API calls for the same user | |
| const permissionCache = {}; | |
| async function hasWriteAccess(username) { | |
| if (permissionCache[username] !== undefined) { | |
| return permissionCache[username]; | |
| } | |
| try { | |
| const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| username, | |
| }); | |
| // 'admin', 'maintain', and 'write' all grant write access | |
| const result = data.permission === 'admin' || data.permission === 'maintain' || data.permission === 'write'; | |
| permissionCache[username] = result; | |
| return result; | |
| } catch (err) { | |
| // User is not a collaborator or permission lookup failed | |
| permissionCache[username] = false; | |
| return false; | |
| } | |
| } | |
| // Fetch all open issues with the 'info-needed' label | |
| const issues = await github.paginate(github.rest.issues.listForRepo, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| labels: 'info-needed', | |
| per_page: 100, | |
| }); | |
| core.info(`Found ${issues.length} open issue(s) with 'info-needed' label`); | |
| let closedCount = 0; | |
| for (const issue of issues) { | |
| // Skip locked issues and pull requests | |
| if (issue.locked) { | |
| core.info(`Issue #${issue.number}: locked, skipping`); | |
| continue; | |
| } | |
| if (issue.pull_request) { | |
| continue; | |
| } | |
| // The listComments endpoint returns comments in ascending chronological | |
| // order and does not support sort/direction params. Use the issue's | |
| // comment count to jump directly to the last page. | |
| if (issue.comments === 0) { | |
| core.info(`Issue #${issue.number}: no comments, skipping`); | |
| continue; | |
| } | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| per_page: 1, | |
| page: issue.comments, // Jump to the last page (with per_page=1), to get the last comment directly | |
| }); | |
| if (comments.length === 0) { | |
| core.info(`Issue #${issue.number}: could not fetch last comment, skipping`); | |
| continue; | |
| } | |
| const lastComment = comments[0]; | |
| const lastCommentDate = new Date(lastComment.created_at); | |
| // Skip issues with recent comment activity | |
| if (lastCommentDate >= cutoffDate) { | |
| core.info(`Issue #${issue.number}: last comment on ${lastComment.created_at} is within the activity window, skipping`); | |
| continue; | |
| } | |
| // Only close if the last comment was made by someone with write access | |
| const commenter = lastComment.user?.login; | |
| if (!commenter || !(await hasWriteAccess(commenter))) { | |
| core.info(`Issue #${issue.number}: last comment by ${commenter} does not have write access, skipping`); | |
| continue; | |
| } | |
| core.info(`Closing issue #${issue.number}: last write-access comment by ${commenter} on ${lastComment.created_at}`); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| body: 'This issue has been closed automatically because it needs more information and has not had recent activity. See also our <a href="https://aka.ms/azcodeissuereporting">issue reporting</a> guidelines.\n\nHappy Coding!', | |
| }); | |
| await github.rest.issues.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| state: 'closed', | |
| state_reason: 'not_planned', | |
| }); | |
| closedCount++; | |
| } | |
| core.info(`Done. Closed ${closedCount} issue(s).`); |