From dc9d954508bf22674565dafbfba903b999c8f359 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:33:39 +0000 Subject: [PATCH] Use Link headers for Adoptium pagination --- .../distributors/adopt-installer.test.ts | 10 +++--- .../distributors/semeru-installer.test.ts | 10 +++--- .../distributors/temurin-installer.test.ts | 10 +++--- __tests__/util.test.ts | 22 +++++++++++++ src/distributions/adopt/installer.ts | 28 +++++++--------- src/distributions/semeru/installer.ts | 31 +++++++----------- src/distributions/temurin/installer.ts | 32 ++++++++----------- src/util.ts | 22 +++++++++++++ 8 files changed, 92 insertions(+), 73 deletions(-) diff --git a/__tests__/distributors/adopt-installer.test.ts b/__tests__/distributors/adopt-installer.test.ts index ff477be0..fa96279d 100644 --- a/__tests__/distributors/adopt-installer.test.ts +++ b/__tests__/distributors/adopt-installer.test.ts @@ -136,22 +136,19 @@ describe('getAvailableVersions', () => { ); it('load available versions', async () => { + const nextPageUrl = + 'https://api.adoptopenjdk.net/v3/assets/version/%5B1.0,100.0%5D?page=1&page_size=20'; spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient .mockReturnValueOnce({ statusCode: 200, - headers: {}, + headers: {link: `<${nextPageUrl}>; rel="next"`}, result: manifestData as any }) .mockReturnValueOnce({ statusCode: 200, headers: {}, result: manifestData as any - }) - .mockReturnValueOnce({ - statusCode: 200, - headers: {}, - result: [] }); const distribution = new AdoptDistribution( @@ -166,6 +163,7 @@ describe('getAvailableVersions', () => { const availableVersions = await distribution['getAvailableVersions'](); expect(availableVersions).not.toBeNull(); expect(availableVersions.length).toBe(manifestData.length * 2); + expect(spyHttpClient).toHaveBeenNthCalledWith(2, nextPageUrl); }); it.each([ diff --git a/__tests__/distributors/semeru-installer.test.ts b/__tests__/distributors/semeru-installer.test.ts index 1c26c79a..d415e747 100644 --- a/__tests__/distributors/semeru-installer.test.ts +++ b/__tests__/distributors/semeru-installer.test.ts @@ -82,22 +82,19 @@ describe('getAvailableVersions', () => { ); it('load available versions', async () => { + const nextPageUrl = + 'https://api.adoptopenjdk.net/v3/assets/version/%5B1.0,100.0%5D?page=1&page_size=20'; spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient .mockReturnValueOnce({ statusCode: 200, - headers: {}, + headers: {link: `<${nextPageUrl}>; rel="next"`}, result: manifestData as any }) .mockReturnValueOnce({ statusCode: 200, headers: {}, result: manifestData as any - }) - .mockReturnValueOnce({ - statusCode: 200, - headers: {}, - result: [] }); const distribution = new SemeruDistribution({ @@ -109,6 +106,7 @@ describe('getAvailableVersions', () => { const availableVersions = await distribution['getAvailableVersions'](); expect(availableVersions).not.toBeNull(); expect(availableVersions.length).toBe(manifestData.length * 2); + expect(spyHttpClient).toHaveBeenNthCalledWith(2, nextPageUrl); }); it.each([ diff --git a/__tests__/distributors/temurin-installer.test.ts b/__tests__/distributors/temurin-installer.test.ts index 0c6ef3f5..93930179 100644 --- a/__tests__/distributors/temurin-installer.test.ts +++ b/__tests__/distributors/temurin-installer.test.ts @@ -93,22 +93,19 @@ describe('getAvailableVersions', () => { ); it('load available versions', async () => { + const nextPageUrl = + 'https://api.adoptium.net/v3/assets/version/%5B1.0,100.0%5D?page=1&page_size=20'; spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient .mockReturnValueOnce({ statusCode: 200, - headers: {}, + headers: {link: `<${nextPageUrl}>; rel="next"`}, result: manifestData as any }) .mockReturnValueOnce({ statusCode: 200, headers: {}, result: manifestData as any - }) - .mockReturnValueOnce({ - statusCode: 200, - headers: {}, - result: [] }); const distribution = new TemurinDistribution( @@ -123,6 +120,7 @@ describe('getAvailableVersions', () => { const availableVersions = await distribution['getAvailableVersions'](); expect(availableVersions).not.toBeNull(); expect(availableVersions.length).toBe(manifestData.length * 2); + expect(spyHttpClient).toHaveBeenNthCalledWith(2, nextPageUrl); }); it.each([ diff --git a/__tests__/util.test.ts b/__tests__/util.test.ts index 85b76069..a0c11de3 100644 --- a/__tests__/util.test.ts +++ b/__tests__/util.test.ts @@ -4,6 +4,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { convertVersionToSemver, + getNextPageUrlFromLinkHeader, getVersionFromFileContent, isVersionSatisfies, isCacheFeatureAvailable, @@ -85,6 +86,27 @@ describe('convertVersionToSemver', () => { }); }); +describe('getNextPageUrlFromLinkHeader', () => { + it.each([ + [ + { + link: '; rel="next"' + }, + 'https://api.adoptium.net/v3/info/release_versions?page=1&page_size=10' + ], + [ + { + Link: '; rel="last", ; rel="next"' + }, + 'https://example.com/next?page=2' + ], + [{link: '; rel="last"'}, null], + [undefined, null] + ])('returns %s -> %s', (headers, expected) => { + expect(getNextPageUrlFromLinkHeader(headers)).toBe(expected); + }); +}); + describe('getVersionFromFileContent', () => { describe('.sdkmanrc', () => { it.each([ diff --git a/src/distributions/adopt/installer.ts b/src/distributions/adopt/installer.ts index 34c1716c..834242c4 100644 --- a/src/distributions/adopt/installer.ts +++ b/src/distributions/adopt/installer.ts @@ -14,6 +14,7 @@ import { } from '../base-models'; import { extractJdkFile, + getNextPageUrlFromLinkHeader, getDownloadArchiveExtension, isVersionSatisfies, renameWinArchive @@ -125,30 +126,23 @@ export class AdoptDistribution extends JavaBase { `jvm_impl=${this.jvmImpl.toLowerCase()}` ].join('&'); - // need to iterate through all pages to retrieve the list of all versions - // Adopt API doesn't provide way to retrieve the count of pages to iterate so infinity loop - let page_index = 0; + const requestArguments = `${baseRequestArguments}&page_size=20&page=0`; + let availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`; const availableVersions: IAdoptAvailableVersions[] = []; - while (true) { - const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`; - const availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`; - if (core.isDebug() && page_index === 0) { - // url is identical except page_index so print it once for debug - core.debug( - `Gathering available versions from '${availableVersionsUrl}'` - ); - } + if (core.isDebug()) { + core.debug(`Gathering available versions from '${availableVersionsUrl}'`); + } - const paginationPage = ( - await this.http.getJson(availableVersionsUrl) - ).result; + while (availableVersionsUrl) { + const response = + await this.http.getJson(availableVersionsUrl); + const paginationPage = response.result; + availableVersionsUrl = getNextPageUrlFromLinkHeader(response.headers); if (paginationPage === null || paginationPage.length === 0) { - // break infinity loop because we have reached end of pagination break; } availableVersions.push(...paginationPage); - page_index++; } if (core.isDebug()) { diff --git a/src/distributions/semeru/installer.ts b/src/distributions/semeru/installer.ts index edb29480..f59f201d 100644 --- a/src/distributions/semeru/installer.ts +++ b/src/distributions/semeru/installer.ts @@ -7,6 +7,7 @@ import { import semver from 'semver'; import { extractJdkFile, + getNextPageUrlFromLinkHeader, getDownloadArchiveExtension, isVersionSatisfies, renameWinArchive @@ -155,32 +156,24 @@ export class SemeruDistribution extends JavaBase { `jvm_impl=openj9` ].join('&'); - // need to iterate through all pages to retrieve the list of all versions - // Adoptium API doesn't provide way to retrieve the count of pages to iterate so infinity loop - let page_index = 0; + const requestArguments = `${baseRequestArguments}&page_size=20&page=0`; + let availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`; const availableVersions: ISemeruAvailableVersions[] = []; - while (true) { - const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`; - const availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`; - if (core.isDebug() && page_index === 0) { - // url is identical except page_index so print it once for debug - core.debug( - `Gathering available versions from '${availableVersionsUrl}'` - ); - } + if (core.isDebug()) { + core.debug(`Gathering available versions from '${availableVersionsUrl}'`); + } - const paginationPage = ( - await this.http.getJson( - availableVersionsUrl - ) - ).result; + while (availableVersionsUrl) { + const response = await this.http.getJson( + availableVersionsUrl + ); + const paginationPage = response.result; + availableVersionsUrl = getNextPageUrlFromLinkHeader(response.headers); if (paginationPage === null || paginationPage.length === 0) { - // break infinity loop because we have reached end of pagination break; } availableVersions.push(...paginationPage); - page_index++; } if (core.isDebug()) { diff --git a/src/distributions/temurin/installer.ts b/src/distributions/temurin/installer.ts index 8d03de74..02c3552f 100644 --- a/src/distributions/temurin/installer.ts +++ b/src/distributions/temurin/installer.ts @@ -14,6 +14,7 @@ import { } from '../base-models'; import { extractJdkFile, + getNextPageUrlFromLinkHeader, getDownloadArchiveExtension, isVersionSatisfies, renameWinArchive @@ -123,32 +124,25 @@ export class TemurinDistribution extends JavaBase { `jvm_impl=${this.jvmImpl.toLowerCase()}` ].join('&'); - // need to iterate through all pages to retrieve the list of all versions - // Adoptium API doesn't provide way to retrieve the count of pages to iterate so infinity loop - let page_index = 0; + const requestArguments = `${baseRequestArguments}&page_size=20&page=0`; + let availableVersionsUrl = `https://api.adoptium.net/v3/assets/version/${versionRange}?${requestArguments}`; const availableVersions: ITemurinAvailableVersions[] = []; - while (true) { - const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`; - const availableVersionsUrl = `https://api.adoptium.net/v3/assets/version/${versionRange}?${requestArguments}`; - if (core.isDebug() && page_index === 0) { - // url is identical except page_index so print it once for debug - core.debug( - `Gathering available versions from '${availableVersionsUrl}'` - ); - } + if (core.isDebug()) { + core.debug(`Gathering available versions from '${availableVersionsUrl}'`); + } + + while (availableVersionsUrl) { + const response = await this.http.getJson( + availableVersionsUrl + ); + const paginationPage = response.result; + availableVersionsUrl = getNextPageUrlFromLinkHeader(response.headers); - const paginationPage = ( - await this.http.getJson( - availableVersionsUrl - ) - ).result; if (paginationPage === null || paginationPage.length === 0) { - // break infinity loop because we have reached end of pagination break; } availableVersions.push(...paginationPage); - page_index++; } if (core.isDebug()) { diff --git a/src/util.ts b/src/util.ts index 0325f7f4..43c004ec 100644 --- a/src/util.ts +++ b/src/util.ts @@ -201,6 +201,28 @@ export function getGitHubHttpHeaders(): OutgoingHttpHeaders { return headers; } +export function getNextPageUrlFromLinkHeader( + headers?: Record +): string | null { + if (!headers) { + return null; + } + + const linkHeader = headers.link ?? headers.Link; + if (!linkHeader) { + return null; + } + + const normalizedLinkHeader = Array.isArray(linkHeader) + ? linkHeader.join(',') + : linkHeader; + const nextLinkMatch = normalizedLinkHeader.match( + /<([^>]+)>\s*;\s*rel="?next"?/i + ); + + return nextLinkMatch?.[1] ?? null; +} + // Rename archive to add extension because after downloading // archive does not contain extension type and it leads to some issues // on Windows runners without PowerShell Core.