From 1e0a8869af1fcf05f373f19494ec1478dd17cc0d Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:12:44 -0700 Subject: [PATCH 1/3] Use a modified no-response workflow in flutter/packages --- .github/scripts/no-response.js | 124 +++++++++++++++++++++++++++++ .github/workflows/no-response.yaml | 34 ++++++++ 2 files changed, 158 insertions(+) create mode 100644 .github/scripts/no-response.js create mode 100644 .github/workflows/no-response.yaml diff --git a/.github/scripts/no-response.js b/.github/scripts/no-response.js new file mode 100644 index 000000000000..7a26e854df6f --- /dev/null +++ b/.github/scripts/no-response.js @@ -0,0 +1,124 @@ +// 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, + }); + + 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, + }); + + 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(), + }); + + for (const comment of comments) { + if (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, + }); + + 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', + }); + } + } + } +}; diff --git a/.github/workflows/no-response.yaml b/.github/workflows/no-response.yaml new file mode 100644 index 000000000000..8941cace2444 --- /dev/null +++ b/.github/workflows/no-response.yaml @@ -0,0 +1,34 @@ +# 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. + +name: No Response + +on: + schedule: + # Schedule for every 15 minutes. + - cron: '*/15 * * * *' + workflow_dispatch: + +# By specifying the access of one of the scopes, all of those that are not +# specified are set to 'none'. +permissions: + issues: write + pull-requests: write + +jobs: + noResponse: + runs-on: ubuntu-latest + if: ${{ github.repository == 'flutter/packages' }} + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: No Response + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ github.token }} + script: | + const script = require('./.github/scripts/no-response.js'); + await script({github, context}); From a08befc905ac5d4ce41a9b4a23669216ddef282d Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Thu, 3 Sep 2026 15:15:27 -0700 Subject: [PATCH 2/3] Respond to bot comments --- .github/scripts/no-response.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/scripts/no-response.js b/.github/scripts/no-response.js index 7a26e854df6f..023325e20a44 100644 --- a/.github/scripts/no-response.js +++ b/.github/scripts/no-response.js @@ -26,6 +26,7 @@ Thanks for your contribution.`; repo, state: 'open', labels: labelName, + per_page: 100, }); for (const pr of prs) { @@ -38,12 +39,13 @@ Thanks for your contribution.`; owner, repo, issue_number: pr.number, + per_page: 100, }); const labelEvent = events.reverse().find( event => event.event === 'labeled' && - event.label.name === labelName + event.label?.name === labelName ); if (!labelEvent) { @@ -59,10 +61,11 @@ Thanks for your contribution.`; repo, issue_number: pr.number, since: labeledAt.toISOString(), + per_page: 100, }); for (const comment of comments) { - if (comment.user?.id === pr.user?.id && new Date(comment.created_at) > labeledAt) { + if (pr.user?.id && comment.user?.id === pr.user.id && new Date(comment.created_at) > labeledAt) { isResponse = true; break; } @@ -74,10 +77,11 @@ Thanks for your contribution.`; 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); + const commitDate = new Date(commit.commit?.committer?.date || commit.commit?.author?.date); if (commitDate > labeledAt) { isResponse = true; break; From 4cf4ba0def9a383881a8360da2c268e3425c4177 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:19:26 -0700 Subject: [PATCH 3/3] Bump to every 30 mins --- .github/workflows/no-response.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/no-response.yaml b/.github/workflows/no-response.yaml index 8941cace2444..f886b165c31c 100644 --- a/.github/workflows/no-response.yaml +++ b/.github/workflows/no-response.yaml @@ -6,8 +6,8 @@ name: No Response on: schedule: - # Schedule for every 15 minutes. - - cron: '*/15 * * * *' + # Schedule for every 30 minutes. + - cron: '*/30 * * * *' workflow_dispatch: # By specifying the access of one of the scopes, all of those that are not