-
Notifications
You must be signed in to change notification settings - Fork 76
209 lines (177 loc) · 9.16 KB
/
auto-unassign-stale-issues.yml
File metadata and controls
209 lines (177 loc) · 9.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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' }}