2026-04-22 17:28:53 -07:00
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { execSync } from 'node:child_process' ;
2026-04-23 07:57:37 -07:00
import { getMaintainers , execGh , getRepoInfo , updateSimulationCsv , getMaintainerWorkload } from './utils.js' ;
2026-04-22 17:28:53 -07:00
const EXECUTE_ACTIONS = process . env . EXECUTE_ACTIONS === 'true' ;
async function run() {
const { owner , repo } = getRepoInfo ( ) ;
console . log ( ` Triage Router starting for ${ owner } / ${ repo } ... (EXECUTE_ACTIONS= ${ EXECUTE_ACTIONS } ) ` ) ;
try {
const MAINTAINERS = await getMaintainers ( ) ;
2026-04-23 07:57:37 -07:00
const WORKLOAD = await getMaintainerWorkload ( ) ;
console . log ( ` Fetched ${ MAINTAINERS . length } maintainers and current workloads. ` ) ;
2026-04-22 17:28:53 -07:00
2026-04-23 07:57:37 -07:00
// 1. Fetch untriaged issues (Increase limit to process the backlog)
2026-04-22 17:28:53 -07:00
const query = `
query( $ owner: String!, $ repo: String!) {
repository(owner: $ owner, name: $ repo) {
2026-04-23 07:57:37 -07:00
issues(first: 1000, states: OPEN, labels: ["status/need-triage"], orderBy: {field: CREATED_AT, direction: ASC}) {
2026-04-22 17:28:53 -07:00
nodes {
number
title
body
author { login }
assignees(first: 1) { nodes { login } }
labels(first: 20) { nodes { name } }
}
}
}
}
` ;
let output ;
try {
output = execSync ( ` gh api graphql -F owner= ${ owner } -F repo= ${ repo } -f query=' ${ query } ' ` , { encoding : 'utf-8' , maxBuffer : 10 * 1024 * 1024 } ) ;
} catch ( err ) {
console . error ( 'Failed to fetch untriaged issues from GitHub:' , err ) ;
process . exit ( 1 ) ;
}
const data = JSON . parse ( output ) . data . repository ;
const issues = data . issues . nodes ;
const actions = [ ] ;
2026-04-23 07:57:37 -07:00
// Sort maintainers by workload (ascending)
const sortedMaintainers = MAINTAINERS
. filter ( m = > m !== 'TOTAL_MAINTAINERS' ) // safeguard
. sort ( ( a , b ) = > ( WORKLOAD [ a ] || 0 ) - ( WORKLOAD [ b ] || 0 ) ) ;
let mIndex = 0 ;
2026-04-22 17:28:53 -07:00
for ( const issue of issues ) {
if ( issue . assignees . nodes . length > 0 ) continue ;
const body = issue . body || '' ;
2026-04-23 07:57:37 -07:00
const title = issue . title . toLowerCase ( ) ;
// Better categorization
const labelsToAdd : string [ ] = [ ] ;
if ( title . includes ( 'bug' ) || body . toLowerCase ( ) . includes ( 'expected behavior' ) ) {
labelsToAdd . push ( 'type/bug' ) ;
} else if ( title . includes ( 'feature' ) || title . includes ( 'enhancement' ) || body . toLowerCase ( ) . includes ( 'proposed change' ) ) {
labelsToAdd . push ( 'type/feature' ) ;
}
2026-04-22 17:28:53 -07:00
// Low quality check
if ( body . length < 50 || title . length < 10 || ! body . includes ( '###' ) ) {
actions . push ( {
number : issue . number ,
type : 'needs-info' ,
2026-04-23 07:57:37 -07:00
labelsToAdd : [ 'status/needs-info' ] ,
labelsToRemove : [ 'status/need-triage' ] ,
2026-04-22 17:28:53 -07:00
comment : ` Hi @ ${ issue . author ? . login || 'author' } ! Thank you for the report. This issue seems to be missing some critical information or doesn't follow the template. Could you please provide more details? Labeling as 'status/needs-info' for now. `
} ) ;
continue ;
}
2026-04-23 07:57:37 -07:00
// Assign to the maintainer with the lowest workload
const assignee = sortedMaintainers [ mIndex % sortedMaintainers . length ] ;
mIndex ++ ;
// Increment local workload tracker to keep distribution even during this run
WORKLOAD [ assignee ] = ( WORKLOAD [ assignee ] || 0 ) + 1 ;
2026-04-22 17:28:53 -07:00
actions . push ( {
number : issue . number ,
type : 'assign' ,
assignee ,
2026-04-23 07:57:37 -07:00
labelsToAdd : [ . . . labelsToAdd , 'status/manual-triage' ] ,
labelsToRemove : [ 'status/need-triage' ] ,
comment : ` Automated Triage: Assigning to @ ${ assignee } based on current workload. Please categorize and set priority. `
2026-04-22 17:28:53 -07:00
} ) ;
}
// 2. Execute actions
const simulationUpdates = new Map < string , Record < string , string > > ( ) ;
for ( const action of actions ) {
try {
2026-04-23 07:57:37 -07:00
const addLabels = action . labelsToAdd ? . map ( l = > ` " ${ l } " ` ) . join ( ',' ) || '' ;
const removeLabels = action . labelsToRemove ? . map ( l = > ` " ${ l } " ` ) . join ( ',' ) || '' ;
let editCmd = ` issue edit ${ action . number } ` ;
if ( addLabels ) editCmd += ` --add-label ${ addLabels } ` ;
if ( removeLabels ) editCmd += ` --remove-label ${ removeLabels } ` ;
if ( action . assignee ) editCmd += ` --add-assignee " ${ action . assignee } " ` ;
await execGh ( editCmd , EXECUTE_ACTIONS ) ;
simulationUpdates . set ( action . number . toString ( ) , {
labels : action.labelsToAdd?.join ( ', ' ) || '' ,
assignee : action.assignee || ''
} ) ;
2026-04-22 17:28:53 -07:00
if ( action . comment ) {
2026-04-23 07:57:37 -07:00
await execGh ( ` issue comment ${ action . number } --body " ${ action . comment } " ` , EXECUTE_ACTIONS ) ;
2026-04-22 17:28:53 -07:00
}
} catch ( err ) {
console . error ( ` Failed to process issue # ${ action . number } : ` , err ) ;
}
}
// 3. Update simulation
await updateSimulationCsv ( 'issues-after.csv' , simulationUpdates ) ;
console . log ( ` Processed ${ actions . length } issues. ` ) ;
} catch ( err ) {
console . error ( 'Error in Triage Router:' , err ) ;
process . exit ( 1 ) ;
}
}
run ( ) ;