name: Close inactive bug issues on: schedule: - cron: "29 6 * * *" # daily workflow_dispatch: inputs: dry_run: description: Log matching issues without commenting or closing them required: false type: boolean default: false permissions: issues: write jobs: close_stale: runs-on: ubuntu-latest steps: - name: Close bug issues inactive longer than 30 days uses: actions/github-script@v7 with: script: | const owner = context.repo.owner; const repo = context.repo.repo; const inputs = context.payload.inputs || {}; const dryRun = String(inputs.dry_run || "false").toLowerCase() === "true"; const CLOSE_AFTER_DAYS = 30; const closeMarker = ""; const cutoff = new Date(Date.now() - CLOSE_AFTER_DAYS * 24 * 60 * 60 * 1000) .toISOString() .slice(0, 10); const items = await github.paginate(github.rest.search.issuesAndPullRequests, { // Staleness is inactivity, not issue age. Do not close an old issue // that has had recent discussion or edits. q: `repo:${owner}/${repo} is:issue is:open label:bug updated:<${cutoff}`, per_page: 100, }); core.info(`Found ${items.length} bug issues inactive for ${CLOSE_AFTER_DAYS} days.`); async function hasBeenReopened(issue_number) { const events = await github.paginate(github.rest.issues.listEvents, { owner, repo, issue_number, per_page: 100, }); return events.some(event => event.event === "reopened"); } for (const item of items) { const issue_number = item.number; if (await hasBeenReopened(issue_number)) { core.info(`#${issue_number}: skipping because it was manually reopened.`); continue; } if (dryRun) { core.info(`#${issue_number}: would comment and close.`); continue; } const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100, }); const alreadyCommented = comments.some(comment => (comment.body || "").includes(closeMarker) ); if (!alreadyCommented) { const body = `${closeMarker}\n` + `Closing this bug report because it has had no activity for more than 30 days.\n\n` + `If this is still relevant, please open a fresh issue with the latest details.`; await github.rest.issues.createComment({ owner, repo, issue_number, body, }); } await github.rest.issues.update({ owner, repo, issue_number, state: "closed", state_reason: "not_planned", }); core.info(`#${issue_number}: closed.`); }