Skip to content

Commit 906f8a3

Browse files
authored
ci: robust stale issue lifecycle and consolidated triage labels (google-gemini#27015)
1 parent 64cb88d commit 906f8a3

7 files changed

Lines changed: 334 additions & 180 deletions

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

Lines changed: 87 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,50 @@ module.exports = async ({ github, context, core }) => {
8787

8888
let labelsToAdd = entry.labels_to_add || [];
8989
let labelsToRemove = entry.labels_to_remove || [];
90+
let existingLabels = [];
91+
92+
// Fetch existing labels early
93+
try {
94+
const { data: issueData } = await github.rest.issues.get({
95+
owner: context.repo.owner,
96+
repo: context.repo.repo,
97+
issue_number: issueNumber,
98+
});
99+
existingLabels = issueData.labels.map((l) =>
100+
typeof l === 'string' ? l : l.name,
101+
);
102+
} catch (e) {
103+
core.warning(
104+
`Failed to fetch existing labels for #${issueNumber}: ${e.message}`,
105+
);
106+
}
107+
108+
// Programmatic Priority Downgrade Logic
109+
if (labelsToAdd.includes('status/need-information')) {
110+
const targetPriority = labelsToAdd.find((l) => l.startsWith('priority/'));
111+
if (targetPriority) {
112+
let downgradedPriority = null;
113+
if (targetPriority === 'priority/p0')
114+
downgradedPriority = 'priority/p1';
115+
if (targetPriority === 'priority/p1')
116+
downgradedPriority = 'priority/p2';
117+
118+
if (downgradedPriority) {
119+
core.info(
120+
`Programmatically downgrading ${targetPriority} to ${downgradedPriority} due to status/need-information`,
121+
);
122+
labelsToAdd = labelsToAdd.filter((l) => l !== targetPriority);
123+
labelsToAdd.push(downgradedPriority);
124+
}
125+
}
126+
}
90127

91128
labelsToRemove.push('status/need-triage');
92129

93-
if (labelsToAdd.includes('status/manual-triage')) {
130+
if (
131+
labelsToAdd.includes('status/manual-triage') ||
132+
existingLabels.includes('status/manual-triage')
133+
) {
94134
// If the AI flagged it for manual triage, remove bot-triaged if it exists
95135
labelsToRemove.push('status/bot-triaged');
96136
// Ensure we don't accidentally try to add bot-triaged if the AI returned it
@@ -105,48 +145,24 @@ module.exports = async ({ github, context, core }) => {
105145
labelsToRemove = [...new Set(labelsToRemove)];
106146

107147
// Fetch existing labels to auto-resolve conflicts
108-
try {
109-
const { data: issueData } = await github.rest.issues.get({
110-
owner: context.repo.owner,
111-
repo: context.repo.repo,
112-
issue_number: issueNumber,
113-
});
114-
const existingLabels = issueData.labels.map((l) =>
115-
typeof l === 'string' ? l : l.name,
116-
);
117-
118-
const hasNewArea = labelsToAdd.some((l) => l.startsWith('area/'));
119-
if (hasNewArea) {
120-
const existingAreas = existingLabels.filter((l) =>
121-
l.startsWith('area/'),
122-
);
123-
labelsToRemove.push(...existingAreas);
124-
}
125-
126-
const hasNewPriority = labelsToAdd.some((l) => l.startsWith('priority/'));
127-
if (hasNewPriority) {
128-
const existingPriorities = existingLabels.filter((l) =>
129-
l.startsWith('priority/'),
130-
);
131-
labelsToRemove.push(...existingPriorities);
132-
}
133-
134-
const hasNewKind = labelsToAdd.some((l) => l.startsWith('kind/'));
135-
if (hasNewKind) {
136-
const existingKinds = existingLabels.filter((l) =>
137-
l.startsWith('kind/'),
138-
);
139-
labelsToRemove.push(...existingKinds);
140-
}
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+
}
141153

142-
// Re-deduplicate and filter out labels we are trying to add
143-
labelsToRemove = [...new Set(labelsToRemove)].filter(
144-
(l) => !labelsToAdd.includes(l),
145-
);
146-
} catch (e) {
147-
core.warning(
148-
`Failed to fetch existing labels for #${issueNumber}: ${e.message}`,
154+
const hasNewPriority = labelsToAdd.some((l) => l.startsWith('priority/'));
155+
if (hasNewPriority) {
156+
const existingPriorities = existingLabels.filter((l) =>
157+
l.startsWith('priority/'),
149158
);
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);
150166
}
151167

152168
// Enforce mutually exclusive area labels
@@ -175,6 +191,13 @@ module.exports = async ({ github, context, core }) => {
175191
);
176192
}
177193

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
196+
labelsToRemove = [...new Set(labelsToRemove)].filter(
197+
(l) => !labelsToAdd.includes(l) && existingLabels.includes(l),
198+
);
199+
labelsToAdd = labelsToAdd.filter((l) => !existingLabels.includes(l));
200+
178201
if (labelsToAdd.length > 0) {
179202
await github.rest.issues.addLabels({
180203
owner: context.repo.owner,
@@ -211,25 +234,36 @@ module.exports = async ({ github, context, core }) => {
211234
);
212235
}
213236

214-
if (
215-
(entry.explanation && process.env.SUPPRESS_COMMENT !== 'true') ||
216-
entry.effort_analysis
217-
) {
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).
241+
const needsInfoAdded =
242+
labelsToAdd.includes('status/need-information') &&
243+
!existingLabels.includes('status/need-information');
244+
const hasEffortAnalysis = !!entry.effort_analysis;
245+
246+
if (needsInfoAdded || hasEffortAnalysis) {
218247
let commentBody = '';
219-
if (entry.explanation && process.env.SUPPRESS_COMMENT !== 'true') {
248+
if (needsInfoAdded && entry.explanation) {
220249
commentBody += entry.explanation;
221250
}
222-
if (entry.effort_analysis) {
251+
if (hasEffortAnalysis) {
223252
if (commentBody) commentBody += '\n\n';
224253
commentBody += `**Effort Analysis:**\n${entry.effort_analysis}`;
225254
}
226255

227-
await github.rest.issues.createComment({
228-
owner: context.repo.owner,
229-
repo: context.repo.repo,
230-
issue_number: issueNumber,
231-
body: commentBody,
232-
});
256+
if (commentBody) {
257+
await github.rest.issues.createComment({
258+
owner: context.repo.owner,
259+
repo: context.repo.repo,
260+
issue_number: issueNumber,
261+
body: commentBody,
262+
});
263+
core.info(
264+
`Posted required comment (need-info or effort) for #${issueNumber}`,
265+
);
266+
}
233267
}
234268

235269
if (

.github/scripts/cleanup-triage-labels.cjs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,29 +22,53 @@ module.exports = async ({ github, context, core }) => {
2222

2323
for (const issue of issuesToCleanup) {
2424
try {
25-
await github.rest.issues.removeLabel({
25+
const { data: issueData } = await github.rest.issues.get({
2626
owner: context.repo.owner,
2727
repo: context.repo.repo,
2828
issue_number: issue.number,
29-
name: 'status/need-triage',
3029
});
31-
core.info(
32-
`Successfully removed status/need-triage from #${issue.number}`,
30+
31+
const labels = issueData.labels.map((l) =>
32+
typeof l === 'string' ? l : l.name,
3333
);
34-
} catch (error) {
35-
if (error.status === 404) {
34+
35+
if (
36+
labels.includes('status/bot-triaged') &&
37+
labels.includes('status/need-triage')
38+
) {
39+
await github.rest.issues.removeLabel({
40+
owner: context.repo.owner,
41+
repo: context.repo.repo,
42+
issue_number: issue.number,
43+
name: 'status/need-triage',
44+
});
3645
core.info(
37-
`Label status/need-triage not found on #${issue.number}, skipping.`,
46+
`Successfully removed status/need-triage from #${issue.number}`,
3847
);
39-
} else {
40-
core.warning(
41-
`Failed to remove label from #${issue.number}: ${error.message}`,
48+
}
49+
50+
if (
51+
labels.includes('status/bot-triaged') &&
52+
labels.includes('status/manual-triage')
53+
) {
54+
await github.rest.issues.removeLabel({
55+
owner: context.repo.owner,
56+
repo: context.repo.repo,
57+
issue_number: issue.number,
58+
name: 'status/bot-triaged',
59+
});
60+
core.info(
61+
`Successfully removed status/bot-triaged from #${issue.number} because it requires manual triage`,
4262
);
4363
}
64+
} catch (error) {
65+
core.warning(
66+
`Failed to clean up labels for #${issue.number}: ${error.message}`,
67+
);
4468
}
4569
}
4670

4771
core.info(
48-
`Cleaned up status/need-triage from ${issuesToCleanup.length} issues.`,
72+
`Cleaned up conflicting labels from ${issuesToCleanup.length} issues.`,
4973
);
5074
};

.github/scripts/find-conflicting-labels.cjs

Lines changed: 0 additions & 60 deletions
This file was deleted.

0 commit comments

Comments
 (0)