/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ module.exports = async ({ github, context, core }) => { const extractJson = (raw) => { if (!raw || raw === '[]' || raw === '') return []; try { // First, try to parse the raw output as JSON. return JSON.parse(raw); } catch { // If that fails, check for a markdown code block. core.info( 'Direct JSON parsing failed. Trying to extract from a markdown block.', ); const jsonMatch = raw.match(/```json\s*([\s\S]*?)\s*```/); if (jsonMatch && jsonMatch[1]) { try { return JSON.parse(jsonMatch[1].trim()); } catch (markdownError) { core.warning( `Failed to parse extracted JSON from markdown block: ${markdownError.message}`, ); } } // Try to find a raw JSON array in the output. const jsonArrayMatch = raw.match( /\[\s*\{\s*"issue_number"[\s\S]*\}\s*\]/, ); if (jsonArrayMatch) { try { return JSON.parse(jsonArrayMatch[0]); } catch { const fallbackMatch = raw.match(/(\[\s*\{\s*"issue_number"[\s\S]*)/); if (fallbackMatch) { try { const cleaned = fallbackMatch[0].substring( 0, fallbackMatch[0].lastIndexOf(']') + 1, ); return JSON.parse(cleaned); } catch (fallbackError) { core.warning( `Failed to parse extracted JSON using fallback regex: ${fallbackError.message}`, ); } } } } } core.warning('No valid JSON could be extracted from input.'); return []; }; // Collect all outputs from environment variables // Prioritize EFFORT results over STANDARD results by processing Effort FIRST // so that its labels appear first in the merged arrays (and thus win in mutually exclusive logic) const effortRaw = process.env.LABELS_OUTPUT_EFFORT; const standardRaw = process.env.LABELS_OUTPUT_STANDARD; const genericRaw = process.env.LABELS_OUTPUT; const resultsByIssue = new Map(); const processResults = (results, _sourceName) => { for (const entry of results) { const issueNumber = entry.issue_number; if (!issueNumber) continue; if (!resultsByIssue.has(issueNumber)) { resultsByIssue.set(issueNumber, { issue_number: issueNumber, labels_to_add: [...(entry.labels_to_add || [])], labels_to_remove: [...(entry.labels_to_remove || [])], explanation: entry.explanation || '', effort_analysis: entry.effort_analysis || '', }); } else { const existing = resultsByIssue.get(issueNumber); // Combine labels existing.labels_to_add = [ ...new Set([ ...existing.labels_to_add, ...(entry.labels_to_add || []), ]), ]; existing.labels_to_remove = [ ...new Set([ ...existing.labels_to_remove, ...(entry.labels_to_remove || []), ]), ]; // Combine explanations (if different) if ( entry.explanation && !existing.explanation.includes(entry.explanation) ) { existing.explanation = existing.explanation ? `${existing.explanation}\n\n${entry.explanation}` : entry.explanation; } // Take effort analysis if present if (entry.effort_analysis && !existing.effort_analysis) { existing.effort_analysis = entry.effort_analysis; } } } }; // Order matters: Effort first so its labels win in conflict resolution processResults(extractJson(effortRaw), 'EFFORT'); processResults(extractJson(standardRaw), 'STANDARD'); processResults(extractJson(genericRaw), 'GENERIC'); const finalResults = Array.from(resultsByIssue.values()); core.info(`Aggregated triage results for ${finalResults.length} issues.`); for (const entry of finalResults) { const issueNumber = entry.issue_number; let labelsToAdd = entry.labels_to_add || []; let labelsToRemove = entry.labels_to_remove || []; let existingLabels = []; // Fetch existing labels early try { const { data: issueData } = await github.rest.issues.get({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, }); existingLabels = issueData.labels.map((l) => typeof l === 'string' ? l : l.name, ); } catch (e) { core.warning( `Failed to fetch existing labels for #${issueNumber}: ${e.message}`, ); } // Programmatic Priority Downgrade Logic if (labelsToAdd.includes('status/need-information')) { const targetPriority = labelsToAdd.find((l) => l.startsWith('priority/')); if (targetPriority) { let downgradedPriority = null; if (targetPriority === 'priority/p0') downgradedPriority = 'priority/p1'; if (targetPriority === 'priority/p1') downgradedPriority = 'priority/p2'; if (downgradedPriority) { core.info( `Programmatically downgrading ${targetPriority} to ${downgradedPriority} due to status/need-information`, ); labelsToAdd = labelsToAdd.filter((l) => l !== targetPriority); labelsToAdd.push(downgradedPriority); } } } labelsToRemove.push('status/need-triage'); if ( labelsToAdd.includes('status/manual-triage') || existingLabels.includes('status/manual-triage') ) { labelsToRemove.push('status/bot-triaged'); labelsToAdd = labelsToAdd.filter((l) => l !== 'status/bot-triaged'); } else { labelsToAdd.push('status/bot-triaged'); } // Resolve internal conflicts (e.g., adding P1 and P2) // We already resolved these by putting Effort first in the combined list // Resolve external conflicts with existing labels if (labelsToAdd.some((l) => l.startsWith('area/'))) { labelsToRemove.push( ...existingLabels.filter((l) => l.startsWith('area/')), ); } if (labelsToAdd.some((l) => l.startsWith('priority/'))) { labelsToRemove.push( ...existingLabels.filter((l) => l.startsWith('priority/')), ); } if (labelsToAdd.some((l) => l.startsWith('kind/'))) { labelsToRemove.push( ...existingLabels.filter((l) => l.startsWith('kind/')), ); } // Enforce mutual exclusivity in the TO-ADD list (Architect wins) const exclusivePrefixes = ['area/', 'priority/', 'kind/']; for (const prefix of exclusivePrefixes) { const filtered = labelsToAdd.filter((l) => l.startsWith(prefix)); if (filtered.length > 1) { const winner = filtered[0]; // First one wins core.info( `Issue #${issueNumber} has multiple ${prefix} labels suggested. Keeping "${winner}" and discarding others.`, ); labelsToAdd = labelsToAdd.filter( (l) => !l.startsWith(prefix) || l === winner, ); } } // Final deduplication and cleanup labelsToRemove = [...new Set(labelsToRemove)].filter( (l) => !labelsToAdd.includes(l) && existingLabels.includes(l), ); labelsToAdd = [...new Set(labelsToAdd)].filter( (l) => !existingLabels.includes(l), ); // Batch label operations if (labelsToAdd.length > 0) { await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, labels: labelsToAdd, }); core.info( `Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}`, ); } if (labelsToRemove.length > 0) { for (const label of labelsToRemove) { try { await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, name: label, }); } catch (e) { if (e.status !== 404) core.warning( `Failed to remove label ${label} from #${issueNumber}: ${e.message}`, ); } } core.info( `Successfully removed labels for #${issueNumber}: ${labelsToRemove.join(', ')}`, ); } // Post comment if needed const needsInfoAdded = labelsToAdd.includes('status/need-information') && !existingLabels.includes('status/need-information'); const hasEffortAnalysis = !!entry.effort_analysis; if (needsInfoAdded || hasEffortAnalysis) { let commentBody = ''; if (needsInfoAdded && entry.explanation) commentBody += entry.explanation; if (hasEffortAnalysis) { if (commentBody) commentBody += '\n\n'; commentBody += `**Effort Analysis:**\n${entry.effort_analysis}`; } if (commentBody) { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, body: commentBody, }); core.info(`Posted required comment for #${issueNumber}`); } } } };