| // Copyright 2013 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| module.exports = async ({ github, context }) => { | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const labelName = 'waiting for response'; | |
| const daysUntilClose = 21; | |
| const prCloseComment = `This pull request is being closed because it has not been updated in the last 21 days after a request for more information. | |
| If you are still working on this, please feel free to reopen it or file a new pull request. | |
| Thanks for your contribution.`; | |
| const now = new Date(); | |
| const closeDate = new Date(now.getTime() - (daysUntilClose * 24 * 60 * 60 * 1000)); | |
| // GitHub's pulls REST API does not support filtering by label. In GitHub's | |
| // data model, pull requests are treated as issues, so we query the issues | |
| // endpoint with the target label. Since issues are disabled in flutter/packages, | |
| // this will only return pull requests. | |
| const prs = await github.paginate(github.rest.issues.listForRepo, { | |
| owner, | |
| repo, | |
| state: 'open', | |
| labels: labelName, | |
| per_page: 100, | |
| }); | |
| for (const pr of prs) { | |
| if (!pr.pull_request) { | |
| continue; | |
| } | |
| // Fetch timeline events to find label date | |
| const events = await github.paginate(github.rest.issues.listEvents, { | |
| owner, | |
| repo, | |
| issue_number: pr.number, | |
| per_page: 100, | |
| }); | |
| const labelEvent = events.reverse().find( | |
| event => | |
| event.event === 'labeled' && | |
| event.label?.name === labelName | |
| ); | |
| if (!labelEvent) { | |
| continue; // Skip if label not found in events | |
| } | |
| const labeledAt = new Date(labelEvent.created_at); | |
| let isResponse = false; | |
| // Check for author comments after label | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner, | |
| repo, | |
| issue_number: pr.number, | |
| since: labeledAt.toISOString(), | |
| per_page: 100, | |
| }); | |
| for (const comment of comments) { | |
| if (pr.user?.id && comment.user?.id === pr.user.id && new Date(comment.created_at) > labeledAt) { | |
| isResponse = true; | |
| break; | |
| } | |
| } | |
| // Check for commits after label | |
| if (!isResponse) { | |
| const commits = await github.paginate(github.rest.pulls.listCommits, { | |
| owner, | |
| repo, | |
| pull_number: pr.number, | |
| per_page: 100, | |
| }); | |
| for (const commit of commits) { | |
| const commitDate = new Date(commit.commit?.committer?.date || commit.commit?.author?.date); | |
| if (commitDate > labeledAt) { | |
| isResponse = true; | |
| break; | |
| } | |
| } | |
| } | |
| if (isResponse) { | |
| console.log(`Removing label from #${pr.number} as author responded.`); | |
| await github.rest.issues.removeLabel({ | |
| owner, | |
| repo, | |
| issue_number: pr.number, | |
| name: labelName, | |
| }); | |
| continue; // Skip stale check | |
| } | |
| // Stale check | |
| if (pr.state === 'open') { | |
| if (labeledAt < closeDate) { | |
| if (pr.locked) { | |
| console.log(`Skipping #${pr.number} because the conversation is locked.`); | |
| continue; | |
| } | |
| console.log(`Closing #${pr.number} due to no response.`); | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: pr.number, | |
| body: prCloseComment, | |
| }); | |
| await github.rest.issues.update({ | |
| owner, | |
| repo, | |
| issue_number: pr.number, | |
| state: 'closed', | |
| }); | |
| } | |
| } | |
| } | |
| }; |