Skip to content

Commit cac7d0d

Browse files
authored
chore: add stale PR workflows and auto-closure policy (#631)
* feat(stale): adding workflow and updating CONTRIBUTING.md to close stale PRs * feat(stale): adding workflow and updating CONTRIBUTING.md to close stale PRs
1 parent 6daa153 commit cac7d0d

3 files changed

Lines changed: 148 additions & 0 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Label PR review state
2+
3+
on:
4+
schedule:
5+
- cron: '0 * * * *' # hourly
6+
workflow_dispatch:
7+
8+
permissions:
9+
pull-requests: read
10+
issues: write
11+
12+
jobs:
13+
reconcile:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Reconcile PR review state labels
17+
uses: actions/github-script@v7
18+
with:
19+
script: |
20+
const { owner, repo } = context.repo;
21+
const stateLabels = ['awaiting-author', 'awaiting-review'];
22+
23+
const prs = await github.paginate(github.rest.pulls.list, {
24+
owner, repo, state: 'open', per_page: 100,
25+
});
26+
27+
for (const pr of prs) {
28+
const reviews = await github.paginate(github.rest.pulls.listReviews, {
29+
owner, repo, pull_number: pr.number, per_page: 100,
30+
});
31+
32+
// Reviews are returned chronologically, so later entries replace
33+
// each reviewer's earlier decision.
34+
const latest = new Map();
35+
for (const r of reviews) {
36+
if (r.state !== 'COMMENTED') {
37+
latest.set(r.user.login, r);
38+
}
39+
}
40+
41+
const changeRequestReviewers = [...latest.entries()]
42+
.filter(([, review]) => review.state === 'CHANGES_REQUESTED')
43+
.map(([login]) => login);
44+
const requestedReviewers = new Set(
45+
pr.requested_reviewers.map(reviewer => reviewer.login),
46+
);
47+
48+
let desiredLabel = null;
49+
if (changeRequestReviewers.length > 0) {
50+
desiredLabel = changeRequestReviewers.every(
51+
reviewer => requestedReviewers.has(reviewer),
52+
)
53+
? 'awaiting-review'
54+
: 'awaiting-author';
55+
}
56+
57+
const currentLabels = new Set(pr.labels.map(label => label.name));
58+
for (const label of stateLabels) {
59+
if (label !== desiredLabel && currentLabels.has(label)) {
60+
await github.rest.issues.removeLabel({
61+
owner, repo, issue_number: pr.number, name: label,
62+
});
63+
}
64+
}
65+
66+
if (desiredLabel && !currentLabels.has(desiredLabel)) {
67+
await github.rest.issues.addLabels({
68+
owner, repo, issue_number: pr.number, labels: [desiredLabel],
69+
});
70+
}
71+
72+
if (
73+
desiredLabel !== 'awaiting-author' &&
74+
currentLabels.has('stale-awaiting-author')
75+
) {
76+
await github.rest.issues.removeLabel({
77+
owner, repo, issue_number: pr.number,
78+
name: 'stale-awaiting-author',
79+
});
80+
}
81+
}

.github/workflows/stale.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Close stale PRs
2+
3+
on:
4+
schedule:
5+
- cron: '0 0 * * *'
6+
workflow_dispatch:
7+
8+
permissions:
9+
pull-requests: write
10+
issues: write
11+
12+
jobs:
13+
stale-inactivity:
14+
name: 60-day inactivity
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/stale@v9
18+
with:
19+
repo-token: ${{ secrets.GITHUB_TOKEN }}
20+
21+
days-before-issue-stale: -1
22+
days-before-issue-close: -1
23+
days-before-pr-stale: 60
24+
days-before-pr-close: 7
25+
26+
stale-pr-label: stale-inactive
27+
stale-pr-message: >
28+
This PR has had no activity for 60 days and will be automatically
29+
closed in 7 days unless there is new activity. If you'd like to
30+
continue, please rebase and leave a comment.
31+
close-pr-message: >
32+
Closing due to 60+ days of inactivity. Feel free to reopen if
33+
you'd like to resume this work.
34+
35+
exempt-pr-labels: 'do-not-close,pinned,work-in-progress'
36+
37+
stale-review-inactivity:
38+
name: 14-day author inactivity after requested changes
39+
runs-on: ubuntu-latest
40+
steps:
41+
- uses: actions/stale@v9
42+
with:
43+
repo-token: ${{ secrets.GITHUB_TOKEN }}
44+
45+
only-labels: awaiting-author
46+
days-before-issue-stale: -1
47+
days-before-issue-close: -1
48+
days-before-pr-stale: 14
49+
days-before-pr-close: 7
50+
51+
stale-pr-label: stale-awaiting-author
52+
stale-pr-message: >
53+
This PR has been awaiting author changes for 14 days and will be
54+
automatically closed in 7 days. Please address the review comments
55+
or leave a comment if you need more time.
56+
close-pr-message: >
57+
Closing due to author inactivity after requested changes. Feel free
58+
to reopen once the requested changes have been addressed.
59+
60+
exempt-pr-labels: 'do-not-close,pinned,work-in-progress'

CONTRIBUTING.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,13 @@ Pull requests should be reviewable, tested, and maintainable. Before opening a P
150150

151151
Maintainers may close PRs that are incomplete, too broad, inactive, not aligned with the project direction, or that create disproportionate review or maintenance burden. Closing a PR is not a judgment on the contributor; it is a maintainer decision that the change cannot be accepted in its present form.
152152

153+
PRs are also closed automatically by bot:
154+
155+
- **60-day inactivity:** A PR with no activity for 60 days is marked stale and closed after a further 7 days if there is still no activity. Any new comment, commit, or review resets the timer.
156+
- **14-day author inactivity:** After a reviewer requests changes, the PR is labelled `awaiting-author`. Author activity resets the inactivity timer. Once the changes are ready, re-request review from the reviewer; the PR will move to `awaiting-review` and is no longer eligible for automatic closure under this policy.
157+
158+
To opt a PR out of automatic closure, apply the `do-not-close`, `pinned`, or `work-in-progress` label.
159+
153160
### AI-Assisted Contributions
154161

155162
Use of AI tools is allowed, but contributors remain fully responsible for their submissions.

0 commit comments

Comments
 (0)