Skip to content

Commit 854f811

Browse files
authored
perf: optimize issue triage and lifecycle management (#27346)
1 parent c4d0e3c commit 854f811

2 files changed

Lines changed: 155 additions & 157 deletions

File tree

.github/scripts/apply-issue-labels.cjs

Lines changed: 131 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -5,86 +5,122 @@
55
*/
66

77
module.exports = async ({ github, context, core }) => {
8-
const rawLabels = process.env.LABELS_OUTPUT;
9-
core.info(`Raw labels JSON: ${rawLabels}`);
10-
let parsedLabels;
11-
try {
12-
// First, try to parse the raw output as JSON.
13-
parsedLabels = JSON.parse(rawLabels);
14-
} catch (jsonError) {
15-
// If that fails, check for a markdown code block.
16-
core.warning(
17-
`Direct JSON parsing failed: ${jsonError.message}. Trying to extract from a markdown block.`,
18-
);
19-
const jsonMatch = rawLabels.match(/```json\s*([\s\S]*?)\s*```/);
20-
if (jsonMatch && jsonMatch[1]) {
21-
try {
22-
parsedLabels = JSON.parse(jsonMatch[1].trim());
23-
} catch (markdownError) {
24-
core.setFailed(
25-
`Failed to parse JSON even after extracting from markdown block: ${markdownError.message}\nRaw output: ${rawLabels}`,
26-
);
27-
return;
8+
const extractJson = (raw) => {
9+
if (!raw || raw === '[]' || raw === '') return [];
10+
try {
11+
// First, try to parse the raw output as JSON.
12+
return JSON.parse(raw);
13+
} catch {
14+
// If that fails, check for a markdown code block.
15+
core.info(
16+
'Direct JSON parsing failed. Trying to extract from a markdown block.',
17+
);
18+
const jsonMatch = raw.match(/```json\s*([\s\S]*?)\s*```/);
19+
if (jsonMatch && jsonMatch[1]) {
20+
try {
21+
return JSON.parse(jsonMatch[1].trim());
22+
} catch (markdownError) {
23+
core.warning(
24+
`Failed to parse extracted JSON from markdown block: ${markdownError.message}`,
25+
);
26+
}
2827
}
29-
} else {
30-
// If no markdown block, try to find a raw JSON array in the output.
31-
// The CLI may include debug/log lines (e.g. telemetry init, YOLO mode)
32-
// before the actual JSON response.
33-
const jsonArrayMatch = rawLabels.match(
28+
29+
// Try to find a raw JSON array in the output.
30+
const jsonArrayMatch = raw.match(
3431
/\[\s*\{\s*"issue_number"[\s\S]*\}\s*\]/,
3532
);
3633
if (jsonArrayMatch) {
3734
try {
38-
parsedLabels = JSON.parse(jsonArrayMatch[0]);
39-
} catch (extractError) {
40-
// It's possible the regex matched from a `[STARTUP]` log all the way to the end
41-
// of the JSON array. We need to be more aggressive and find the FIRST `[ { "issue_number"`
42-
core.warning(
43-
`Strict array match failed: ${extractError.message}. Attempting to clean leading noisy brackets.`,
44-
);
45-
const fallbackMatch = rawLabels.match(
46-
/(\[\s*\{\s*"issue_number"[\s\S]*)/,
47-
);
35+
return JSON.parse(jsonArrayMatch[0]);
36+
} catch {
37+
const fallbackMatch = raw.match(/(\[\s*\{\s*"issue_number"[\s\S]*)/);
4838
if (fallbackMatch) {
4939
try {
50-
// We might have grabbed trailing noise too, so we find the last closing bracket
5140
const cleaned = fallbackMatch[0].substring(
5241
0,
5342
fallbackMatch[0].lastIndexOf(']') + 1,
5443
);
55-
parsedLabels = JSON.parse(cleaned);
44+
return JSON.parse(cleaned);
5645
} catch (fallbackError) {
57-
core.setFailed(
58-
`Found JSON-like content but failed to parse: ${fallbackError.message}\nRaw output: ${rawLabels}`,
46+
core.warning(
47+
`Failed to parse extracted JSON using fallback regex: ${fallbackError.message}`,
5948
);
60-
return;
6149
}
62-
} else {
63-
core.setFailed(
64-
`Found JSON-like content but failed to parse: ${extractError.message}\nRaw output: ${rawLabels}`,
65-
);
66-
return;
6750
}
6851
}
69-
} else {
70-
core.setFailed(
71-
`Output is not valid JSON and does not contain extractable JSON.\nRaw output: ${rawLabels}`,
72-
);
73-
return;
7452
}
7553
}
76-
}
77-
core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`);
54+
core.warning('No valid JSON could be extracted from input.');
55+
return [];
56+
};
7857

79-
for (const entry of parsedLabels) {
80-
const issueNumber = entry.issue_number;
81-
if (!issueNumber) {
82-
core.info(
83-
`Skipping entry with no issue number: ${JSON.stringify(entry)}`,
84-
);
85-
continue;
58+
// Collect all outputs from environment variables
59+
// Prioritize EFFORT results over STANDARD results by processing Effort FIRST
60+
// so that its labels appear first in the merged arrays (and thus win in mutually exclusive logic)
61+
const effortRaw = process.env.LABELS_OUTPUT_EFFORT;
62+
const standardRaw = process.env.LABELS_OUTPUT_STANDARD;
63+
const genericRaw = process.env.LABELS_OUTPUT;
64+
65+
const resultsByIssue = new Map();
66+
67+
const processResults = (results, _sourceName) => {
68+
for (const entry of results) {
69+
const issueNumber = entry.issue_number;
70+
if (!issueNumber) continue;
71+
72+
if (!resultsByIssue.has(issueNumber)) {
73+
resultsByIssue.set(issueNumber, {
74+
issue_number: issueNumber,
75+
labels_to_add: [...(entry.labels_to_add || [])],
76+
labels_to_remove: [...(entry.labels_to_remove || [])],
77+
explanation: entry.explanation || '',
78+
effort_analysis: entry.effort_analysis || '',
79+
});
80+
} else {
81+
const existing = resultsByIssue.get(issueNumber);
82+
// Combine labels
83+
existing.labels_to_add = [
84+
...new Set([
85+
...existing.labels_to_add,
86+
...(entry.labels_to_add || []),
87+
]),
88+
];
89+
existing.labels_to_remove = [
90+
...new Set([
91+
...existing.labels_to_remove,
92+
...(entry.labels_to_remove || []),
93+
]),
94+
];
95+
96+
// Combine explanations (if different)
97+
if (
98+
entry.explanation &&
99+
!existing.explanation.includes(entry.explanation)
100+
) {
101+
existing.explanation = existing.explanation
102+
? `${existing.explanation}\n\n${entry.explanation}`
103+
: entry.explanation;
104+
}
105+
106+
// Take effort analysis if present
107+
if (entry.effort_analysis && !existing.effort_analysis) {
108+
existing.effort_analysis = entry.effort_analysis;
109+
}
110+
}
86111
}
112+
};
113+
114+
// Order matters: Effort first so its labels win in conflict resolution
115+
processResults(extractJson(effortRaw), 'EFFORT');
116+
processResults(extractJson(standardRaw), 'STANDARD');
117+
processResults(extractJson(genericRaw), 'GENERIC');
87118

119+
const finalResults = Array.from(resultsByIssue.values());
120+
core.info(`Aggregated triage results for ${finalResults.length} issues.`);
121+
122+
for (const entry of finalResults) {
123+
const issueNumber = entry.issue_number;
88124
let labelsToAdd = entry.labels_to_add || [];
89125
let labelsToRemove = entry.labels_to_remove || [];
90126
let existingLabels = [];
@@ -131,84 +167,65 @@ module.exports = async ({ github, context, core }) => {
131167
labelsToAdd.includes('status/manual-triage') ||
132168
existingLabels.includes('status/manual-triage')
133169
) {
134-
// If the AI flagged it for manual triage, remove bot-triaged if it exists
135170
labelsToRemove.push('status/bot-triaged');
136-
// Ensure we don't accidentally try to add bot-triaged if the AI returned it
137171
labelsToAdd = labelsToAdd.filter((l) => l !== 'status/bot-triaged');
138172
} else {
139-
// Standard successful bot triage
140173
labelsToAdd.push('status/bot-triaged');
141174
}
142175

143-
// Deduplicate arrays
144-
labelsToAdd = [...new Set(labelsToAdd)];
145-
labelsToRemove = [...new Set(labelsToRemove)];
176+
// Resolve internal conflicts (e.g., adding P1 and P2)
177+
// We already resolved these by putting Effort first in the combined list
146178

147-
// Fetch existing labels to auto-resolve conflicts
148-
const hasNewArea = labelsToAdd.some((l) => l.startsWith('area/'));
149-
if (hasNewArea) {
150-
const existingAreas = existingLabels.filter((l) => l.startsWith('area/'));
151-
labelsToRemove.push(...existingAreas);
152-
}
153-
154-
const hasNewPriority = labelsToAdd.some((l) => l.startsWith('priority/'));
155-
if (hasNewPriority) {
156-
const existingPriorities = existingLabels.filter((l) =>
157-
l.startsWith('priority/'),
179+
// Resolve external conflicts with existing labels
180+
if (labelsToAdd.some((l) => l.startsWith('area/'))) {
181+
labelsToRemove.push(
182+
...existingLabels.filter((l) => l.startsWith('area/')),
158183
);
159-
labelsToRemove.push(...existingPriorities);
160-
}
161-
162-
const hasNewKind = labelsToAdd.some((l) => l.startsWith('kind/'));
163-
if (hasNewKind) {
164-
const existingKinds = existingLabels.filter((l) => l.startsWith('kind/'));
165-
labelsToRemove.push(...existingKinds);
166184
}
167-
168-
// Enforce mutually exclusive area labels
169-
const areaLabelsToAdd = labelsToAdd.filter((l) => l.startsWith('area/'));
170-
if (areaLabelsToAdd.length > 1) {
171-
core.warning(
172-
`Issue #${issueNumber} has multiple area labels to add: ${areaLabelsToAdd.join(', ')}. Keeping only the first one.`,
185+
if (labelsToAdd.some((l) => l.startsWith('priority/'))) {
186+
labelsToRemove.push(
187+
...existingLabels.filter((l) => l.startsWith('priority/')),
173188
);
174-
const firstArea = areaLabelsToAdd[0];
175-
labelsToAdd = labelsToAdd.filter(
176-
(l) => !l.startsWith('area/') || l === firstArea,
189+
}
190+
if (labelsToAdd.some((l) => l.startsWith('kind/'))) {
191+
labelsToRemove.push(
192+
...existingLabels.filter((l) => l.startsWith('kind/')),
177193
);
178194
}
179195

180-
// Enforce mutually exclusive priority labels
181-
const priorityLabelsToAdd = labelsToAdd.filter((l) =>
182-
l.startsWith('priority/'),
183-
);
184-
if (priorityLabelsToAdd.length > 1) {
185-
core.warning(
186-
`Issue #${issueNumber} has multiple priority labels to add: ${priorityLabelsToAdd.join(', ')}. Keeping only the first one.`,
187-
);
188-
const firstPriority = priorityLabelsToAdd[0];
189-
labelsToAdd = labelsToAdd.filter(
190-
(l) => !l.startsWith('priority/') || l === firstPriority,
191-
);
196+
// Enforce mutual exclusivity in the TO-ADD list (Architect wins)
197+
const exclusivePrefixes = ['area/', 'priority/', 'kind/'];
198+
for (const prefix of exclusivePrefixes) {
199+
const filtered = labelsToAdd.filter((l) => l.startsWith(prefix));
200+
if (filtered.length > 1) {
201+
const winner = filtered[0]; // First one wins
202+
core.info(
203+
`Issue #${issueNumber} has multiple ${prefix} labels suggested. Keeping "${winner}" and discarding others.`,
204+
);
205+
labelsToAdd = labelsToAdd.filter(
206+
(l) => !l.startsWith(prefix) || l === winner,
207+
);
208+
}
192209
}
193210

194-
// Re-deduplicate and filter out labels we are trying to add,
195-
// and filter out labels that are already present or absent to avoid unnecessary API calls
211+
// Final deduplication and cleanup
196212
labelsToRemove = [...new Set(labelsToRemove)].filter(
197213
(l) => !labelsToAdd.includes(l) && existingLabels.includes(l),
198214
);
199-
labelsToAdd = labelsToAdd.filter((l) => !existingLabels.includes(l));
215+
labelsToAdd = [...new Set(labelsToAdd)].filter(
216+
(l) => !existingLabels.includes(l),
217+
);
200218

219+
// Batch label operations
201220
if (labelsToAdd.length > 0) {
202221
await github.rest.issues.addLabels({
203222
owner: context.repo.owner,
204223
repo: context.repo.repo,
205224
issue_number: issueNumber,
206225
labels: labelsToAdd,
207226
});
208-
209-
const explanation = entry.explanation ? ` - ${entry.explanation}` : '';
210227
core.info(
211-
`Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}${explanation}`,
228+
`Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}`,
212229
);
213230
}
214231

@@ -222,32 +239,26 @@ module.exports = async ({ github, context, core }) => {
222239
name: label,
223240
});
224241
} catch (e) {
225-
if (e.status !== 404) {
242+
if (e.status !== 404)
226243
core.warning(
227244
`Failed to remove label ${label} from #${issueNumber}: ${e.message}`,
228245
);
229-
}
230246
}
231247
}
232248
core.info(
233249
`Successfully removed labels for #${issueNumber}: ${labelsToRemove.join(', ')}`,
234250
);
235251
}
236252

237-
// Restrictive Commenting Policy:
238-
// - Silence standard triage (Area/Kind/Priority) to avoid spam.
239-
// - Only comment if status/need-information is added (to explain what is missing).
240-
// - Only comment if effort_analysis is present (deep technical dive).
253+
// Post comment if needed
241254
const needsInfoAdded =
242255
labelsToAdd.includes('status/need-information') &&
243256
!existingLabels.includes('status/need-information');
244257
const hasEffortAnalysis = !!entry.effort_analysis;
245258

246259
if (needsInfoAdded || hasEffortAnalysis) {
247260
let commentBody = '';
248-
if (needsInfoAdded && entry.explanation) {
249-
commentBody += entry.explanation;
250-
}
261+
if (needsInfoAdded && entry.explanation) commentBody += entry.explanation;
251262
if (hasEffortAnalysis) {
252263
if (commentBody) commentBody += '\n\n';
253264
commentBody += `**Effort Analysis:**\n${entry.effort_analysis}`;
@@ -260,19 +271,8 @@ module.exports = async ({ github, context, core }) => {
260271
issue_number: issueNumber,
261272
body: commentBody,
262273
});
263-
core.info(
264-
`Posted required comment (need-info or effort) for #${issueNumber}`,
265-
);
274+
core.info(`Posted required comment for #${issueNumber}`);
266275
}
267276
}
268-
269-
if (
270-
(!entry.labels_to_add || entry.labels_to_add.length === 0) &&
271-
(!entry.labels_to_remove || entry.labels_to_remove.length === 0)
272-
) {
273-
core.info(
274-
`No labels to add or remove for #${issueNumber}, leaving as is`,
275-
);
276-
}
277277
}
278278
};

0 commit comments

Comments
 (0)