






















@@ -4,6 +4,32 @@ on:
44schedule:
55 - cron: "17 3 * * *"
66workflow_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
733834env:
935FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -12,6 +38,7 @@ permissions: {}
12381339jobs:
1440stale:
41+if: ${{ github.event_name != 'workflow_dispatch' || inputs.backfill_stale_closures != true }}
1542permissions:
1643issues: write
1744pull-requests: write
@@ -35,10 +62,10 @@ jobs:
3562uses: actions/stale@v10
3663with:
3764repo-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
4269stale-issue-label: stale
4370stale-pr-label: stale
4471exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
@@ -95,7 +122,7 @@ jobs:
95122days-before-issue-stale: -1
96123days-before-issue-close: -1
97124days-before-pr-stale: 27
98-days-before-pr-close: 3
125+days-before-pr-close: 7
99126stale-pr-label: stale
100127exempt-pr-labels: maintainer,no-stale,bad-barnacle
101128operations-per-run: 2000
@@ -139,10 +166,10 @@ jobs:
139166uses: actions/stale@v10
140167with:
141168repo-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
146173stale-issue-label: stale
147174stale-pr-label: stale
148175exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
@@ -197,7 +224,7 @@ jobs:
197224days-before-issue-stale: -1
198225days-before-issue-close: -1
199226days-before-pr-stale: 27
200-days-before-pr-close: 3
227+days-before-pr-close: 7
201228stale-pr-label: stale
202229exempt-pr-labels: maintainer,no-stale,bad-barnacle
203230operations-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.
215242243+ 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 }}
217454permissions:
218455issues: write
219456runs-on: blacksmith-16vcpu-ubuntu-2404
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。