Skip to content

Commit 464f82a

Browse files
authored
ci: better stale issues (#1220)
1 parent 3d54db8 commit 464f82a

1 file changed

Lines changed: 104 additions & 21 deletions

File tree

.github/workflows/stale.yml

Lines changed: 104 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,108 @@ jobs:
1313
stale:
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/stale@v9
16+
- uses: actions/github-script@v9
1717
with:
18-
repo-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
19-
days-before-issue-stale: 30
20-
days-before-issue-close: 14
21-
days-before-pr-stale: 45
22-
days-before-pr-close: 21
23-
stale-issue-label: "stale"
24-
stale-pr-label: "stale"
25-
exempt-issue-labels: "pinned,security,help-wanted,good-first-issue,in-progress"
26-
exempt-pr-labels: "pinned,security,in-progress,blocked"
27-
stale-issue-message: >
28-
This issue has had no activity for 30 days. It will be closed in 14 days unless updated.
29-
Add a comment or remove the `stale` label to keep it open.
30-
stale-pr-message: >
31-
This PR has had no activity for 45 days. It will be closed in 21 days unless updated.
32-
close-issue-message: >
33-
Closing due to inactivity. Reopen if still relevant.
34-
close-pr-message: >
35-
Closing due to inactivity. Reopen if still relevant.
36-
remove-stale-when-updated: true
37-
operations-per-run: 100
18+
github-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
19+
script: |
20+
const IGNORED_AUTHORS = new Set(['floatpanebot', 'github-actions[bot]', 'renovate[bot]', 'dependabot[bot]']);
21+
const STALE_LABEL = 'stale';
22+
const EXEMPT_ISSUE = ['pinned', 'security', 'help-wanted', 'good-first-issue', 'in-progress'];
23+
const EXEMPT_PR = ['pinned', 'security', 'in-progress', 'blocked'];
24+
const ISSUE_STALE_DAYS = 30;
25+
const ISSUE_CLOSE_DAYS = 14;
26+
const PR_STALE_DAYS = 45;
27+
const PR_CLOSE_DAYS = 21;
28+
const OPS_LIMIT = 100;
29+
30+
const STALE_MSG = (days, close) =>
31+
`This has had no activity for ${days} days. It will be closed in ${close} days unless updated. ` +
32+
`Comment or remove the \`stale\` label to keep it open.`;
33+
const CLOSE_MSG = 'Closing due to inactivity. Reopen if still relevant.';
34+
35+
const now = Date.now();
36+
let ops = 0;
37+
38+
async function lastHumanActivity(issue) {
39+
let latest = new Date(issue.created_at).getTime();
40+
const comments = await github.paginate(github.rest.issues.listComments, {
41+
owner: context.repo.owner,
42+
repo: context.repo.repo,
43+
issue_number: issue.number,
44+
per_page: 100,
45+
});
46+
for (const c of comments) {
47+
if (c.user && IGNORED_AUTHORS.has(c.user.login)) continue;
48+
const t = new Date(c.created_at).getTime();
49+
if (t > latest) latest = t;
50+
}
51+
if (issue.pull_request) {
52+
const commits = await github.paginate(github.rest.pulls.listCommits, {
53+
owner: context.repo.owner,
54+
repo: context.repo.repo,
55+
pull_number: issue.number,
56+
per_page: 100,
57+
});
58+
for (const c of commits) {
59+
const t = new Date(c.commit.author.date).getTime();
60+
if (t > latest) latest = t;
61+
}
62+
}
63+
return latest;
64+
}
65+
66+
const items = await github.paginate(github.rest.issues.listForRepo, {
67+
owner: context.repo.owner,
68+
repo: context.repo.repo,
69+
state: 'open',
70+
per_page: 100,
71+
});
72+
73+
for (const item of items) {
74+
if (ops >= OPS_LIMIT) { core.info('ops limit hit, stopping'); break; }
75+
const isPR = !!item.pull_request;
76+
const exempt = isPR ? EXEMPT_PR : EXEMPT_ISSUE;
77+
const labels = item.labels.map(l => l.name || l);
78+
if (exempt.some(l => labels.includes(l))) continue;
79+
80+
const staleDays = isPR ? PR_STALE_DAYS : ISSUE_STALE_DAYS;
81+
const closeDays = isPR ? PR_CLOSE_DAYS : ISSUE_CLOSE_DAYS;
82+
const isStale = labels.includes(STALE_LABEL);
83+
84+
const lastTs = await lastHumanActivity(item);
85+
const ageDays = (now - lastTs) / 86400000;
86+
87+
if (!isStale && ageDays >= staleDays) {
88+
await github.rest.issues.addLabels({
89+
owner: context.repo.owner, repo: context.repo.repo,
90+
issue_number: item.number, labels: [STALE_LABEL],
91+
});
92+
await github.rest.issues.createComment({
93+
owner: context.repo.owner, repo: context.repo.repo,
94+
issue_number: item.number,
95+
body: STALE_MSG(staleDays, closeDays),
96+
});
97+
core.info(`marked #${item.number} stale (age ${ageDays.toFixed(1)}d)`);
98+
ops++;
99+
} else if (isStale && ageDays < staleDays) {
100+
try {
101+
await github.rest.issues.removeLabel({
102+
owner: context.repo.owner, repo: context.repo.repo,
103+
issue_number: item.number, name: STALE_LABEL,
104+
});
105+
core.info(`unstaled #${item.number} (real activity ${ageDays.toFixed(1)}d ago)`);
106+
ops++;
107+
} catch (e) { if (e.status !== 404) throw e; }
108+
} else if (isStale && ageDays >= staleDays + closeDays) {
109+
await github.rest.issues.createComment({
110+
owner: context.repo.owner, repo: context.repo.repo,
111+
issue_number: item.number, body: CLOSE_MSG,
112+
});
113+
await github.rest.issues.update({
114+
owner: context.repo.owner, repo: context.repo.repo,
115+
issue_number: item.number, state: 'closed', state_reason: 'not_planned',
116+
});
117+
core.info(`closed #${item.number} (age ${ageDays.toFixed(1)}d)`);
118+
ops++;
119+
}
120+
}

0 commit comments

Comments
 (0)