Skip to content

Commit d117ed1

Browse files
authored
chore(ci): tune stale policy and add backfill
* chore(ci): tune stale grace periods * chore(ci): add stale closure backfill
1 parent 005eeca commit d117ed1

1 file changed

Lines changed: 247 additions & 10 deletions

File tree

.github/workflows/stale.yml

Lines changed: 247 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,32 @@ on:
44
schedule:
55
- cron: "17 3 * * *"
66
workflow_dispatch:
7+
inputs:
8+
backfill_stale_closures:
9+
description: "Close currently stale-eligible issues and PRs with the Barnacle app"
10+
required: false
11+
type: boolean
12+
default: false
13+
dry_run:
14+
description: "List matching stale-eligible items without closing them"
15+
required: false
16+
type: boolean
17+
default: true
18+
include_issues:
19+
description: "Include stale-eligible issues in the backfill"
20+
required: false
21+
type: boolean
22+
default: true
23+
include_prs:
24+
description: "Include stale-eligible pull requests in the backfill"
25+
required: false
26+
type: boolean
27+
default: true
28+
max_closures:
29+
description: "Maximum items to close when dry_run is false"
30+
required: false
31+
type: number
32+
default: 50
733

834
env:
935
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -12,6 +38,7 @@ permissions: {}
1238

