(owner: string, repo: string)
| 90 | } |
| 91 | |
| 92 | async function closeExpired(owner: string, repo: string) { |
| 93 | let closed = 0; |
| 94 | |
| 95 | for (const { label, days, reason } of lifecycle) { |
| 96 | const cutoff = new Date(); |
| 97 | cutoff.setDate(cutoff.getDate() - days); |
| 98 | console.log(`\n=== ${label} (${days}d timeout) ===`); |
| 99 | |
| 100 | for (let page = 1; page <= 10; page++) { |
| 101 | const issues = await githubRequest<any[]>( |
| 102 | `/repos/${owner}/${repo}/issues?state=open&labels=${label}&sort=updated&direction=asc&per_page=100&page=${page}` |
| 103 | ); |
| 104 | if (issues.length === 0) break; |
| 105 | |
| 106 | for (const issue of issues) { |
| 107 | if (issue.pull_request) continue; |
| 108 | if (issue.locked) continue; |
| 109 | |
| 110 | const thumbsUp = issue.reactions?.["+1"] ?? 0; |
| 111 | if (thumbsUp >= STALE_UPVOTE_THRESHOLD) continue; |
| 112 | |
| 113 | const base = `/repos/${owner}/${repo}/issues/${issue.number}`; |
| 114 | |
| 115 | const events = await githubRequest<any[]>(`${base}/events?per_page=100`); |
| 116 | |
| 117 | const labeledAt = events |
| 118 | .filter((e) => e.event === "labeled" && e.label?.name === label) |
| 119 | .map((e) => new Date(e.created_at)) |
| 120 | .pop(); |
| 121 | |
| 122 | if (!labeledAt || labeledAt > cutoff) continue; |
| 123 | |
| 124 | // Skip if a non-bot user commented after the label was applied. |
| 125 | // The triage workflow should remove lifecycle labels on human |
| 126 | // activity, but check here too as a safety net. |
| 127 | const comments = await githubRequest<any[]>( |
| 128 | `${base}/comments?since=${labeledAt.toISOString()}&per_page=100` |
| 129 | ); |
| 130 | const hasHumanComment = comments.some( |
| 131 | (c) => c.user && c.user.type !== "Bot" |
| 132 | ); |
| 133 | if (hasHumanComment) { |
| 134 | console.log( |
| 135 | `#${issue.number}: skipping (human activity after ${label} label)` |
| 136 | ); |
| 137 | continue; |
| 138 | } |
| 139 | |
| 140 | if (DRY_RUN) { |
| 141 | const age = Math.floor((Date.now() - labeledAt.getTime()) / 86400000); |
| 142 | console.log(`#${issue.number}: would close (${label}, ${age}d old) — ${issue.title}`); |
| 143 | } else { |
| 144 | await githubRequest(`${base}/comments`, "POST", { body: CLOSE_MESSAGE(reason) }); |
| 145 | await githubRequest(base, "PATCH", { state: "closed", state_reason: "not_planned" }); |
| 146 | console.log(`#${issue.number}: closed (${label})`); |
| 147 | } |
| 148 | closed++; |
| 149 | } |
no test coverage detected