Skip to content

ci: auto-close-duplicates cron — 14-day window, dry-run mode (#289) - #291

Merged
allxsmith merged 2 commits into
mainfrom
ci/auto-close-duplicates
Jul 12, 2026
Merged

allxsmith merged 2 commits into
mainfrom
ci/auto-close-duplicates

Conversation

@allxsmith

@allxsmith allxsmith commented Jul 11, 2026 •

Copy link
Copy Markdown
Owner

Pull Request

Description

PR B of #289: the zero-Claude daily cron that closes silence-approved duplicate issues, ported from oven-sh/bun's scripts/auto-close-duplicates.ts with the objection window widened from 3 to 14 days.

  • scripts/auto-close-duplicates.mjs — dependency-free (node: builtins + global fetch against the GitHub REST API, GITHUB_TOKEN auth). Scans open issues for a latest claude[bot] comment carrying the <!-- ai-triage:dedupe --> marker plus a parseable Duplicate of #N line; closes only when the marker is ≥14 days old and none of the three vetoes hit: a non-bot comment after the marker, a 👎 reaction on the marker comment, or a reopened event in the issue timeline (a reopened issue is never auto-closed again). Closes with state_reason: duplicate, falling back to not_planned on a 422. --mode=dry-run only logs the would-close list. Rate-limit courtesy: aborts politely below 20 remaining requests. Exit codes: 0 clean / 1 API failure / 2 usage; every action/skip line names the issue and the reason; summary counts at the end.
  • .github/workflows/auto-close-duplicates.yml — cron 37 6 * * * (non-colliding slot) + workflow_dispatch; permissions: {} top-level, the single job gets exactly issues: write + contents: read; pinned checkout/setup-node, no pnpm/cache (the script is dep-free). Deliberately NOT gated on AI_LOOP_ENABLED — zero-Claude workflows ignore the AI-spend kill switch (house rule, same as auto-label-claude-prs.yml). A workflow_dispatch mode input may de-escalate on → dry-run but can never escalate dry-run → on unless the repo variable itself is on.

The job never runs until the AI_TRIAGE_AUTOCLOSE repo variable is set (dry-run or on) — unset fails closed. Per the #289 rollout plan: start with dry-run, flip to on after a week of sane logs.

Docs for #289 (ai-development guide + root CLAUDE.md) live in PR A, which also ships the ai-triage v2 workflow and slash commands that produce the dedupe markers this cron consumes.

  • bulma-ui (@allxsmith/bestax-bulma)
  • create-bestax (create-bestax)
  • docs (@allxsmith/bestax-docs)
  • Other (please specify): repo tooling (scripts/, .github/workflows/)

Related Issue(s)

Refs #289 (PR A + the rollout checklist complete it)

Type of Change

  • Build tooling

Checklist

  • My code follows the project style guidelines
  • I have performed a self-review of my code
  • I have added tests that prove my fix is effective or that my feature works (inline node -e fixture asserts over the exported pure helpers: argv parsing, marker matching, age math, veto logic; plus a live dry-run against this repo — 20 issues scanned, 0 candidates, clean exit)
  • I have added/updated documentation as needed (docs are PR A's scope)
  • I have updated Storybook stories as needed (bulma-ui) — n/a
  • My changes require a change to the documentation (covered by PR A)
  • All new and existing tests passed
  • Any relevant dependencies are updated — none; the script is deliberately dependency-free
  • If this PR changes commands, conventions, or package structure, the affected CLAUDE.md files are updated — CLAUDE.md update is PR A's scope

Screenshots / Demos

Live dry-run output (read-only) against allxsmith/bestax:

auto-close-duplicates: repo=allxsmith/bestax mode=dry-run wait-days=14
Summary: scanned=20 would-close=0 deferred=0 vetoed=0

Additional Context

Verification performed: node --check, python3 yaml.safe_load on the workflow, prettier + eslint clean, fixture asserts, live dry-run.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Pohc8xLkdx4gwXkW3xd7up


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Introduced an automated duplicate-issue cleanup that, after a configurable wait window, closes eligible issues when they have an idempotent marker and no subsequent objections.
    • Added both scheduled (daily) and manual executions, including a safe dry-run option.
    • Respects safeguards: issues with objections, 👎 reactions, reopen activity, or invalid/self-duplicate references are deferred or left unchanged.
    • Logs a final summary of scan results.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 73ce51f7-58db-4bc9-833f-406edee7c5cb

📥 Commits

Reviewing files that changed from the base of the PR and between 3d03830 and adb353c.

📒 Files selected for processing (2)
  • .github/workflows/auto-close-duplicates.yml
  • scripts/auto-close-duplicates.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • .github/workflows/auto-close-duplicates.yml
  • scripts/auto-close-duplicates.mjs

Walkthrough

Adds a scheduled and manually triggered workflow plus a Node.js CLI that detects marked duplicate issues, applies age and objection safeguards, supports dry-run mode, and closes eligible issues with GitHub API fallbacks.

Changes

Duplicate issue auto-closure

Layer / File(s) Summary
Workflow scheduling and mode gating
.github/workflows/auto-close-duplicates.yml
Runs daily or manually, gates execution through AI_TRIAGE_AUTOCLOSE, configures Node.js 24, and prevents unauthorized escalation from dry-run to active mode.
CLI parsing and GitHub API primitives
scripts/auto-close-duplicates.mjs
Adds marker parsing, bot and objection detection, argument validation, paginated API access, and rate-limit handling.
Duplicate detection and closure flow
scripts/auto-close-duplicates.mjs
Scans open issues, applies duplicate vetoes, supports dry-run reporting, and closes eligible issues with a fallback state reason.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHubActions
  participant DuplicateCLI
  participant GitHubAPI
  GitHubActions->>DuplicateCLI: Start with repository and effective mode
  DuplicateCLI->>GitHubAPI: Fetch open issues and comments
  GitHubAPI-->>DuplicateCLI: Return issue and deduplication data
  DuplicateCLI->>GitHubAPI: Comment on and close eligible issue
  GitHubAPI-->>DuplicateCLI: Return request status
Loading

Possibly related issues

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the cron workflow and 14-day auto-close behavior.
Description check ✅ Passed The description follows the template well and includes summary, affected area, related issue, change type, checklist, demo, and context.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/auto-close-duplicates

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://1b492ee6.bestax.pages.dev

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
scripts/auto-close-duplicates.mjs (1)

209-338: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

One failing issue aborts the entire scan.

The per-issue body has no local error handling, so a single transient/permanent error (e.g., a transferred duplicate target returning 301, which is not in okStatuses, or a one-off 5xx) throws to the outer catch and stops processing every remaining issue for that run. A persistently problematic issue would block all other closures on every daily run.

Consider wrapping the per-issue work in a try/catch that logs and continues (tracking a failed count), reserving the outer hard-fail for RateLimitAbort and unexpected fatal errors.

♻️ Sketch: isolate per-issue failures
     for (const issue of issues) {
       if (issue.pull_request || isBot(issue.user)) continue;
       counts.scanned++;
-      checkRateBudget();
-      ...
-      counts.closed++;
+      try {
+        checkRateBudget();
+        // ... existing per-issue logic ...
+        counts.closed++;
+      } catch (err) {
+        if (err instanceof RateLimitAbort) throw err;
+        console.error(`#${issue.number}: skipped after error: ${err.message}`);
+        counts.failed++;
+      }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/auto-close-duplicates.mjs` around lines 209 - 338, Wrap each
iteration of the issues loop around the per-issue workflow in a local try/catch
so transient or issue-specific API failures are logged and processing continues
with subsequent issues. Add and increment a failed count for these skipped
issues, while rethrowing RateLimitAbort and preserving the outer catch for
unexpected fatal errors; ensure the final summary includes the new failed count.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/auto-close-duplicates.mjs`:
- Around line 124-150: The api() helper must handle GitHub secondary rate limits
and request timeouts. Add an AbortSignal.timeout(...) to each fetch request, and
retry 403/429 responses using the Retry-After header as the backoff delay before
retrying; preserve existing okStatuses handling and error reporting for
non-retriable failures.

---

Nitpick comments:
In `@scripts/auto-close-duplicates.mjs`:
- Around line 209-338: Wrap each iteration of the issues loop around the
per-issue workflow in a local try/catch so transient or issue-specific API
failures are logged and processing continues with subsequent issues. Add and
increment a failed count for these skipped issues, while rethrowing
RateLimitAbort and preserving the outer catch for unexpected fatal errors;
ensure the final summary includes the new failed count.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e9ca0027-6708-4fba-8807-c896dd005609

📥 Commits

Reviewing files that changed from the base of the PR and between b02eccc and 3d03830.

📒 Files selected for processing (2)
  • .github/workflows/auto-close-duplicates.yml
  • scripts/auto-close-duplicates.mjs

Comment thread scripts/auto-close-duplicates.mjs
Comment thread .github/workflows/auto-close-duplicates.yml

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review — 1 finding

No. Severity Area Finding Location
1 🟡 Minor Correctness No concurrency group — a manual workflow_dispatch can race the daily cron and double-comment/double-close the same issue .github/workflows/auto-close-duplicates.yml:41

Overall: The change is sound and unusually well-guarded. The veto stack (age gate → human-comment-after → 👎 reaction → reopened timeline event → target-exists/self-reference), the fail-closed AI_TRIAGE_AUTOCLOSE gate, the de-escalate-always / never-escalate-to-on dispatch logic, and the state_reason=duplicate → not_planned 422 fallback all check out. Scanning only state=open and materializing the full issue list up front avoids pagination-shift bugs, and the polite rate-limit floor exits 0 as documented. The riskiest area is purely operational — overlapping runs — and it only yields a redundant comment, not a wrong close. Minor edges worth a glance but not blocking: a Duplicate-of-#PR target passes the existence check (PRs resolve via /issues/{n}), and there are no unit tests for the exported helpers, though that matches the existing scripts/*.mjs (which have none either).

🏄 Clean set, dude — this cron paddles out with reef shoes on: fails closed, vetoes politely, and will not wipe out on a 422. Only ripple is two boards catching the same wave if a manual run drops in on the cron, so toss a concurrency leash on it and it is all-time. Good to send.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://910da6b5.bestax.pages.dev

@allxsmith
allxsmith merged commit 572f1fa into main Jul 12, 2026
19 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.2.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 5.4.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 1.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants