Replace review-triggered labelling with a scheduled scan
Review events on fork PRs run with a read-only token, so labels can't be added from the pull_request_review trigger. Move label-adding to a daily scheduled scan, which also covers PRs with merge conflicts. The event-driven workflow now handles removal only.
This commit is contained in:
parent
9a898cd611
commit
617e842341
40
.github/workflows/awaiting-response-label.yml
vendored
40
.github/workflows/awaiting-response-label.yml
vendored
@ -1,9 +1,6 @@
|
|||||||
name: Awaiting Response Label Sync
|
name: Awaiting Response Label Sync
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# Fires when someone finishes reviewing a PR (e.g. picks "Request changes")
|
|
||||||
pull_request_review:
|
|
||||||
types: [submitted]
|
|
||||||
# Fires when the PR author pushes new commits
|
# Fires when the PR author pushes new commits
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [synchronize]
|
types: [synchronize]
|
||||||
@ -24,7 +21,7 @@ jobs:
|
|||||||
github.event.issue.pull_request != null
|
github.event.issue.pull_request != null
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Sync "awaiting response" label
|
- name: Clear "awaiting response" label on author response
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0
|
||||||
env:
|
env:
|
||||||
AWAITING_LABEL: 'awaiting response'
|
AWAITING_LABEL: 'awaiting response'
|
||||||
@ -37,8 +34,8 @@ jobs:
|
|||||||
: context.payload.pull_request.number;
|
: context.payload.pull_request.number;
|
||||||
const { owner, repo } = context.repo;
|
const { owner, repo } = context.repo;
|
||||||
|
|
||||||
// Check whether the label is already on the PR, so we don't add it twice or
|
// Check whether the label is already on the PR, so we don't try to
|
||||||
// try to remove something that isn't there
|
// remove something that isn't there
|
||||||
const { data: issue } = await github.rest.issues.get({
|
const { data: issue } = await github.rest.issues.get({
|
||||||
owner, repo, issue_number: prNumber,
|
owner, repo, issue_number: prNumber,
|
||||||
});
|
});
|
||||||
@ -46,37 +43,22 @@ jobs:
|
|||||||
(typeof l === 'string' ? l : l.name) === awaitingLabel
|
(typeof l === 'string' ? l : l.name) === awaitingLabel
|
||||||
);
|
);
|
||||||
|
|
||||||
// A reviewer chose "Request changes" -> the author needs to respond
|
|
||||||
if (context.eventName === 'pull_request_review' && context.payload.review.state === 'changes_requested') {
|
|
||||||
if (!hasLabel) {
|
if (!hasLabel) {
|
||||||
await github.rest.issues.addLabels({
|
core.info('Label not present; nothing to do.');
|
||||||
owner, repo, issue_number: prNumber, labels: [awaitingLabel],
|
return;
|
||||||
});
|
|
||||||
core.info(`Added "${awaitingLabel}".`);
|
|
||||||
} else {
|
|
||||||
core.info('Label already applied.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The author pushed new commits -> treat that as their response and clear the label
|
// The author pushed new commits -> treat that as their response and clear the label
|
||||||
} else if (context.eventName === 'pull_request_target' && context.payload.action === 'synchronize') {
|
const authorPushed = context.eventName === 'pull_request_target' && context.payload.action === 'synchronize';
|
||||||
if (hasLabel) {
|
// The PR author left a comment -> treat any reply from them as a response too
|
||||||
|
const authorCommented = context.eventName === 'issue_comment' && context.payload.comment.user.login === context.payload.issue.user.login;
|
||||||
|
|
||||||
|
if (authorPushed || authorCommented) {
|
||||||
// If the label was already gone for some reason, that's fine, not an error
|
// If the label was already gone for some reason, that's fine, not an error
|
||||||
await github.rest.issues.removeLabel({
|
await github.rest.issues.removeLabel({
|
||||||
owner, repo, issue_number: prNumber, name: awaitingLabel,
|
owner, repo, issue_number: prNumber, name: awaitingLabel,
|
||||||
}).catch(e => core.warning(`removeLabel failed: ${e.message}`));
|
}).catch(e => core.warning(`removeLabel failed: ${e.message}`));
|
||||||
core.info(`Removed "${awaitingLabel}".`);
|
core.info(`Removed "${awaitingLabel}".`);
|
||||||
} else {
|
|
||||||
core.info('Label not present; nothing to do.');
|
|
||||||
}
|
|
||||||
// The PR author left a comment -> treat any reply from them as a response
|
|
||||||
} else if (context.eventName === 'issue_comment' && context.payload.comment.user.login === context.payload.issue.user.login) {
|
|
||||||
if (hasLabel) {
|
|
||||||
await github.rest.issues.removeLabel({
|
|
||||||
owner, repo, issue_number: prNumber, name: awaitingLabel,
|
|
||||||
}).catch(e => core.warning(`removeLabel failed: ${e.message}`));
|
|
||||||
core.info(`Removed "${awaitingLabel}".`);
|
|
||||||
} else {
|
|
||||||
core.info('Label not present; nothing to do.');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
core.info('Event does not require a label change.');
|
core.info('Event does not require a label change.');
|
||||||
}
|
}
|
||||||
|
|||||||
104
.github/workflows/awaiting-response-scan.yml
vendored
Normal file
104
.github/workflows/awaiting-response-scan.yml
vendored
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The list endpoint doesn't include merge state, so fetch the full PR
|
||||||
|
const { data: pr } = await github.rest.pulls.get({
|
||||||
|
owner, repo, pull_number: prSummary.number,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Case 1: merge conflict -> the author needs to resolve it.
|
||||||
|
// mergeable_state can be temporarily "unknown" while GitHub computes
|
||||||
|
// it; those PRs are picked up on a later run instead of retrying now.
|
||||||
|
if (pr.mergeable_state === 'dirty') {
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner, repo, issue_number: pr.number, labels: [awaitingLabel],
|
||||||
|
});
|
||||||
|
core.info(`#${pr.number}: added "${awaitingLabel}" (merge conflict).`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: a reviewer requested changes and the author hasn't responded.
|
||||||
|
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).`);
|
||||||
Loading…
x
Reference in New Issue
Block a user