Use Link headers for Adoptium pagination

This commit is contained in:
copilot-swe-agent[bot] 2026-06-03 15:33:39 +00:00 committed by GitHub
parent b622de1dfa
commit dc9d954508
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 92 additions and 73 deletions

View File

@ -136,22 +136,19 @@ describe('getAvailableVersions', () => {
); );
it('load available versions', async () => { 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 = jest.spyOn(HttpClient.prototype, 'getJson');
spyHttpClient spyHttpClient
.mockReturnValueOnce({ .mockReturnValueOnce({
statusCode: 200, statusCode: 200,
headers: {}, headers: {link: `<${nextPageUrl}>; rel="next"`},
result: manifestData as any result: manifestData as any
}) })
.mockReturnValueOnce({ .mockReturnValueOnce({
statusCode: 200, statusCode: 200,
headers: {}, headers: {},
result: manifestData as any result: manifestData as any
})
.mockReturnValueOnce({
statusCode: 200,
headers: {},
result: []
}); });
const distribution = new AdoptDistribution( const distribution = new AdoptDistribution(
@ -166,6 +163,7 @@ describe('getAvailableVersions', () => {
const availableVersions = await distribution['getAvailableVersions'](); const availableVersions = await distribution['getAvailableVersions']();
expect(availableVersions).not.toBeNull(); expect(availableVersions).not.toBeNull();
expect(availableVersions.length).toBe(manifestData.length * 2); expect(availableVersions.length).toBe(manifestData.length * 2);
expect(spyHttpClient).toHaveBeenNthCalledWith(2, nextPageUrl);
}); });
it.each([ it.each([

View File

@ -82,22 +82,19 @@ describe('getAvailableVersions', () => {
); );
it('load available versions', async () => { 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 = jest.spyOn(HttpClient.prototype, 'getJson');
spyHttpClient spyHttpClient
.mockReturnValueOnce({ .mockReturnValueOnce({
statusCode: 200, statusCode: 200,
headers: {}, headers: {link: `<${nextPageUrl}>; rel="next"`},
result: manifestData as any result: manifestData as any
}) })
.mockReturnValueOnce({ .mockReturnValueOnce({
statusCode: 200, statusCode: 200,
headers: {}, headers: {},
result: manifestData as any result: manifestData as any
})
.mockReturnValueOnce({
statusCode: 200,
headers: {},
result: []
}); });
const distribution = new SemeruDistribution({ const distribution = new SemeruDistribution({
@ -109,6 +106,7 @@ describe('getAvailableVersions', () => {
const availableVersions = await distribution['getAvailableVersions'](); const availableVersions = await distribution['getAvailableVersions']();
expect(availableVersions).not.toBeNull(); expect(availableVersions).not.toBeNull();
expect(availableVersions.length).toBe(manifestData.length * 2); expect(availableVersions.length).toBe(manifestData.length * 2);
expect(spyHttpClient).toHaveBeenNthCalledWith(2, nextPageUrl);
}); });
it.each([ it.each([

View File

@ -93,22 +93,19 @@ describe('getAvailableVersions', () => {
); );
it('load available versions', async () => { 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 = jest.spyOn(HttpClient.prototype, 'getJson');
spyHttpClient spyHttpClient
.mockReturnValueOnce({ .mockReturnValueOnce({
statusCode: 200, statusCode: 200,
headers: {}, headers: {link: `<${nextPageUrl}>; rel="next"`},
result: manifestData as any result: manifestData as any
}) })
.mockReturnValueOnce({ .mockReturnValueOnce({
statusCode: 200, statusCode: 200,
headers: {}, headers: {},
result: manifestData as any result: manifestData as any
})
.mockReturnValueOnce({
statusCode: 200,
headers: {},
result: []
}); });
const distribution = new TemurinDistribution( const distribution = new TemurinDistribution(
@ -123,6 +120,7 @@ describe('getAvailableVersions', () => {
const availableVersions = await distribution['getAvailableVersions'](); const availableVersions = await distribution['getAvailableVersions']();
expect(availableVersions).not.toBeNull(); expect(availableVersions).not.toBeNull();
expect(availableVersions.length).toBe(manifestData.length * 2); expect(availableVersions.length).toBe(manifestData.length * 2);
expect(spyHttpClient).toHaveBeenNthCalledWith(2, nextPageUrl);
}); });
it.each([ it.each([

View File

@ -4,6 +4,7 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { import {
convertVersionToSemver, convertVersionToSemver,
getNextPageUrlFromLinkHeader,
getVersionFromFileContent, getVersionFromFileContent,
isVersionSatisfies, isVersionSatisfies,
isCacheFeatureAvailable, isCacheFeatureAvailable,
@ -85,6 +86,27 @@ describe('convertVersionToSemver', () => {
}); });
}); });
describe('getNextPageUrlFromLinkHeader', () => {
it.each([
[
{
link: '<https://api.adoptium.net/v3/info/release_versions?page=1&page_size=10>; rel="next"'
},
'https://api.adoptium.net/v3/info/release_versions?page=1&page_size=10'
],
[
{
Link: '<https://example.com/last?page=5>; rel="last", <https://example.com/next?page=2>; rel="next"'
},
'https://example.com/next?page=2'
],
[{link: '<https://example.com/last?page=5>; rel="last"'}, null],
[undefined, null]
])('returns %s -> %s', (headers, expected) => {
expect(getNextPageUrlFromLinkHeader(headers)).toBe(expected);
});
});
describe('getVersionFromFileContent', () => { describe('getVersionFromFileContent', () => {
describe('.sdkmanrc', () => { describe('.sdkmanrc', () => {
it.each([ it.each([

View File

@ -14,6 +14,7 @@ import {
} from '../base-models'; } from '../base-models';
import { import {
extractJdkFile, extractJdkFile,
getNextPageUrlFromLinkHeader,
getDownloadArchiveExtension, getDownloadArchiveExtension,
isVersionSatisfies, isVersionSatisfies,
renameWinArchive renameWinArchive
@ -125,30 +126,23 @@ export class AdoptDistribution extends JavaBase {
`jvm_impl=${this.jvmImpl.toLowerCase()}` `jvm_impl=${this.jvmImpl.toLowerCase()}`
].join('&'); ].join('&');
// need to iterate through all pages to retrieve the list of all versions const requestArguments = `${baseRequestArguments}&page_size=20&page=0`;
// Adopt API doesn't provide way to retrieve the count of pages to iterate so infinity loop let availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`;
let page_index = 0;
const availableVersions: IAdoptAvailableVersions[] = []; const availableVersions: IAdoptAvailableVersions[] = [];
while (true) { if (core.isDebug()) {
const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`; core.debug(`Gathering available versions from '${availableVersionsUrl}'`);
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}'`
);
}
const paginationPage = ( while (availableVersionsUrl) {
await this.http.getJson<IAdoptAvailableVersions[]>(availableVersionsUrl) const response =
).result; await this.http.getJson<IAdoptAvailableVersions[]>(availableVersionsUrl);
const paginationPage = response.result;
availableVersionsUrl = getNextPageUrlFromLinkHeader(response.headers);
if (paginationPage === null || paginationPage.length === 0) { if (paginationPage === null || paginationPage.length === 0) {
// break infinity loop because we have reached end of pagination
break; break;
} }
availableVersions.push(...paginationPage); availableVersions.push(...paginationPage);
page_index++;
} }
if (core.isDebug()) { if (core.isDebug()) {

View File

@ -7,6 +7,7 @@ import {
import semver from 'semver'; import semver from 'semver';
import { import {
extractJdkFile, extractJdkFile,
getNextPageUrlFromLinkHeader,
getDownloadArchiveExtension, getDownloadArchiveExtension,
isVersionSatisfies, isVersionSatisfies,
renameWinArchive renameWinArchive
@ -155,32 +156,24 @@ export class SemeruDistribution extends JavaBase {
`jvm_impl=openj9` `jvm_impl=openj9`
].join('&'); ].join('&');
// need to iterate through all pages to retrieve the list of all versions const requestArguments = `${baseRequestArguments}&page_size=20&page=0`;
// Adoptium API doesn't provide way to retrieve the count of pages to iterate so infinity loop let availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`;
let page_index = 0;
const availableVersions: ISemeruAvailableVersions[] = []; const availableVersions: ISemeruAvailableVersions[] = [];
while (true) { if (core.isDebug()) {
const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`; core.debug(`Gathering available versions from '${availableVersionsUrl}'`);
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}'`
);
}
const paginationPage = ( while (availableVersionsUrl) {
await this.http.getJson<ISemeruAvailableVersions[]>( const response = await this.http.getJson<ISemeruAvailableVersions[]>(
availableVersionsUrl availableVersionsUrl
) );
).result; const paginationPage = response.result;
availableVersionsUrl = getNextPageUrlFromLinkHeader(response.headers);
if (paginationPage === null || paginationPage.length === 0) { if (paginationPage === null || paginationPage.length === 0) {
// break infinity loop because we have reached end of pagination
break; break;
} }
availableVersions.push(...paginationPage); availableVersions.push(...paginationPage);
page_index++;
} }
if (core.isDebug()) { if (core.isDebug()) {

View File

@ -14,6 +14,7 @@ import {
} from '../base-models'; } from '../base-models';
import { import {
extractJdkFile, extractJdkFile,
getNextPageUrlFromLinkHeader,
getDownloadArchiveExtension, getDownloadArchiveExtension,
isVersionSatisfies, isVersionSatisfies,
renameWinArchive renameWinArchive
@ -123,32 +124,25 @@ export class TemurinDistribution extends JavaBase {
`jvm_impl=${this.jvmImpl.toLowerCase()}` `jvm_impl=${this.jvmImpl.toLowerCase()}`
].join('&'); ].join('&');
// need to iterate through all pages to retrieve the list of all versions const requestArguments = `${baseRequestArguments}&page_size=20&page=0`;
// Adoptium API doesn't provide way to retrieve the count of pages to iterate so infinity loop let availableVersionsUrl = `https://api.adoptium.net/v3/assets/version/${versionRange}?${requestArguments}`;
let page_index = 0;
const availableVersions: ITemurinAvailableVersions[] = []; const availableVersions: ITemurinAvailableVersions[] = [];
while (true) { if (core.isDebug()) {
const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`; core.debug(`Gathering available versions from '${availableVersionsUrl}'`);
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 while (availableVersionsUrl) {
core.debug( const response = await this.http.getJson<ITemurinAvailableVersions[]>(
`Gathering available versions from '${availableVersionsUrl}'` availableVersionsUrl
); );
} const paginationPage = response.result;
availableVersionsUrl = getNextPageUrlFromLinkHeader(response.headers);
const paginationPage = (
await this.http.getJson<ITemurinAvailableVersions[]>(
availableVersionsUrl
)
).result;
if (paginationPage === null || paginationPage.length === 0) { if (paginationPage === null || paginationPage.length === 0) {
// break infinity loop because we have reached end of pagination
break; break;
} }
availableVersions.push(...paginationPage); availableVersions.push(...paginationPage);
page_index++;
} }
if (core.isDebug()) { if (core.isDebug()) {

View File

@ -201,6 +201,28 @@ export function getGitHubHttpHeaders(): OutgoingHttpHeaders {
return headers; return headers;
} }
export function getNextPageUrlFromLinkHeader(
headers?: Record<string, string | string[] | undefined>
): 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 // Rename archive to add extension because after downloading
// archive does not contain extension type and it leads to some issues // archive does not contain extension type and it leads to some issues
// on Windows runners without PowerShell Core. // on Windows runners without PowerShell Core.