1339
jobs:
1440
stale:
41+
if: ${{ github.event_name != 'workflow_dispatch' || inputs.backfill_stale_closures != true }}
1542
permissions:
1643
issues: write
1744
pull-requests: write
@@ -35,10 +62,10 @@ jobs:
3562
uses: actions/stale@v10
3663
with:
3764
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
38-
days-before-issue-stale: 7
39-
days-before-issue-close: 5
40-
days-before-pr-stale: 5
41-
days-before-pr-close: 3
65+
days-before-issue-stale: 14
66+
days-before-issue-close: 7
67+
days-before-pr-stale: 14
68+
days-before-pr-close: 7
4269
stale-issue-label: stale
4370
stale-pr-label: stale
4471
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
@@ -95,7 +122,7 @@ jobs:
95122
days-before-issue-stale: -1
96123
days-before-issue-close: -1
97124
days-before-pr-stale: 27
98-
days-before-pr-close: 3
125+
days-before-pr-close: 7
99126
stale-pr-label: stale
100127
exempt-pr-labels: maintainer,no-stale,bad-barnacle
101128
operations-per-run: 2000
@@ -139,10 +166,10 @@ jobs:
139166
uses: actions/stale@v10
140167
with:
141168
repo-token: ${{ steps.app-token-fallback.outputs.token }}
142-
days-before-issue-stale: 7
143-
days-before-issue-close: 5
144-
days-before-pr-stale: 5
145-
days-before-pr-close: 3
169+
days-before-issue-stale: 14
170+
days-before-issue-close: 7
171+
days-before-pr-stale: 14
172+
days-before-pr-close: 7
146173
stale-issue-label: stale
147174
stale-pr-label: stale
148175
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
@@ -197,7 +224,7 @@ jobs:
197224
days-before-issue-stale: -1
198225
days-before-issue-close: -1
199226
days-before-pr-stale: 27
200-
days-before-pr-close: 3
227+
days-before-pr-close: 7
201228
stale-pr-label: stale
202229
exempt-pr-labels: maintainer,no-stale,bad-barnacle
203230
operations-per-run: 2000
@@ -213,7 +240,217 @@ jobs:
213240
If you believe this PR should be revived, post in #clawtributors on Discord to talk to a maintainer.
214241
That channel is the escape hatch for high-quality PRs that get auto-closed.
215242
243+
backfill-stale-closures:
244+
if: ${{ github.event_name == 'workflow_dispatch' && inputs.backfill_stale_closures == true }}
245+
permissions:
246+
issues: write
247+
pull-requests: write
248+
runs-on: blacksmith-16vcpu-ubuntu-2404
249+
steps:
250+
- uses: actions/create-github-app-token@v3
251+
id: app-token
252+
with:
253+
app-id: "2971289"
254+
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
255+
- name: Backfill stale closures
256+
uses: actions/github-script@v9
257+
env:
258+
DRY_RUN: ${{ inputs.dry_run }}
259+
INCLUDE_ISSUES: ${{ inputs.include_issues }}
260+
INCLUDE_PRS: ${{ inputs.include_prs }}
261+
MAX_CLOSURES: ${{ inputs.max_closures }}
262+
with:
263+
github-token: ${{ steps.app-token.outputs.token }}
264+
script: |
265+
const dayMs = 24 * 60 * 60 * 1000;
266+
const dryRun = process.env.DRY_RUN !== "false";
267+
const includeIssues = process.env.INCLUDE_ISSUES !== "false";
268+
const includePrs = process.env.INCLUDE_PRS !== "false";
269+
const maxClosures = Math.max(0, Number(process.env.MAX_CLOSURES || "50"));
270+
const nowMs = Date.now();
271+
const { owner, repo } = context.repo;
272+
273+
const issueExemptLabels = new Set([
274+
"enhancement",
275+
"maintainer",
276+
"pinned",
277+
"security",
278+
"no-stale",
279+
"bad-barnacle",
280+
]);
281+
const prExemptLabels = new Set(["maintainer", "no-stale", "bad-barnacle"]);
282+
const maintainerAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
283+
284+
const issueCloseMessage = [
285+
"Closing due to inactivity.",
286+
"If this is still an issue, please retry on the latest OpenClaw release and share updated details.",
287+
"If you are absolutely sure it still happens on the latest release, open a new issue with fresh steps to reproduce.",
288+
].join("\n");
289+
const prCloseMessage = [
290+
"Closing due to inactivity.",
291+
"If you believe this PR should be revived, post in #clawtributors on Discord to talk to a maintainer.",
292+
"That channel is the escape hatch for high-quality PRs that get auto-closed.",
293+
].join("\n");
294+
295+
const hasAny = (labels, exemptLabels) => {
296+
for (const label of labels) {
297+
if (exemptLabels.has(label)) {
298+
return true;
299+
}
300+
}
301+
return false;
302+
};
303+
const isOlderThan = (dateString, days) => {
304+
const timestamp = Date.parse(dateString);
305+
return Number.isFinite(timestamp) && timestamp < nowMs - days * dayMs;
306+
};
307+
308+
const candidates = [];
309+
const skipped = {
310+
missingStale: 0,
311+
exemptLabel: 0,
312+
maintainerAuthor: 0,
313+
notOldEnough: 0,
314+
disabledType: 0,
315+
};
316+
317+
for await (const response of github.paginate.iterator(github.rest.issues.listForRepo, {
318+
owner,
319+
repo,
320+
state: "open",
321+
sort: "updated",
322+
direction: "asc",
323+
per_page: 100,
324+
})) {
325+
for (const item of response.data) {
326+
const isPr = Boolean(item.pull_request);
327+
if ((isPr && !includePrs) || (!isPr && !includeIssues)) {
328+
skipped.disabledType += 1;
329+
continue;
330+
}
331+
332+
const labels = new Set((item.labels || []).map(label => label.name));
333+
if (!labels.has("stale")) {
334+
skipped.missingStale += 1;
335+
continue;
336+
}
337+
338+
const exemptLabels = isPr ? prExemptLabels : issueExemptLabels;
339+
if (hasAny(labels, exemptLabels)) {
340+
skipped.exemptLabel += 1;
341+
continue;
342+
}
343+
344+
if (maintainerAssociations.has(item.author_association)) {
345+
skipped.maintainerAuthor += 1;
346+
continue;
347+
}
348+
349+
const assigned = (item.assignees || []).length > 0;
350+
let eligible = false;
351+
let lane = "";
352+
if (isPr && assigned) {
353+
lane = "assigned-pr";
354+
eligible = isOlderThan(item.created_at, 34);
355+
} else if (isPr) {
356+
lane = "unassigned-pr";
357+
eligible = isOlderThan(item.updated_at, 7);
358+
} else if (assigned) {
359+
lane = "assigned-issue";
360+
eligible = isOlderThan(item.updated_at, 10);
361+
} else {
362+
lane = "unassigned-issue";
363+
eligible = isOlderThan(item.updated_at, 7);
364+
}
365+
366+
if (!eligible) {
367+
skipped.notOldEnough += 1;
368+
continue;
369+
}
370+
371+
candidates.push({
372+
number: item.number,
373+
title: item.title,
374+
lane,
375+
isPr,
376+
assigned,
377+
createdAt: item.created_at,
378+
updatedAt: item.updated_at,
379+
authorAssociation: item.author_association,
380+
url: item.html_url,
381+
});
382+
}
383+
}
384+
385+
const countsByLane = candidates.reduce((counts, candidate) => {
386+
counts[candidate.lane] = (counts[candidate.lane] || 0) + 1;
387+
return counts;
388+
}, {});
389+
const selected = maxClosures === 0 ? candidates : candidates.slice(0, maxClosures);
390+
391+
core.info(`Dry run: ${dryRun}`);
392+
core.info(`Candidates: ${candidates.length}`);
393+
core.info(`Selected: ${selected.length}`);
394+
core.info(`Counts by lane: ${JSON.stringify(countsByLane)}`);
395+
core.info(`Skipped: ${JSON.stringify(skipped)}`);
396+
for (const candidate of selected) {
397+
core.info(`${dryRun ? "Would close" : "Closing"} ${candidate.lane} #${candidate.number}: ${candidate.title} (${candidate.url})`);
398+
}
399+
400+
await core.summary
401+
.addHeading("Stale Closure Backfill")
402+
.addRaw(`Dry run: ${dryRun}\n\n`)
403+
.addRaw(`Candidates: ${candidates.length}\n\n`)
404+
.addRaw(`Selected: ${selected.length}\n\n`)
405+
.addCodeBlock(JSON.stringify({ countsByLane, skipped }, null, 2), "json")
406+
.addTable([
407+
[
408+
{ data: "Lane", header: true },
409+
{ data: "Number", header: true },
410+
{ data: "Title", header: true },
411+
{ data: "URL", header: true },
412+
],
413+
...selected.map(candidate => [
414+
candidate.lane,
415+
String(candidate.number),
416+
candidate.title,
417+
candidate.url,
418+
]),
419+
])
420+
.write();
421+
422+
if (dryRun) {
423+
return;
424+
}
425+
426+
for (const candidate of selected) {
427+
await github.rest.issues.createComment({
428+
owner,
429+
repo,
430+
issue_number: candidate.number,
431+
body: candidate.isPr ? prCloseMessage : issueCloseMessage,
432+
});
433+
434+
if (candidate.isPr) {
435+
await github.rest.pulls.update({
436+
owner,
437+
repo,
438+
pull_number: candidate.number,
439+
state: "closed",
440+
});
441+
} else {
442+
await github.rest.issues.update({
443+
owner,
444+
repo,
445+
issue_number: candidate.number,
446+
state: "closed",
447+
state_reason: "not_planned",
448+
});
449+
}
450+
}
451+
216452
lock-closed-issues:
453+
if: ${{ github.event_name != 'workflow_dispatch' || inputs.backfill_stale_closures != true }}
217454
permissions:
218455
issues: write
219456
runs-on: blacksmith-16vcpu-ubuntu-2404

0 commit comments

Comments
 (0)