| |
| |
| |
| |
| |
|
|
| module.exports = async ({ github, context, core }) => { |
| const extractJson = (raw) => { |
| if (!raw || raw === '[]' || raw === '') return []; |
| try { |
| |
| return JSON.parse(raw); |
| } catch { |
| |
| 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}`, |
| ); |
| } |
| } |
|
|
| |
| 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 []; |
| }; |
|
|
| |
| |
| |
| 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); |
| |
| 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 || []), |
| ]), |
| ]; |
|
|
| |
| if ( |
| entry.explanation && |
| !existing.explanation.includes(entry.explanation) |
| ) { |
| existing.explanation = existing.explanation |
| ? `${existing.explanation}\n\n${entry.explanation}` |
| : entry.explanation; |
| } |
|
|
| |
| if (entry.effort_analysis && !existing.effort_analysis) { |
| existing.effort_analysis = entry.effort_analysis; |
| } |
| } |
| } |
| }; |
|
|
| |
| 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 = []; |
|
|
| |
| 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}`, |
| ); |
| } |
|
|
| |
| 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'); |
| } |
|
|
| |
| |
|
|
| |
| 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/')), |
| ); |
| } |
|
|
| |
| 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]; |
| core.info( |
| `Issue #${issueNumber} has multiple ${prefix} labels suggested. Keeping "${winner}" and discarding others.`, |
| ); |
| labelsToAdd = labelsToAdd.filter( |
| (l) => !l.startsWith(prefix) || l === winner, |
| ); |
| } |
| } |
|
|
| |
| labelsToRemove = [...new Set(labelsToRemove)].filter( |
| (l) => !labelsToAdd.includes(l) && existingLabels.includes(l), |
| ); |
| labelsToAdd = [...new Set(labelsToAdd)].filter( |
| (l) => !existingLabels.includes(l), |
| ); |
|
|
| |
| 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(', ')}`, |
| ); |
| } |
|
|
| |
| 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}`); |
| } |
| } |
| } |
| }; |
|
|