@@ -13,25 +13,108 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- - uses: actions/stale@v9
+ - uses: actions/github-script@v9
with:
- repo-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
- days-before-issue-stale: 30
- days-before-issue-close: 14
- days-before-pr-stale: 45
- days-before-pr-close: 21
- stale-issue-label: "stale"
- stale-pr-label: "stale"
- exempt-issue-labels: "pinned,security,help-wanted,good-first-issue,in-progress"
- exempt-pr-labels: "pinned,security,in-progress,blocked"
- stale-issue-message: >
- This issue has had no activity for 30 days. It will be closed in 14 days unless updated.
- Add a comment or remove the `stale` label to keep it open.
- stale-pr-message: >
- This PR has had no activity for 45 days. It will be closed in 21 days unless updated.
- close-issue-message: >
- Closing due to inactivity. Reopen if still relevant.
- close-pr-message: >
- Closing due to inactivity. Reopen if still relevant.
- remove-stale-when-updated: true
- operations-per-run: 100
+ github-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
+ script: |
+ const IGNORED_AUTHORS = new Set(['floatpanebot', 'github-actions[bot]', 'renovate[bot]', 'dependabot[bot]']);
+ const STALE_LABEL = 'stale';
+ const EXEMPT_ISSUE = ['pinned', 'security', 'help-wanted', 'good-first-issue', 'in-progress'];
+ const EXEMPT_PR = ['pinned', 'security', 'in-progress', 'blocked'];
+ const ISSUE_STALE_DAYS = 30;
+ const ISSUE_CLOSE_DAYS = 14;
+ const PR_STALE_DAYS = 45;
+ const PR_CLOSE_DAYS = 21;
+ const OPS_LIMIT = 100;
+
+ const STALE_MSG = (days, close) =>
+ `This has had no activity for ${days} days. It will be closed in ${close} days unless updated. ` +
+ `Comment or remove the \`stale\` label to keep it open.`;
+ const CLOSE_MSG = 'Closing due to inactivity. Reopen if still relevant.';
+
+ const now = Date.now();
+ let ops = 0;
+
+ async function lastHumanActivity(issue) {
+ let latest = new Date(issue.created_at).getTime();
+ const comments = await github.paginate(github.rest.issues.listComments, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue.number,
+ per_page: 100,
+ });
+ for (const c of comments) {
+ if (c.user && IGNORED_AUTHORS.has(c.user.login)) continue;
+ const t = new Date(c.created_at).getTime();
+ if (t > latest) latest = t;
+ }
+ if (issue.pull_request) {
+ const commits = await github.paginate(github.rest.pulls.listCommits, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: issue.number,
+ per_page: 100,
+ });
+ for (const c of commits) {
+ const t = new Date(c.commit.author.date).getTime();
+ if (t > latest) latest = t;
+ }
+ }
+ return latest;
+ }
+
+ const items = await github.paginate(github.rest.issues.listForRepo, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ per_page: 100,
+ });
+
+ for (const item of items) {
+ if (ops >= OPS_LIMIT) { core.info('ops limit hit, stopping'); break; }
+ const isPR = !!item.pull_request;
+ const exempt = isPR ? EXEMPT_PR : EXEMPT_ISSUE;
+ const labels = item.labels.map(l => l.name || l);
+ if (exempt.some(l => labels.includes(l))) continue;
+
+ const staleDays = isPR ? PR_STALE_DAYS : ISSUE_STALE_DAYS;
+ const closeDays = isPR ? PR_CLOSE_DAYS : ISSUE_CLOSE_DAYS;
+ const isStale = labels.includes(STALE_LABEL);
+
+ const lastTs = await lastHumanActivity(item);
+ const ageDays = (now - lastTs) / 86400000;
+
+ if (!isStale && ageDays >= staleDays) {
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner, repo: context.repo.repo,
+ issue_number: item.number, labels: [STALE_LABEL],
+ });
+ await github.rest.issues.createComment({
+ owner: context.repo.owner, repo: context.repo.repo,
+ issue_number: item.number,
+ body: STALE_MSG(staleDays, closeDays),
+ });
+ core.info(`marked #${item.number} stale (age ${ageDays.toFixed(1)}d)`);
+ ops++;
+ } else if (isStale && ageDays < staleDays) {
+ try {
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner, repo: context.repo.repo,
+ issue_number: item.number, name: STALE_LABEL,
+ });
+ core.info(`unstaled #${item.number} (real activity ${ageDays.toFixed(1)}d ago)`);
+ ops++;
+ } catch (e) { if (e.status !== 404) throw e; }
+ } else if (isStale && ageDays >= staleDays + closeDays) {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner, repo: context.repo.repo,
+ issue_number: item.number, body: CLOSE_MSG,
+ });
+ await github.rest.issues.update({
+ owner: context.repo.owner, repo: context.repo.repo,
+ issue_number: item.number, state: 'closed', state_reason: 'not_planned',
+ });
+ core.info(`closed #${item.number} (age ${ageDays.toFixed(1)}d)`);
+ ops++;
+ }
+ }