@@ -107,9 +107,7 @@ export function parsePRs(raw: GHPullRequestNode[]): PullRequest[] {
107107 }
108108 }
109109 }
110- const allApproved =
111- r . reviews . nodes . filter ( rv => rv . submittedAt ) . length > 0 &&
112- r . reviews . nodes . filter ( rv => rv . submittedAt ) . every ( rv => rv . state === 'APPROVED' ) ;
110+ const allApproved = sortedReviews . length > 0 && sortedReviews . every ( rv => rv . state === 'APPROVED' ) ;
113111 const state : 'open' | 'closed' = r . state === 'OPEN' ? 'open' : 'closed' ;
114112 let bucket : PullRequest [ 'bucket' ] ;
115113 if ( state === 'closed' ) {
@@ -280,12 +278,12 @@ function computeDistribution(field: string, items: (Issue | PullRequest)[]): Cha
280278 const sorted = [ ...counts . entries ( ) ] . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] ) . slice ( 0 , 15 ) ;
281279 return { labels : sorted . map ( ( [ l ] ) => l ) , values : sorted . map ( ( [ , v ] ) => v ) } ;
282280 } else if ( field === 'bucket' ) {
283- for ( const item of items . filter ( i => i . state === 'open' && ! isIssue ( i ) ) ) {
284- inc ( ( item as PullRequest ) . bucket ) ;
281+ for ( const item of items ) {
282+ if ( item . state === 'open' && ! isIssue ( item ) ) inc ( item . bucket ) ;
285283 }
286284 } else if ( field === 'linkedIssuePriority' ) {
287- for ( const item of items . filter ( i => ! isIssue ( i ) ) ) {
288- inc ( ( item as PullRequest ) . linkedIssuePriority ?? '(none)' ) ;
285+ for ( const item of items ) {
286+ if ( ! isIssue ( item ) ) inc ( item . linkedIssuePriority ?? '(none)' ) ;
289287 }
290288 }
291289
@@ -489,7 +487,7 @@ function computeTermFrequency(
489487 . slice ( 0 , 20 )
490488 . map ( ( [ term , count ] ) => ( { term, count } ) ) ;
491489
492- const usedLabels = new Set ( items . flatMap ( i => i . labels ) ) ;
490+ const usedLabels = new Set ( filtered . flatMap ( i => i . labels ) ) ;
493491 const allLabels = new Set ( items . flatMap ( i => i . labels ) ) ;
494492 const unusedLabels = [ ...allLabels ] . filter ( l => ! usedLabels . has ( l ) ) ;
495493
@@ -514,7 +512,7 @@ export function computePage(
514512 const now = new Date ( ) ;
515513 result . windowedStats = { } ;
516514 for ( const w of sec . windows ) {
517- const cutoff = new Date ( now . getTime ( ) - w . days * 24 * 60 * 60 * 1000 ) ;
515+ const cutoff = new Date ( now . getTime ( ) - w . days * MS_PER_DAY ) ;
518516 const filtered = items . filter ( i => i . created >= cutoff ) ;
519517 result . windowedStats [ w . label ] = computeStats ( sec . metrics , filtered ) ;
520518 }
@@ -548,37 +546,33 @@ export function computePage(
548546 } ;
549547}
550548
551- function computeCI ( runs : WorkflowRun [ ] ) : CIData {
552- // Pass rate helpers
553- function calcPassRates ( subset : WorkflowRun [ ] ) : { overall : number ; perWf : Record < string , number > } {
554- const overall =
555- subset . length > 0 ? Math . round ( ( subset . filter ( r => r . conclusion === 'success' ) . length / subset . length ) * 100 ) : 0 ;
556- const perWf : Record < string , number > = { } ;
557- const byW : Record < string , WorkflowRun [ ] > = { } ;
558- for ( const r of subset ) ( byW [ r . workflowName ] ??= [ ] ) . push ( r ) ;
559- for ( const [ name , wfRuns ] of Object . entries ( byW ) ) {
560- perWf [ name ] =
561- wfRuns . length > 0
562- ? Math . round ( ( wfRuns . filter ( r => r . conclusion === 'success' ) . length / wfRuns . length ) * 100 )
563- : 0 ;
564- }
565- return { overall, perWf } ;
566- }
549+ function groupBy < T > ( items : T [ ] , keyFn : ( item : T ) => string ) : Record < string , T [ ] > {
550+ const result : Record < string , T [ ] > = { } ;
551+ for ( const item of items ) ( result [ keyFn ( item ) ] ??= [ ] ) . push ( item ) ;
552+ return result ;
553+ }
567554
568- const allRates = calcPassRates ( runs ) ;
569- const overallPassRate = allRates . overall ;
570- const passRate = allRates . perWf ;
555+ function calcPassRates ( runs : WorkflowRun [ ] ) : { overall : number ; perWf : Record < string , number > } {
556+ const overall =
557+ runs . length > 0 ? Math . round ( ( runs . filter ( r => r . conclusion === 'success' ) . length / runs . length ) * 100 ) : 0 ;
558+ const perWf : Record < string , number > = { } ;
559+ for ( const [ name , wfRuns ] of Object . entries ( groupBy ( runs , r => r . workflowName ) ) ) {
560+ perWf [ name ] =
561+ wfRuns . length > 0 ? Math . round ( ( wfRuns . filter ( r => r . conclusion === 'success' ) . length / wfRuns . length ) * 100 ) : 0 ;
562+ }
563+ return { overall, perWf } ;
564+ }
571565
572- // Weekly timeline
566+ function buildCITimeline ( runs : WorkflowRun [ ] ) : CIData [ ' timeline' ] {
573567 const sorted = [ ...runs ] . sort ( ( a , b ) => a . created . getTime ( ) - b . created . getTime ( ) ) ;
574568 const start =
575- sorted . length > 0 ? new Date ( sorted [ 0 ] . created . getTime ( ) - sorted [ 0 ] . created . getDay ( ) * 86400000 ) : new Date ( ) ;
569+ sorted . length > 0 ? new Date ( sorted [ 0 ] . created . getTime ( ) - sorted [ 0 ] . created . getDay ( ) * MS_PER_DAY ) : new Date ( ) ;
576570 start . setHours ( 0 , 0 , 0 , 0 ) ;
577571 const end = sorted . length > 0 ? sorted [ sorted . length - 1 ] . created : new Date ( ) ;
578572 const timeline : CIData [ 'timeline' ] = [ ] ;
579573 const cur = new Date ( start ) ;
580574 while ( cur <= end ) {
581- const nxt = new Date ( cur . getTime ( ) + 7 * 86400000 ) ;
575+ const nxt = new Date ( cur . getTime ( ) + 7 * MS_PER_DAY ) ;
582576 const weekRuns = runs . filter ( r => r . created >= cur && r . created < nxt ) ;
583577 timeline . push ( {
584578 week : cur . toLocaleDateString ( 'en-US' , { month : 'short' , day : 'numeric' } ) ,
@@ -587,8 +581,10 @@ function computeCI(runs: WorkflowRun[]): CIData {
587581 } ) ;
588582 cur . setTime ( nxt . getTime ( ) ) ;
589583 }
584+ return timeline ;
585+ }
590586
591- // Failing jobs — aggregate across all runs
587+ function findFailingJobs ( runs : WorkflowRun [ ] ) : CIData [ 'failingJobs' ] {
592588 const jobStats : Record < string , { failures : number ; total : number } > = { } ;
593589 for ( const r of runs ) {
594590 for ( const j of r . jobs ) {
@@ -598,16 +594,15 @@ function computeCI(runs: WorkflowRun[]): CIData {
598594 if ( j . conclusion === 'failure' ) s . failures ++ ;
599595 }
600596 }
601- const failingJobs = Object . entries ( jobStats )
597+ return Object . entries ( jobStats )
602598 . filter ( ( [ , s ] ) => s . failures > 0 )
603599 . map ( ( [ job , s ] ) => ( { job, failures : s . failures , total : s . total , rate : Math . round ( ( s . failures / s . total ) * 100 ) } ) )
604600 . sort ( ( a , b ) => b . failures - a . failures ) ;
601+ }
605602
606- // Flaky detection — jobs that flip between pass/fail across consecutive runs per workflow
603+ function detectFlakyJobs ( runs : WorkflowRun [ ] ) : CIData [ 'flaky' ] {
607604 const flaky : CIData [ 'flaky' ] = [ ] ;
608- const byWf : Record < string , WorkflowRun [ ] > = { } ;
609- for ( const r of runs ) ( byWf [ r . workflowName ] ??= [ ] ) . push ( r ) ;
610- for ( const wfRuns of Object . values ( byWf ) ) {
605+ for ( const wfRuns of Object . values ( groupBy ( runs , r => r . workflowName ) ) ) {
611606 const chronological = [ ...wfRuns ] . sort ( ( a , b ) => a . created . getTime ( ) - b . created . getTime ( ) ) ;
612607 const jobHistory : Record < string , string [ ] > = { } ;
613608 for ( const r of chronological ) {
@@ -628,10 +623,11 @@ function computeCI(runs: WorkflowRun[]): CIData {
628623 }
629624 }
630625 }
631- flaky . sort ( ( a , b ) => b . flipCount - a . flipCount ) ;
626+ return flaky . sort ( ( a , b ) => b . flipCount - a . flipCount ) ;
627+ }
632628
633- // Recent failures
634- const recentFailures = runs
629+ function getRecentFailures ( runs : WorkflowRun [ ] ) : CIData [ 'recentFailures' ] {
630+ return runs
635631 . filter ( r => r . conclusion === 'failure' )
636632 . sort ( ( a , b ) => b . created . getTime ( ) - a . created . getTime ( ) )
637633 . slice ( 0 , 20 )
@@ -641,38 +637,47 @@ function computeCI(runs: WorkflowRun[]): CIData {
641637 date : r . created . toISOString ( ) . slice ( 0 , 16 ) . replace ( 'T' , ' ' ) ,
642638 failedJobs : r . jobs . filter ( j => j . conclusion === 'failure' ) . map ( j => j . name ) ,
643639 } ) ) ;
640+ }
644641
645- // Avg duration per job
642+ function calcAvgDuration ( runs : WorkflowRun [ ] ) : Record < string , number > {
646643 const jobDurations : Record < string , number [ ] > = { } ;
647644 for ( const r of runs ) {
648645 for ( const j of r . jobs ) {
649646 if ( j . durationMin > 0 ) ( jobDurations [ j . name ] ??= [ ] ) . push ( j . durationMin ) ;
650647 }
651648 }
652- const avgDuration : Record < string , number > = { } ;
649+ const result : Record < string , number > = { } ;
653650 for ( const [ job , durations ] of Object . entries ( jobDurations ) ) {
654- avgDuration [ job ] = Math . round ( ( durations . reduce ( ( a , b ) => a + b , 0 ) / durations . length ) * 10 ) / 10 ;
651+ result [ job ] = Math . round ( ( durations . reduce ( ( a , b ) => a + b , 0 ) / durations . length ) * 10 ) / 10 ;
655652 }
653+ return result ;
654+ }
655+
656+ function calcCIWindows ( runs : WorkflowRun [ ] ) : CIData [ 'windows' ] {
657+ return Object . fromEntries (
658+ [
659+ [ 'Past 24h' , 1 ] ,
660+ [ 'Past 7 days' , 7 ] ,
661+ [ 'Past 30 days' , 30 ] ,
662+ ] . map ( ( [ label , days ] ) => {
663+ const cutoff = new Date ( Date . now ( ) - ( days as number ) * MS_PER_DAY ) ;
664+ const subset = runs . filter ( r => r . created >= cutoff ) ;
665+ const rates = calcPassRates ( subset ) ;
666+ return [ label , { overallPassRate : rates . overall , passRate : rates . perWf } ] ;
667+ } )
668+ ) ;
669+ }
656670
671+ function computeCI ( runs : WorkflowRun [ ] ) : CIData {
672+ const { overall : overallPassRate , perWf : passRate } = calcPassRates ( runs ) ;
657673 return {
658674 overallPassRate,
659675 passRate,
660- timeline,
661- failingJobs,
662- flaky,
663- recentFailures,
664- avgDuration,
665- windows : Object . fromEntries (
666- [
667- [ 'Past 24h' , 1 ] ,
668- [ 'Past 7 days' , 7 ] ,
669- [ 'Past 30 days' , 30 ] ,
670- ] . map ( ( [ label , days ] ) => {
671- const cutoff = new Date ( Date . now ( ) - ( days as number ) * 86400000 ) ;
672- const subset = runs . filter ( r => r . created >= cutoff ) ;
673- const rates = calcPassRates ( subset ) ;
674- return [ label , { overallPassRate : rates . overall , passRate : rates . perWf } ] ;
675- } )
676- ) ,
676+ timeline : buildCITimeline ( runs ) ,
677+ failingJobs : findFailingJobs ( runs ) ,
678+ flaky : detectFlakyJobs ( runs ) ,
679+ recentFailures : getRecentFailures ( runs ) ,
680+ avgDuration : calcAvgDuration ( runs ) ,
681+ windows : calcCIWindows ( runs ) ,
677682 } ;
678683}
0 commit comments