@@ -396,78 +396,89 @@ async function graphqlSearchIssues(
396396 let cursor : string | null = null ;
397397
398398 while ( true ) {
399- let response : GraphQLIssueSearchResponse ;
400- let isPartial = false ;
401399 try {
402- response = await octokit . graphql < GraphQLIssueSearchResponse > (
403- ISSUES_SEARCH_QUERY ,
404- { q : queryString , cursor }
405- ) ;
406- } catch ( err ) {
407- // GraphqlResponseError contains partial data — extract valid nodes before recording error
408- const partial = extractGraphQLPartialData < GraphQLIssueSearchResponse > ( err ) ;
409- if ( partial ) {
410- response = partial ;
411- isPartial = true ;
412- const { message } = extractRejectionError ( err ) ;
413- errors . push ( {
414- repo : `search-batch-${ chunkIdx + 1 } /${ chunks . length } ` ,
415- statusCode : null ,
416- message,
417- retryable : true ,
418- } ) ;
419- } else {
420- const { statusCode, message } = extractRejectionError ( err ) ;
421- errors . push ( {
422- repo : `search-batch-${ chunkIdx + 1 } /${ chunks . length } ` ,
423- statusCode,
424- message,
425- retryable : statusCode === null || statusCode >= 500 ,
400+ let response : GraphQLIssueSearchResponse ;
401+ let isPartial = false ;
402+ try {
403+ response = await octokit . graphql < GraphQLIssueSearchResponse > (
404+ ISSUES_SEARCH_QUERY ,
405+ { q : queryString , cursor }
406+ ) ;
407+ } catch ( err ) {
408+ const partial = extractGraphQLPartialData < GraphQLIssueSearchResponse > ( err ) ;
409+ if ( partial ) {
410+ response = partial ;
411+ isPartial = true ;
412+ const { message } = extractRejectionError ( err ) ;
413+ errors . push ( {
414+ repo : `search-batch-${ chunkIdx + 1 } /${ chunks . length } ` ,
415+ statusCode : null ,
416+ message,
417+ retryable : true ,
418+ } ) ;
419+ } else {
420+ const { statusCode, message } = extractRejectionError ( err ) ;
421+ errors . push ( {
422+ repo : `search-batch-${ chunkIdx + 1 } /${ chunks . length } ` ,
423+ statusCode,
424+ message,
425+ retryable : statusCode === null || statusCode >= 500 ,
426+ } ) ;
427+ break ;
428+ }
429+ }
430+
431+ if ( response . rateLimit ) updateGraphqlRateLimit ( response . rateLimit ) ;
432+
433+ for ( const node of response . search . nodes ) {
434+ if ( ! node || node . databaseId == null || ! node . repository ) continue ;
435+ if ( seen . has ( node . databaseId ) ) continue ;
436+ seen . add ( node . databaseId ) ;
437+ issues . push ( {
438+ id : node . databaseId ,
439+ number : node . number ,
440+ title : node . title ,
441+ state : node . state ,
442+ htmlUrl : node . url ,
443+ createdAt : node . createdAt ,
444+ updatedAt : node . updatedAt ,
445+ userLogin : node . author ?. login ?? "" ,
446+ userAvatarUrl : node . author ?. avatarUrl ?? "" ,
447+ labels : node . labels . nodes . map ( ( l ) => ( { name : l . name , color : l . color } ) ) ,
448+ assigneeLogins : node . assignees . nodes . map ( ( a ) => a . login ) ,
449+ repoFullName : node . repository . nameWithOwner ,
450+ comments : node . comments . totalCount ,
426451 } ) ;
427- break ;
428452 }
429- }
430453
431- if ( response . rateLimit ) updateGraphqlRateLimit ( response . rateLimit ) ;
432-
433- for ( const node of response . search . nodes ) {
434- if ( ! node || node . databaseId == null || ! node . repository ) continue ;
435- if ( seen . has ( node . databaseId ) ) continue ;
436- seen . add ( node . databaseId ) ;
437- issues . push ( {
438- id : node . databaseId ,
439- number : node . number ,
440- title : node . title ,
441- state : node . state ,
442- htmlUrl : node . url ,
443- createdAt : node . createdAt ,
444- updatedAt : node . updatedAt ,
445- userLogin : node . author ?. login ?? "" ,
446- userAvatarUrl : node . author ?. avatarUrl ?? "" ,
447- labels : node . labels . nodes . map ( ( l ) => ( { name : l . name , color : l . color } ) ) ,
448- assigneeLogins : node . assignees . nodes . map ( ( a ) => a . login ) ,
449- repoFullName : node . repository . nameWithOwner ,
450- comments : node . comments . totalCount ,
451- } ) ;
452- }
454+ if ( isPartial ) break ;
453455
454- // Don't paginate after partial error — pageInfo may be unreliable
455- if ( isPartial ) break ;
456+ if ( issues . length >= 1000 && ! capReached ) {
457+ capReached = true ;
458+ const total = response . search . issueCount ;
459+ console . warn ( `[api] Issue search results capped at 1000 (${ total } total)` ) ;
460+ pushNotification (
461+ "search/issues" ,
462+ `Issue search results capped at 1,000 of ${ total . toLocaleString ( ) } total — some items are hidden` ,
463+ "warning"
464+ ) ;
465+ break ;
466+ }
456467
457- if ( issues . length >= 1000 && ! capReached ) {
458- capReached = true ;
459- const total = response . search . issueCount ;
460- console . warn ( `[api] Issue search results capped at 1000 (${ total } total)` ) ;
461- pushNotification (
462- "search/issues" ,
463- `Issue search results capped at 1,000 of ${ total . toLocaleString ( ) } total — some items are hidden` ,
464- "warning"
465- ) ;
468+ if ( ! response . search . pageInfo . hasNextPage || ! response . search . pageInfo . endCursor ) break ;
469+ cursor = response . search . pageInfo . endCursor ;
470+ } catch ( err ) {
471+ // Catch-all for unexpected runtime errors (malformed response shapes, TypeErrors, etc.)
472+ // Preserves any issues collected so far rather than losing the entire fetch
473+ const { message } = extractRejectionError ( err ) ;
474+ errors . push ( {
475+ repo : `search-batch-${ chunkIdx + 1 } /${ chunks . length } ` ,
476+ statusCode : null ,
477+ message,
478+ retryable : false ,
479+ } ) ;
466480 break ;
467481 }
468-
469- if ( ! response . search . pageInfo . hasNextPage || ! response . search . pageInfo . endCursor ) break ;
470- cursor = response . search . pageInfo . endCursor ;
471482 }
472483 }
473484
@@ -531,118 +542,125 @@ async function graphqlSearchPRs(
531542 let cursor : string | null = null ;
532543
533544 while ( true ) {
534- let response : GraphQLPRSearchResponse ;
535- let isPartial = false ;
536545 try {
537- response = await octokit . graphql < GraphQLPRSearchResponse > (
538- PR_SEARCH_QUERY ,
539- { q : queryString , cursor }
540- ) ;
541- } catch ( err ) {
542- const partial = extractGraphQLPartialData < GraphQLPRSearchResponse > ( err ) ;
543- if ( partial ) {
544- response = partial ;
545- isPartial = true ;
546- const { message } = extractRejectionError ( err ) ;
547- errors . push ( {
548- repo : `pr-search-batch-${ chunkIdx + 1 } /${ chunks . length } ` ,
549- statusCode : null ,
550- message,
551- retryable : true ,
552- } ) ;
553- } else {
554- const { statusCode, message } = extractRejectionError ( err ) ;
555- errors . push ( {
556- repo : `pr-search-batch-${ chunkIdx + 1 } /${ chunks . length } ` ,
557- statusCode,
558- message,
559- retryable : statusCode === null || statusCode >= 500 ,
560- } ) ;
561- break ;
546+ let response : GraphQLPRSearchResponse ;
547+ let isPartial = false ;
548+ try {
549+ response = await octokit . graphql < GraphQLPRSearchResponse > (
550+ PR_SEARCH_QUERY ,
551+ { q : queryString , cursor }
552+ ) ;
553+ } catch ( err ) {
554+ const partial = extractGraphQLPartialData < GraphQLPRSearchResponse > ( err ) ;
555+ if ( partial ) {
556+ response = partial ;
557+ isPartial = true ;
558+ const { message } = extractRejectionError ( err ) ;
559+ errors . push ( {
560+ repo : `pr-search-batch-${ chunkIdx + 1 } /${ chunks . length } ` ,
561+ statusCode : null ,
562+ message,
563+ retryable : true ,
564+ } ) ;
565+ } else {
566+ const { statusCode, message } = extractRejectionError ( err ) ;
567+ errors . push ( {
568+ repo : `pr-search-batch-${ chunkIdx + 1 } /${ chunks . length } ` ,
569+ statusCode,
570+ message,
571+ retryable : statusCode === null || statusCode >= 500 ,
572+ } ) ;
573+ break ;
574+ }
562575 }
563- }
564-
565- if ( response . rateLimit ) updateGraphqlRateLimit ( response . rateLimit ) ;
566576
567- for ( const node of response . search . nodes ) {
568- if ( ! node || node . databaseId == null || ! node . repository ) continue ;
569- if ( prMap . has ( node . databaseId ) ) continue ;
570-
571- const pendingLogins = node . reviewRequests . nodes
572- . map ( ( n ) => n . requestedReviewer ?. login )
573- . filter ( ( l ) : l is string => l != null ) ;
574- const actualLogins = node . latestReviews . nodes
575- . map ( ( n ) => n . author ?. login )
576- . filter ( ( l ) : l is string => l != null ) ;
577- // Normalize logins to lowercase to avoid case-sensitive duplicates
578- const reviewerLogins = [ ...new Set ( [ ...pendingLogins , ...actualLogins ] . map ( l => l . toLowerCase ( ) ) ) ] ;
579-
580- const rawState =
581- node . commits . nodes [ 0 ] ?. commit ?. statusCheckRollup ?. state ?? null ;
582- const checkStatus = mapCheckStatus ( rawState ) ;
583-
584- // Store headRepository info for fork detection
585- if ( node . headRepository ) {
586- const parts = node . headRepository . nameWithOwner . split ( "/" ) ;
587- if ( parts . length === 2 ) {
588- headRepoInfoMap . set ( node . databaseId , {
589- owner : node . headRepository . owner . login ,
590- repoName : parts [ 1 ] ,
591- } ) ;
577+ if ( response . rateLimit ) updateGraphqlRateLimit ( response . rateLimit ) ;
578+
579+ for ( const node of response . search . nodes ) {
580+ if ( ! node || node . databaseId == null || ! node . repository ) continue ;
581+ if ( prMap . has ( node . databaseId ) ) continue ;
582+
583+ const pendingLogins = node . reviewRequests . nodes
584+ . map ( ( n ) => n . requestedReviewer ?. login )
585+ . filter ( ( l ) : l is string => l != null ) ;
586+ const actualLogins = node . latestReviews . nodes
587+ . map ( ( n ) => n . author ?. login )
588+ . filter ( ( l ) : l is string => l != null ) ;
589+ const reviewerLogins = [ ...new Set ( [ ...pendingLogins , ...actualLogins ] . map ( l => l . toLowerCase ( ) ) ) ] ;
590+
591+ const rawState =
592+ node . commits . nodes [ 0 ] ?. commit ?. statusCheckRollup ?. state ?? null ;
593+ const checkStatus = mapCheckStatus ( rawState ) ;
594+
595+ if ( node . headRepository ) {
596+ const parts = node . headRepository . nameWithOwner . split ( "/" ) ;
597+ if ( parts . length === 2 ) {
598+ headRepoInfoMap . set ( node . databaseId , {
599+ owner : node . headRepository . owner . login ,
600+ repoName : parts [ 1 ] ,
601+ } ) ;
602+ } else {
603+ headRepoInfoMap . set ( node . databaseId , null ) ;
604+ }
592605 } else {
593- // Malformed nameWithOwner — treat as deleted fork (no fallback)
594606 headRepoInfoMap . set ( node . databaseId , null ) ;
595607 }
596- } else {
597- headRepoInfoMap . set ( node . databaseId , null ) ;
598- }
599608
600- prMap . set ( node . databaseId , {
601- id : node . databaseId ,
602- number : node . number ,
603- title : node . title ,
604- state : node . state ,
605- draft : node . isDraft ,
606- htmlUrl : node . url ,
607- createdAt : node . createdAt ,
608- updatedAt : node . updatedAt ,
609- userLogin : node . author ?. login ?? "" ,
610- userAvatarUrl : node . author ?. avatarUrl ?? "" ,
611- headSha : node . headRefOid ,
612- headRef : node . headRefName ,
613- baseRef : node . baseRefName ,
614- assigneeLogins : node . assignees . nodes . map ( ( a ) => a . login ) ,
615- reviewerLogins,
616- repoFullName : node . repository . nameWithOwner ,
617- checkStatus,
618- additions : node . additions ,
619- deletions : node . deletions ,
620- changedFiles : node . changedFiles ,
621- comments : node . comments . totalCount ,
622- reviewThreads : node . reviewThreads . totalCount ,
623- labels : node . labels . nodes . map ( ( l ) => ( { name : l . name , color : l . color } ) ) ,
624- reviewDecision : mapReviewDecision ( node . reviewDecision ) ,
625- totalReviewCount : node . latestReviews . totalCount ,
626- } ) ;
627- }
609+ prMap . set ( node . databaseId , {
610+ id : node . databaseId ,
611+ number : node . number ,
612+ title : node . title ,
613+ state : node . state ,
614+ draft : node . isDraft ,
615+ htmlUrl : node . url ,
616+ createdAt : node . createdAt ,
617+ updatedAt : node . updatedAt ,
618+ userLogin : node . author ?. login ?? "" ,
619+ userAvatarUrl : node . author ?. avatarUrl ?? "" ,
620+ headSha : node . headRefOid ,
621+ headRef : node . headRefName ,
622+ baseRef : node . baseRefName ,
623+ assigneeLogins : node . assignees . nodes . map ( ( a ) => a . login ) ,
624+ reviewerLogins,
625+ repoFullName : node . repository . nameWithOwner ,
626+ checkStatus,
627+ additions : node . additions ,
628+ deletions : node . deletions ,
629+ changedFiles : node . changedFiles ,
630+ comments : node . comments . totalCount ,
631+ reviewThreads : node . reviewThreads . totalCount ,
632+ labels : node . labels . nodes . map ( ( l ) => ( { name : l . name , color : l . color } ) ) ,
633+ reviewDecision : mapReviewDecision ( node . reviewDecision ) ,
634+ totalReviewCount : node . latestReviews . totalCount ,
635+ } ) ;
636+ }
628637
629- // Don't paginate after partial error — pageInfo may be unreliable
630- if ( isPartial ) break ;
638+ if ( isPartial ) break ;
639+
640+ if ( prMap . size >= 1000 && ! prCapReached ) {
641+ prCapReached = true ;
642+ const total = response . search . issueCount ;
643+ console . warn ( `[api] PR search results capped at 1000 (${ total } total)` ) ;
644+ pushNotification (
645+ "search/prs" ,
646+ `PR search results capped at 1,000 of ${ total . toLocaleString ( ) } total — some items are hidden` ,
647+ "warning"
648+ ) ;
649+ break ;
650+ }
631651
632- if ( prMap . size >= 1000 && ! prCapReached ) {
633- prCapReached = true ;
634- const total = response . search . issueCount ;
635- console . warn ( `[api] PR search results capped at 1000 (${ total } total)` ) ;
636- pushNotification (
637- "search/prs" ,
638- `PR search results capped at 1,000 of ${ total . toLocaleString ( ) } total — some items are hidden` ,
639- "warning"
640- ) ;
652+ if ( ! response . search . pageInfo . hasNextPage || ! response . search . pageInfo . endCursor ) break ;
653+ cursor = response . search . pageInfo . endCursor ;
654+ } catch ( err ) {
655+ const { message } = extractRejectionError ( err ) ;
656+ errors . push ( {
657+ repo : `pr-search-batch-${ chunkIdx + 1 } /${ chunks . length } ` ,
658+ statusCode : null ,
659+ message,
660+ retryable : false ,
661+ } ) ;
641662 break ;
642663 }
643-
644- if ( ! response . search . pageInfo . hasNextPage || ! response . search . pageInfo . endCursor ) break ;
645- cursor = response . search . pageInfo . endCursor ;
646664 }
647665 }
648666 }
0 commit comments