@@ -257,11 +257,14 @@ type GitHubOpenPullRequestsResponse = {
257257 errors ?: Array < { message ?: string } > ;
258258} ;
259259
260+ type GitHubGraphQlPageInfo = { hasNextPage ?: boolean | null ; endCursor ?: string | null } | null ;
261+
260262type GitHubPullRequestDetailsResponse = {
261263 data ?: {
262264 repository ?: {
263265 pullRequest ?: {
264266 files ?: {
267+ pageInfo ?: GitHubGraphQlPageInfo ;
265268 nodes ?: Array < {
266269 path ?: string | null ;
267270 additions ?: number | null ;
@@ -270,6 +273,7 @@ type GitHubPullRequestDetailsResponse = {
270273 } | null > | null ;
271274 } | null ;
272275 reviews ?: {
276+ pageInfo ?: GitHubGraphQlPageInfo ;
273277 nodes ?: Array < {
274278 databaseId ?: number | null ;
275279 author ?: { login ?: string | null } | null ;
@@ -4137,6 +4141,12 @@ export async function fetchLinkedIssueFacts(
41374141 } ;
41384142}
41394143
4144+ // GraphQL's `files`/`reviews` connections default to first: 100 with no follow-up, so a PR with more than
4145+ // 100 changed files or more than 100 reviews silently truncates here too -- mirror PR_DETAIL_MAX_PAGES's
4146+ // REST bound with the same 10-page-of-100 (1,000 item) ceiling, walked independently per connection via
4147+ // `pageInfo { hasNextPage endCursor }` instead of a Link header.
4148+ const PR_DETAIL_GRAPHQL_MAX_PAGES = 10 ;
4149+
41404150async function fetchPullRequestDetailsFromGraphQl (
41414151 env : Env ,
41424152 repoFullName : string ,
@@ -4146,48 +4156,93 @@ async function fetchPullRequestDetailsFromGraphQl(
41464156) : Promise < { files : GitHubFilePayload [ ] ; reviews : GitHubReviewPayload [ ] } > {
41474157 /* v8 ignore start -- GitHub detail GraphQL sparse-node fallbacks are exercised through PR detail hydration tests. */
41484158 const { owner, name } = repoParts ( repoFullName ) ;
4149- const query = `query LoopOverPullRequestDetails {
4150- repository(owner: ${ JSON . stringify ( owner ) } , name: ${ JSON . stringify ( name ) } ) {
4151- pullRequest(number: ${ pullNumber } ) {
4152- files(first: 100) {
4159+ const files : GitHubFilePayload [ ] = [ ] ;
4160+ const reviews : GitHubReviewPayload [ ] = [ ] ;
4161+ let filesCursor : string | undefined ;
4162+ let reviewsCursor : string | undefined ;
4163+ let filesHasNextPage = true ;
4164+ let reviewsHasNextPage = true ;
4165+
4166+ for ( let page = 1 ; page <= PR_DETAIL_GRAPHQL_MAX_PAGES && ( filesHasNextPage || reviewsHasNextPage ) ; page += 1 ) {
4167+ // Only request a connection that still has more pages -- once one connection finishes, the other keeps
4168+ // paginating alone rather than re-fetching data already fully collected.
4169+ const filesSelection = filesHasNextPage
4170+ ? `files(first: 100${ filesCursor ? `, after: ${ JSON . stringify ( filesCursor ) } ` : "" } ) {
4171+ pageInfo { hasNextPage endCursor }
41534172 nodes { path additions deletions changeType }
4154- }
4155- reviews(first: 100) {
4173+ }`
4174+ : "" ;
4175+ const reviewsSelection = reviewsHasNextPage
4176+ ? `reviews(first: 100${ reviewsCursor ? `, after: ${ JSON . stringify ( reviewsCursor ) } ` : "" } ) {
4177+ pageInfo { hasNextPage endCursor }
41564178 nodes { databaseId author { login } state authorAssociation submittedAt }
4157- }
4179+ }`
4180+ : "" ;
4181+ const query = `query LoopOverPullRequestDetails {
4182+ repository(owner: ${ JSON . stringify ( owner ) } , name: ${ JSON . stringify ( name ) } ) {
4183+ pullRequest(number: ${ pullNumber } ) {
4184+ ${ filesSelection }
4185+ ${ reviewsSelection }
41584186 }
41594187 }
41604188 rateLimit { remaining resetAt }
41614189 }` ;
4162- const response = await githubGraphQl < GitHubPullRequestDetailsResponse > ( env , query , token , admissionKey ) ;
4163- const pullRequest = response . data ?. repository ?. pullRequest ;
4164- if ( ! pullRequest ) throw new GitHubApiError ( `GitHub GraphQL failed for ${ repoFullName } pull request #${ pullNumber } : pull request not found` , 404 , null , null , null , "" ) ;
4165- const files : GitHubFilePayload [ ] = ( pullRequest . files ?. nodes ?? [ ] ) . flatMap ( ( file ) => {
4166- if ( ! file ?. path ) return [ ] ;
4167- const additions = Number ( file . additions ?? 0 ) ;
4168- const deletions = Number ( file . deletions ?? 0 ) ;
4169- return [
4170- {
4171- filename : file . path ,
4172- status : String ( file . changeType ?? "modified" ) . toLowerCase ( ) ,
4173- additions,
4174- deletions,
4175- changes : additions + deletions ,
4176- } ,
4177- ] ;
4178- } ) ;
4179- const reviews : GitHubReviewPayload [ ] = ( pullRequest . reviews ?. nodes ?? [ ] ) . flatMap ( ( review ) => {
4180- if ( ! review ?. databaseId ) return [ ] ;
4181- return [
4182- {
4183- id : review . databaseId ,
4184- ...( review . author ?. login ? { user : { login : review . author . login } } : { } ) ,
4185- ...( review . state ? { state : review . state } : { } ) ,
4186- ...( review . authorAssociation ? { author_association : review . authorAssociation } : { } ) ,
4187- ...( review . submittedAt === undefined ? { } : { submitted_at : review . submittedAt } ) ,
4188- } ,
4189- ] ;
4190- } ) ;
4190+ // A page-1 failure surfaces the same way the un-paginated call always did (the caller's `.catch(() =>
4191+ // undefined)` falls through to the REST+GraphQL-failed warning). A LATER page failing must not discard
4192+ // the pages already collected -- same "keep a successful partial result" semantics as githubPaginatedList.
4193+ let response : GitHubPullRequestDetailsResponse | undefined ;
4194+ try {
4195+ response = await githubGraphQl < GitHubPullRequestDetailsResponse > ( env , query , token , admissionKey ) ;
4196+ } catch ( error ) {
4197+ if ( page === 1 ) throw error ;
4198+ break ;
4199+ }
4200+ const pullRequest = response . data ?. repository ?. pullRequest ;
4201+ if ( ! pullRequest ) {
4202+ if ( page === 1 ) throw new GitHubApiError ( `GitHub GraphQL failed for ${ repoFullName } pull request #${ pullNumber } : pull request not found` , 404 , null , null , null , "" ) ;
4203+ break ;
4204+ }
4205+ if ( filesHasNextPage ) {
4206+ files . push (
4207+ ...( pullRequest . files ?. nodes ?? [ ] ) . flatMap ( ( file ) => {
4208+ if ( ! file ?. path ) return [ ] ;
4209+ const additions = Number ( file . additions ?? 0 ) ;
4210+ const deletions = Number ( file . deletions ?? 0 ) ;
4211+ return [
4212+ {
4213+ filename : file . path ,
4214+ status : String ( file . changeType ?? "modified" ) . toLowerCase ( ) ,
4215+ additions,
4216+ deletions,
4217+ changes : additions + deletions ,
4218+ } ,
4219+ ] ;
4220+ } ) ,
4221+ ) ;
4222+ const pageInfo = pullRequest . files ?. pageInfo ;
4223+ if ( pageInfo ?. hasNextPage && pageInfo . endCursor ) filesCursor = pageInfo . endCursor ;
4224+ else filesHasNextPage = false ;
4225+ }
4226+ if ( reviewsHasNextPage ) {
4227+ reviews . push (
4228+ ...( pullRequest . reviews ?. nodes ?? [ ] ) . flatMap ( ( review ) => {
4229+ if ( ! review ?. databaseId ) return [ ] ;
4230+ return [
4231+ {
4232+ id : review . databaseId ,
4233+ ...( review . author ?. login ? { user : { login : review . author . login } } : { } ) ,
4234+ ...( review . state ? { state : review . state } : { } ) ,
4235+ ...( review . authorAssociation ? { author_association : review . authorAssociation } : { } ) ,
4236+ ...( review . submittedAt === undefined ? { } : { submitted_at : review . submittedAt } ) ,
4237+ } ,
4238+ ] ;
4239+ } ) ,
4240+ ) ;
4241+ const pageInfo = pullRequest . reviews ?. pageInfo ;
4242+ if ( pageInfo ?. hasNextPage && pageInfo . endCursor ) reviewsCursor = pageInfo . endCursor ;
4243+ else reviewsHasNextPage = false ;
4244+ }
4245+ }
41914246 return { files, reviews } ;
41924247 /* v8 ignore stop */
41934248}
0 commit comments