Skip to content

Auto-unassign stale issues #150

Auto-unassign stale issues

Auto-unassign stale issues #150

name: Auto-unassign stale issues
on:
schedule:
# Run daily at 6:00 AM UTC
- cron: '0 6 * * *'
workflow_dispatch:
# Allow manual triggering for testing
inputs:
days_threshold:
description: 'Days threshold for unassignment'
required: false
default: '7'
type: string
permissions:
issues: write
pull-requests: read
contents: read
jobs:
unassign-stale-issues:
runs-on: ubuntu-latest
steps:
- name: Check stale assigned issues
uses: actions/github-script@v7
with:
script: |
const daysThreshold = parseInt(process.env.DAYS_THRESHOLD || '${{ inputs.days_threshold || '7' }}');
const millisecondsThreshold = daysThreshold * 24 * 60 * 60 * 1000;
console.log(`🔍 Checking for issues assigned more than ${daysThreshold} days ago without PRs...`);
// Get all open issues with assignees
const { data: issues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
assignee: '*',
per_page: 100
});
console.log(`📋 Found ${issues.length} assigned open issues to check`);
let processedCount = 0;
let unassignedCount = 0;
for (const issue of issues) {
// Skip if it's a pull request (issues and PRs share the same API)
if (issue.pull_request) {
continue;
}
processedCount++;
console.log(`\n📝 Processing issue #${issue.number}: "${issue.title}"`);
// Get assignment events to find when the issue was assigned
const { data: events } = await github.rest.issues.listEvents({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100
});
// Find the latest assignment event
const assignmentEvents = events
.filter(event => event.event === 'assigned')
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
if (assignmentEvents.length === 0) {
console.log(`⚠️ No assignment events found for issue #${issue.number}`);
continue;
}
const latestAssignment = assignmentEvents[0];
const assignedDate = new Date(latestAssignment.created_at);
const now = new Date();
const timeDiff = now - assignedDate;
console.log(`📅 Issue #${issue.number} assigned ${Math.floor(timeDiff / (24 * 60 * 60 * 1000))} days ago to @${latestAssignment.assignee?.login || 'unknown'}`);
// Check if issue was assigned more than threshold days ago
if (timeDiff < millisecondsThreshold) {
console.log(`✅ Issue #${issue.number} is within the ${daysThreshold}-day threshold`);
continue;
}
// Search for PRs that reference this issue
const searchQueries = [
`is:pr repo:${context.repo.owner}/${context.repo.repo} ${issue.number}`,
`is:pr repo:${context.repo.owner}/${context.repo.repo} "fixes #${issue.number}"`,
`is:pr repo:${context.repo.owner}/${context.repo.repo} "closes #${issue.number}"`,
`is:pr repo:${context.repo.owner}/${context.repo.repo} "resolves #${issue.number}"`
];
let hasPR = false;
for (const query of searchQueries) {
try {
const { data: searchResults } = await github.rest.search.issuesAndPullRequests({
q: query,
per_page: 10
});
if (searchResults.total_count > 0) {
hasPR = true;
console.log(`🔗 Found ${searchResults.total_count} PR(s) referencing issue #${issue.number}`);
break;
}
} catch (error) {
console.log(`⚠️ Search error for "${query}": ${error.message}`);
}
}
if (hasPR) {
console.log(`✅ Issue #${issue.number} has associated PR(s), keeping assignment`);
continue;
}
// No PR found and issue is stale - unassign
console.log(`🚨 Issue #${issue.number} has no associated PRs and is stale - proceeding with unassignment`);
try {
// Get current assignees
const assignees = issue.assignees.map(assignee => assignee.login);
// Unassign all assignees
await github.rest.issues.removeAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
assignees: assignees
});
// Add explanatory comment
const assigneesList = assignees.map(username => `@${username}`).join(', ');
const commentBody = [
'🤖 **Auto-unassignment Notice**',
'',
`This issue has been automatically unassigned from ${assigneesList} because it was assigned more than ${daysThreshold} days ago (on ${assignedDate.toDateString()}) and no pull request has been opened to address it.`,
'',
'**What happens next?**',
'- This issue is now available for anyone to pick up',
'- If you were working on this and need more time, please comment on this issue and ask to be reassigned',
'- If you have a draft PR or work in progress, please open a draft PR to maintain your assignment',
'',
'**For future reference:**',
`- Please open a draft PR within ${daysThreshold} days of being assigned to an issue`,
'- This helps keep the project moving and ensures issues don\'t get stuck',
'',
'Thank you for your understanding! 🙏'
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: commentBody
});
unassignedCount++;
console.log(`✅ Successfully unassigned and commented on issue #${issue.number}`);
} catch (error) {
console.log(`❌ Failed to unassign issue #${issue.number}: ${error.message}`);
}
// Add a small delay to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 1000));
}
console.log(`\n📊 Summary: Processed ${processedCount} issues, unassigned ${unassignedCount} stale issues`);
// Create a summary comment if any issues were unassigned
if (unassignedCount > 0) {
const summaryBody = [
'## Daily Auto-unassignment Report',
'',
`📅 **Date**: ${new Date().toDateString()}`,
`🔢 **Issues Processed**: ${processedCount}`,
`🚨 **Issues Unassigned**: ${unassignedCount}`,
`⏰ **Threshold**: ${daysThreshold} days`,
'',
'### What was done?',
'- Checked all open assigned issues',
`- Unassigned issues that were assigned more than ${daysThreshold} days ago without associated PRs`,
'- Added explanatory comments to unassigned issues',
'',
'### Why does this happen?',
'This automation helps keep the project moving by:',
'- Preventing issues from getting stuck with inactive assignees',
'- Making issues available for other contributors',
'- Encouraging timely progress or communication',
'',
'*This is an automated report. The issues have been unassigned and are now available for anyone to work on.*'
].join('\n');
const summaryIssue = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `🤖 Auto-unassignment Summary - ${new Date().toDateString()}`,
body: summaryBody,
labels: ['automation', 'maintenance']
});
console.log(`📋 Created summary issue #${summaryIssue.data.number}`);
}
env:
DAYS_THRESHOLD: ${{ inputs.days_threshold || '7' }}