-
Notifications
You must be signed in to change notification settings - Fork 361
330 lines (275 loc) Β· 13.5 KB
/
auto-close-parent-issues.yml
File metadata and controls
330 lines (275 loc) Β· 13.5 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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
name: Auto-Close Parent Issues
# Trigger when any issue is closed
on:
issues:
types: [closed]
permissions:
issues: write
jobs:
close-parent-issues:
runs-on: ubuntu-latest
steps:
- name: Auto-close parent issues when all sub-issues are closed
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const { owner, repo } = context.repo;
const closedIssueNumber = context.payload.issue.number;
core.info('=================================================');
core.info('Auto-Close Parent Issues Workflow');
core.info('=================================================');
core.info(`Triggered by: Issue #${closedIssueNumber} was closed`);
core.info(`Repository: ${owner}/${repo}`);
core.info(`Event: ${context.eventName}`);
core.info('');
/**
* Get the full issue details including parent relationships using GraphQL
* Uses pagination to handle issues with many sub-issues (e.g., 1000+)
*/
async function getIssueWithRelationships(issueNumber) {
core.info(`π Fetching issue #${issueNumber} with relationship data...`);
// Fetch parent issues (trackedInIssues) - usually small number
const parentQuery = `
query($owner: String!, $repo: String!, $issueNumber: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $issueNumber) {
id
number
title
state
stateReason
trackedInIssues(first: 10) {
nodes {
id
number
title
state
stateReason
}
}
}
}
}
`;
try {
const result = await github.graphql(parentQuery, {
owner,
repo,
issueNumber
});
const issue = result.repository.issue;
// Now fetch all sub-issues with pagination
core.info(`π Fetching sub-issues with pagination...`);
const allSubIssues = await fetchAllSubIssues(issue.id);
// Add sub-issues to the issue object
issue.trackedIssues = {
nodes: allSubIssues
};
core.info(`β Fetched issue #${issue.number}: "${issue.title}"`);
core.info(` State: ${issue.state} (${issue.stateReason || 'N/A'})`);
core.info(` Parent issues: ${issue.trackedInIssues.nodes.length}`);
core.info(` Sub-issues: ${issue.trackedIssues.nodes.length}`);
return issue;
} catch (error) {
core.error(`β Failed to fetch issue #${issueNumber}: ${error.message}`);
throw error;
}
}
/**
* Fetch all sub-issues using pagination to handle large numbers (e.g., 1000+)
*/
async function fetchAllSubIssues(issueId) {
const allSubIssues = [];
let hasNextPage = true;
let cursor = null;
let pageCount = 0;
const query = `
query($issueId: ID!, $cursor: String) {
node(id: $issueId) {
... on Issue {
trackedIssues(first: 100, after: $cursor) {
nodes {
id
number
title
state
stateReason
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`;
while (hasNextPage) {
pageCount++;
core.info(` Fetching page ${pageCount} of sub-issues...`);
const result = await github.graphql(query, {
issueId,
cursor
});
const trackedIssues = result.node.trackedIssues;
allSubIssues.push(...trackedIssues.nodes);
hasNextPage = trackedIssues.pageInfo.hasNextPage;
cursor = trackedIssues.pageInfo.endCursor;
core.info(` Retrieved ${trackedIssues.nodes.length} sub-issues (total so far: ${allSubIssues.length})`);
// Safety check to prevent infinite loops
if (pageCount > 50) {
core.warning(`β οΈ Reached maximum page limit (50 pages, 5000 sub-issues). Some sub-issues may not be processed.`);
break;
}
}
core.info(`β Total sub-issues fetched: ${allSubIssues.length}`);
return allSubIssues;
}
/**
* Check if all sub-issues of a parent are closed
*/
function areAllSubIssuesClosed(parentIssue) {
const subIssues = parentIssue.trackedIssues.nodes;
core.info(`π Checking sub-issues of #${parentIssue.number} "${parentIssue.title}"...`);
core.info(` Total sub-issues: ${subIssues.length}`);
if (subIssues.length === 0) {
core.info(` β οΈ Issue #${parentIssue.number} has no sub-issues`);
return false;
}
const openSubIssues = [];
const closedSubIssues = [];
for (const subIssue of subIssues) {
if (subIssue.state === 'OPEN') {
openSubIssues.push(subIssue);
core.info(` - #${subIssue.number}: "${subIssue.title}" [OPEN]`);
} else {
closedSubIssues.push(subIssue);
core.info(` - #${subIssue.number}: "${subIssue.title}" [CLOSED]`);
}
}
core.info(` Summary: ${closedSubIssues.length} closed, ${openSubIssues.length} open`);
return openSubIssues.length === 0;
}
/**
* Close a parent issue
*/
async function closeIssue(issueNumber, reason) {
core.info(`π Closing issue #${issueNumber}...`);
core.info(` Reason: ${reason}`);
try {
await github.rest.issues.update({
owner,
repo,
issue_number: issueNumber,
state: 'closed',
state_reason: 'completed'
});
core.info(`β Successfully closed issue #${issueNumber}`);
// Add a comment explaining why it was closed
const comment = `π **Automatically closed**\n\nAll sub-issues have been completed. This parent issue is now being closed automatically.\n\n${reason}`;
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: comment
});
core.info(`β Added closure comment to issue #${issueNumber}`);
return true;
} catch (error) {
core.error(`β Failed to close issue #${issueNumber}: ${error.message}`);
return false;
}
}
/**
* Process a parent issue and recursively check its parents
*/
async function processParentIssue(parentIssue, depth = 0) {
const indent = ' '.repeat(depth);
core.info('');
core.info(`${indent}${'='.repeat(50)}`);
core.info(`${indent}Processing Parent Issue (Depth: ${depth})`);
core.info(`${indent}${'='.repeat(50)}`);
core.info(`${indent}Issue: #${parentIssue.number} "${parentIssue.title}"`);
core.info(`${indent}Current State: ${parentIssue.state}`);
// If already closed, skip
if (parentIssue.state === 'CLOSED') {
core.info(`${indent}βοΈ Issue #${parentIssue.number} is already closed, skipping`);
return;
}
// Check if all sub-issues are closed
if (areAllSubIssuesClosed(parentIssue)) {
core.info(`${indent}β
All sub-issues of #${parentIssue.number} are closed!`);
// Close the parent issue
const reason = `Triggered by cascade from issue #${closedIssueNumber} at depth ${depth}`;
const closed = await closeIssue(parentIssue.number, reason);
if (closed) {
// Now check if this issue has parents and process them recursively
core.info(`${indent}πΌ Looking for parent issues of #${parentIssue.number}...`);
// Fetch fresh data to get the parent relationships
const updatedIssue = await getIssueWithRelationships(parentIssue.number);
const grandParents = updatedIssue.trackedInIssues.nodes;
if (grandParents.length > 0) {
core.info(`${indent}π Found ${grandParents.length} parent issue(s) to check`);
for (const grandParent of grandParents) {
core.info(`${indent}πΌ Walking up to parent #${grandParent.number}`);
// Fetch full details for the grandparent
const grandParentFull = await getIssueWithRelationships(grandParent.number);
// Recursively process the grandparent
await processParentIssue(grandParentFull, depth + 1);
}
} else {
core.info(`${indent}π No parent issues found. Reached top of the tree.`);
}
}
} else {
core.info(`${indent}βΈοΈ Not all sub-issues of #${parentIssue.number} are closed yet`);
core.info(`${indent} This issue will remain open until all sub-issues are completed`);
}
}
/**
* Main execution
*/
async function main() {
try {
core.info('π Starting parent issue closure check...');
core.info('');
// Get the closed issue with its relationships
const closedIssue = await getIssueWithRelationships(closedIssueNumber);
// Get parent issues (issues that track this one)
const parentIssues = closedIssue.trackedInIssues.nodes;
if (parentIssues.length === 0) {
core.info('βΉοΈ This issue has no parent issues');
core.info('β Nothing to do');
return;
}
core.info('');
core.info(`π Found ${parentIssues.length} parent issue(s) to check:`);
parentIssues.forEach(parent => {
core.info(` - #${parent.number}: "${parent.title}" [${parent.state}]`);
});
core.info('');
// Process each parent issue
for (const parentIssue of parentIssues) {
// Fetch full details for the parent including its sub-issues
const parentFull = await getIssueWithRelationships(parentIssue.number);
// Process this parent (and recursively its parents)
await processParentIssue(parentFull, 0);
}
core.info('');
core.info('=================================================');
core.info('β
Workflow completed successfully');
core.info('=================================================');
} catch (error) {
core.error('');
core.error('=================================================');
core.error('β Workflow failed with error');
core.error('=================================================');
core.error(`Error: ${error.message}`);
if (error.stack) {
core.error(`Stack trace: ${error.stack}`);
}
throw error;
}
}
// Run the main function
await main();