name: Awaiting Response Scheduled Scan on: schedule: # Runs daily at 02:45 UTC (offset from the 01:30 CLA stale scan). - cron: '45 2 * * *' workflow_dispatch: {} permissions: pull-requests: write issues: write contents: read env: AWAITING_LABEL: 'awaiting response' # Cap on PRs examined per run, most recently updated first. MAX_PRS: 200 jobs: scan: runs-on: ubuntu-latest steps: - name: Label open PRs that are awaiting an author response uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0 with: script: | const awaitingLabel = process.env.AWAITING_LABEL; const maxPrs = parseInt(process.env.MAX_PRS, 10); const { owner, repo } = context.repo; // This scan only ADDS the label. Removal is handled by the // event-driven workflow (awaiting-response-label.yml) when the // author pushes or comments, or manually by a maintainer. const prs = await github.paginate(github.rest.pulls.list, { owner, repo, state: 'open', sort: 'updated', direction: 'desc', per_page: 100, }); let examined = 0; for (const prSummary of prs) { if (examined >= maxPrs) { core.info(`Reached MAX_PRS (${maxPrs}); stopping.`); break; } examined++; if (prSummary.labels.some(l => l.name === awaitingLabel)) { continue; } // A reviewer requested changes and the author hasn't responded. // (Merge conflicts are intentionally not labelled here: maintainers // usually know the reason and clean these up as part of the merge // process, or leave a review comment asking the author to resolve // it, which is then caught by the check below.) const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prSummary.number, }); const reviews = await github.paginate(github.rest.pulls.listReviews, { owner, repo, pull_number: pr.number, per_page: 100, }); // Note: the REST API reports review states in UPPERCASE // ("CHANGES_REQUESTED"), unlike webhook payloads which are lowercase. const decisive = reviews.filter(r => r.state === 'CHANGES_REQUESTED' || r.state === 'APPROVED' ); const latest = decisive[decisive.length - 1]; if (!latest || latest.state !== 'CHANGES_REQUESTED') { continue; } // If the author has pushed since the review, treat that as a response // (same rule as the event-driven removal workflow). if (latest.commit_id !== pr.head.sha) { continue; } // If the author has commented since the review, that also counts // as a response. const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr.number, since: latest.submitted_at, per_page: 100, }); const authorResponded = comments.some(c => (c.user?.login || '') === pr.user.login ); if (authorResponded) { continue; } await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: [awaitingLabel], }); core.info(`#${pr.number}: added "${awaitingLabel}" (changes requested, no response).`); } core.info(`Scan complete; examined ${examined} PR(s).`);