@@ -40,6 +40,7 @@ import type {
4040} from './types.ts' ;
4141
4242const githubBaseUrl = 'https://api.github.com/' ;
43+ const MAX_PAGINATION_PAGES = 100 ;
4344let baseUrl = githubBaseUrl ;
4445export function setBaseUrl ( url : string ) : void {
4546 baseUrl = url ;
@@ -333,6 +334,20 @@ function replaceUrlBase(url: URL, baseUrl: string): URL {
333334 return new URL ( relativeUrl , baseUrl ) ;
334335}
335336
337+ function resolvePaginationUrl (
338+ url : string ,
339+ baseUrl : string | undefined ,
340+ rebasePaginationLinks : boolean ,
341+ ) : URL {
342+ const parsedUrl = new URL ( url , baseUrl ) ;
343+ const rebasePagination =
344+ ! ! baseUrl &&
345+ rebasePaginationLinks &&
346+ // Preserve github.com URLs for use cases like release notes
347+ parsedUrl . origin !== 'https://api.github.com' ;
348+ return rebasePagination ? replaceUrlBase ( parsedUrl , baseUrl ) : parsedUrl ;
349+ }
350+
336351export class GithubHttp extends HttpBase < GithubHttpOptions > {
337352 protected override get baseUrl ( ) : string | undefined {
338353 return baseUrl ;
@@ -423,36 +438,79 @@ export class GithubHttp extends HttpBase<GithubHttpOptions> {
423438 const linkHeader = parseLinkHeader ( result ?. headers ?. link ) ;
424439 const next = linkHeader ?. next ;
425440 const env = getEnv ( ) ;
426- if ( next ?. url && linkHeader ?. last ?. page ) {
427- let lastPage = parseInt ( linkHeader . last . page , 10 ) ;
428- // v8 ignore else -- TODO: add test #40625
429- if ( ! env . RENOVATE_PAGINATE_ALL && httpOptions . paginate !== 'all' ) {
430- lastPage = Math . min ( pageLimit , lastPage ) ;
431- }
441+ if ( next ?. url ) {
432442 const baseUrl = httpOptions . baseUrl ?? this . baseUrl ;
433- const parsedUrl = new URL ( next . url , baseUrl ) ;
434- const rebasePagination =
435- ! ! baseUrl &&
436- ! ! env . RENOVATE_X_REBASE_PAGINATION_LINKS &&
437- // Preserve github.com URLs for use cases like release notes
438- parsedUrl . origin !== 'https://api.github.com' ;
439- const firstPageUrl = rebasePagination
440- ? replaceUrlBase ( parsedUrl , baseUrl )
441- : parsedUrl ;
443+ const rebasePaginationLinks = ! ! env . RENOVATE_X_REBASE_PAGINATION_LINKS ;
444+ const firstPageUrl = resolvePaginationUrl (
445+ next . url ,
446+ baseUrl ,
447+ rebasePaginationLinks ,
448+ ) ;
442449 // Don't follow a cross-origin request, unless we've been explicitly requested to do so with `RENOVATE_X_REBASE_PAGINATION_LINKS`
443450 if ( firstPageUrl . origin === resolvedUrl . origin ) {
444- const queue = [ ...range ( 2 , lastPage ) ] . map (
445- ( pageNumber ) => ( ) : Promise < HttpResponse < T > > => {
446- // copy before modifying searchParams
447- const nextUrl = parseUrl ( firstPageUrl . toString ( ) ) ! ;
448- nextUrl . searchParams . set ( 'page' , String ( pageNumber ) ) ;
449- return super . requestJsonUnsafe < T > ( method , {
450- ...opts ,
451- url : nextUrl ,
452- } ) ;
453- } ,
454- ) ;
455- const pages = await p . all ( queue ) ;
451+ let pages : HttpResponse < T > [ ] ;
452+ if ( linkHeader ?. last ?. page ) {
453+ logger . debug ( 'Using GitHub offset-based pagination' ) ;
454+ let lastPage = parseInt ( linkHeader . last . page , 10 ) ;
455+ // v8 ignore else -- TODO: add test #40625
456+ if ( ! env . RENOVATE_PAGINATE_ALL && httpOptions . paginate !== 'all' ) {
457+ lastPage = Math . min ( pageLimit , lastPage ) ;
458+ }
459+ const queue = [ ...range ( 2 , lastPage ) ] . map (
460+ ( pageNumber ) => ( ) : Promise < HttpResponse < T > > => {
461+ // copy before modifying searchParams
462+ const nextUrl = parseUrl ( firstPageUrl . toString ( ) ) ! ;
463+ nextUrl . searchParams . set ( 'page' , String ( pageNumber ) ) ;
464+ return super . requestJsonUnsafe < T > ( method , {
465+ ...opts ,
466+ url : nextUrl ,
467+ } ) ;
468+ } ,
469+ ) ;
470+ pages = await p . all ( queue ) ;
471+ } else {
472+ logger . debug ( 'Using GitHub cursor-based pagination' ) ;
473+ pages = [ ] ;
474+ const paginateAll =
475+ ! ! env . RENOVATE_PAGINATE_ALL || httpOptions . paginate === 'all' ;
476+ const cursorPageLimit = paginateAll
477+ ? MAX_PAGINATION_PAGES
478+ : pageLimit ;
479+ let nextUrl : URL | null = firstPageUrl ;
480+ let pageNumber = 2 ;
481+ for ( ; nextUrl && pageNumber <= cursorPageLimit ; pageNumber += 1 ) {
482+ if ( nextUrl . origin !== resolvedUrl . origin ) {
483+ logger . once . warn (
484+ {
485+ requestHost : resolvedUrl . host ,
486+ paginationHost : nextUrl . host ,
487+ } ,
488+ 'Ignoring cross-origin GitHub pagination link. Set RENOVATE_X_REBASE_PAGINATION_LINKS if this is a self-hosted instance that returns a different host in pagination links.' ,
489+ ) ;
490+ break ;
491+ }
492+ const nextPage : HttpResponse < T > =
493+ await super . requestJsonUnsafe < T > ( method , {
494+ ...opts ,
495+ url : nextUrl ,
496+ } ) ;
497+ pages . push ( nextPage ) ;
498+ const nextLink = parseLinkHeader ( nextPage . headers . link ) ?. next ;
499+ nextUrl = nextLink ?. url
500+ ? resolvePaginationUrl (
501+ nextLink . url ,
502+ baseUrl ,
503+ rebasePaginationLinks ,
504+ )
505+ : null ;
506+ }
507+ if ( paginateAll && nextUrl && pageNumber > cursorPageLimit ) {
508+ logger . warn (
509+ { maxPages : MAX_PAGINATION_PAGES } ,
510+ 'GitHub cursor pagination limit reached' ,
511+ ) ;
512+ }
513+ }
456514 // v8 ignore else -- TODO: add test #40625
457515 if ( httpOptions . paginationField && isPlainObject ( result . body ) ) {
458516 const paginatedResult = result . body [ httpOptions . paginationField ] ;
0 commit comments