Skip to content

Issue: Close stale issues #24

Issue: Close stale issues

Issue: Close stale issues #24

# Auto-close stale issues that have no response from the author.
#
# Runs daily. Adds a "waiting-for-response" label when a maintainer
# replies. If the author doesn't follow up within 14 days, the issue
# is closed automatically with a polite message.
name: "Issue: Close stale issues"
on:
schedule:
# Run daily at 09:00 UTC
- cron: "0 9 * * *"
workflow_dispatch: # Allow manual trigger
permissions:
issues: write
jobs:
label-responded:
# Label issues where a maintainer (repo member/owner) has replied
# but the original author hasn't responded yet.
runs-on: ubuntu-latest
steps:
- name: Label issues awaiting author response
uses: actions/github-script@v7
with:
script: |
const LABEL = 'waiting-for-response';
const STALE_DAYS = 14;
// Ensure the label exists
try {
await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: LABEL,
});
} catch {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: LABEL,
color: 'fbca04',
description: 'Awaiting response from the issue author',
});
}
// Get all open issues (not PRs)
const issues = await github.paginate(
github.rest.issues.listForRepo,
{
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100,
}
);
for (const issue of issues) {
// Skip pull requests
if (issue.pull_request) continue;
const comments = await github.paginate(
github.rest.issues.listComments,
{
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100,
}
);
if (comments.length === 0) continue;
// Find the last comment by someone other than the author
// (i.e., a maintainer response)
const maintainerComments = comments.filter(
c => c.user.login !== issue.user.login
);
if (maintainerComments.length === 0) continue;
const lastMaintainer =
maintainerComments[maintainerComments.length - 1];
// Check if the author replied after the maintainer
const authorRepliesAfter = comments.filter(
c =>
c.user.login === issue.user.login &&
new Date(c.created_at) > new Date(lastMaintainer.created_at)
);
const hasLabel = issue.labels.some(l => l.name === LABEL);
if (authorRepliesAfter.length === 0) {
// Maintainer replied, author hasn't followed up
if (!hasLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: [LABEL],
});
}
// Check if stale (14 days since last maintainer comment)
const daysSince = Math.floor(
(Date.now() - new Date(lastMaintainer.created_at)) /
(1000 * 60 * 60 * 24)
);
if (daysSince >= STALE_DAYS) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: [
`This issue has been automatically closed after ${STALE_DAYS} days of inactivity.`,
``,
`If you still need help, feel free to reopen this issue or create a new one. We're happy to assist! 😊`,
].join('\n'),
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'closed',
state_reason: 'not_planned',
});
}
} else {
// Author replied β€” remove label if present
if (hasLabel) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
name: LABEL,
});
}
}
}