5
0
mirror of https://gitea.com/actions/setup-java.git synced 2026-08-07 02:31:20 +00:00

Fix JetBrains Runtime release pagination (#1218)

* Fix JetBrains release pagination

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Update setup distribution

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Bruno Borges <brborges@microsoft.com>
This commit is contained in:
Julien Dubois
2026-08-05 18:32:52 +02:00
committed by GitHub
parent 2b61aea53d
commit fb58a661f3
3 changed files with 226 additions and 50 deletions
@@ -47,6 +47,24 @@ const core = await import('@actions/core');
const {JetBrainsDistribution} = const {JetBrainsDistribution} =
await import('../../src/distributions/jetbrains/installer.js'); await import('../../src/distributions/jetbrains/installer.js');
const {RetryingHttpClient} = await import('../../src/retrying-http-client.js'); const {RetryingHttpClient} = await import('../../src/retrying-http-client.js');
const {MAX_PAGINATION_PAGES} = await import('../../src/util.js');
const JETBRAINS_RELEASES_URL =
'https://api.github.com/repos/JetBrains/JetBrainsRuntime/releases?per_page=100';
function release(tagName: string, prerelease: boolean) {
return {
tag_name: tagName,
name: tagName,
prerelease
};
}
function nextPageHeader(page: number) {
return {
link: `<${JETBRAINS_RELEASES_URL}&page=${page}>; rel="next"`
};
}
function response( function response(
statusCode: number, statusCode: number,
@@ -110,6 +128,138 @@ describe('getAvailableVersions', () => {
expect(availableVersions.length).toBe(length); expect(availableVersions.length).toBe(length);
}, 10_000); }, 10_000);
it('continues a stable request after an all-prerelease page', async () => {
jest.spyOn(HttpClient.prototype, 'head').mockResolvedValue({
message: {statusCode: 200}
} as any);
spyHttpClient
.mockResolvedValueOnce({
statusCode: 200,
headers: nextPageHeader(2),
result: [release('jbr-release-26.0.0b1.1', true)]
})
.mockResolvedValueOnce({
statusCode: 200,
headers: {},
result: [release('jbr-release-21.0.11b1163.116', false)]
});
const distribution = new JetBrainsDistribution({
version: '21',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
});
const availableVersions = await distribution['getAvailableVersions']();
expect(availableVersions.map(version => version.tag_name)).toContain(
'jbr-release-21.0.11b1163.116'
);
expect(availableVersions.map(version => version.tag_name)).not.toContain(
'jbr-release-26.0.0b1.1'
);
expect(spyHttpClient).toHaveBeenCalledTimes(2);
});
it('continues an EA request after an all-stable page', async () => {
jest.spyOn(HttpClient.prototype, 'head').mockResolvedValue({
message: {statusCode: 200}
} as any);
spyHttpClient
.mockResolvedValueOnce({
statusCode: 200,
headers: nextPageHeader(2),
result: [release('jbr-release-21.0.11b1163.116', false)]
})
.mockResolvedValueOnce({
statusCode: 200,
headers: {},
result: [release('jbr-release-26.0.0b1.1', true)]
});
const distribution = new JetBrainsDistribution({
version: '26-ea',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
});
const availableVersions = await distribution['getAvailableVersions']();
expect(availableVersions.map(version => version.tag_name)).toEqual([
'jbr-release-26.0.0b1.1'
]);
expect(spyHttpClient).toHaveBeenCalledTimes(2);
});
it('stops pagination when a raw GitHub page is empty', async () => {
spyHttpClient
.mockResolvedValueOnce({
statusCode: 200,
headers: nextPageHeader(2),
result: [release('jbr-release-21.0.11b1163.116', false)]
})
.mockResolvedValueOnce({
statusCode: 200,
headers: nextPageHeader(3),
result: []
});
const distribution = new JetBrainsDistribution({
version: '26-ea',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
});
await distribution['getAvailableVersions']();
expect(spyHttpClient).toHaveBeenCalledTimes(2);
});
it('stops at the pagination safeguard', async () => {
spyHttpClient.mockResolvedValue({
statusCode: 200,
headers: nextPageHeader(2),
result: [release('jbr-release-21.0.11b1163.116', false)]
});
const distribution = new JetBrainsDistribution({
version: '26-ea',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
});
const availableVersions = await distribution['getAvailableVersions']();
expect(availableVersions).toEqual([]);
expect(spyHttpClient).toHaveBeenCalledTimes(MAX_PAGINATION_PAGES);
expect(core.warning).toHaveBeenCalledWith(
`Reached pagination safeguard limit (${MAX_PAGINATION_PAGES} pages) while listing JetBrains Runtime releases.`
);
});
it('ignores pagination links with an unexpected origin', async () => {
spyHttpClient.mockResolvedValueOnce({
statusCode: 200,
headers: {
link: '<https://example.com/releases?page=2>; rel="next"'
},
result: [release('jbr-release-21.0.11b1163.116', false)]
});
const distribution = new JetBrainsDistribution({
version: '26-ea',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
});
await distribution['getAvailableVersions']();
expect(spyHttpClient).toHaveBeenCalledTimes(1);
expect(core.warning).toHaveBeenCalledWith(
'Ignoring pagination link with unexpected origin: https://example.com/releases?page=2'
);
});
it('retries a GitHub rate limit using Retry-After', async () => { it('retries a GitHub rate limit using Retry-After', async () => {
spyHttpClient.mockRestore(); spyHttpClient.mockRestore();
const sleep = jest.fn(async () => undefined); const sleep = jest.fn(async () => undefined);
+28 -21
View File
@@ -25,6 +25,8 @@ export const modules = {
const JETBRAINS_RELEASES_URL = 'https://api.github.com/repos/JetBrains/JetBrainsRuntime/releases?per_page=100';
const GITHUB_API_ORIGIN = 'https://api.github.com';
class JetBrainsDistribution extends _base_installer_js__WEBPACK_IMPORTED_MODULE_4__/* .JavaBase */ .O { class JetBrainsDistribution extends _base_installer_js__WEBPACK_IMPORTED_MODULE_4__/* .JavaBase */ .O {
constructor(installerOptions) { constructor(installerOptions) {
super('JetBrains', installerOptions); super('JetBrains', installerOptions);
@@ -74,34 +76,39 @@ class JetBrainsDistribution extends _base_installer_js__WEBPACK_IMPORTED_MODULE_
if (_actions_core__WEBPACK_IMPORTED_MODULE_0__/* .isDebug */ ._o()) { if (_actions_core__WEBPACK_IMPORTED_MODULE_0__/* .isDebug */ ._o()) {
console.time('Retrieving available versions for JBR took'); // eslint-disable-line no-console console.time('Retrieving available versions for JBR took'); // eslint-disable-line no-console
} }
// need to iterate through all pages to retrieve the list of all versions
// GitHub API doesn't provide way to retrieve the count of pages to iterate so infinity loop
let page_index = 1;
const rawVersions = []; const rawVersions = [];
const bearerToken = process.env.GITHUB_TOKEN; const bearerToken = process.env.GITHUB_TOKEN;
while (true) { const requestHeaders = {};
const requestArguments = `per_page=100&page=${page_index}`; if (bearerToken) {
const requestHeaders = {}; requestHeaders['Authorization'] = `Bearer ${bearerToken}`;
if (bearerToken) { }
requestHeaders['Authorization'] = `Bearer ${bearerToken}`; let releasesUrl = JETBRAINS_RELEASES_URL;
} let pageCount = 0;
const rawUrl = `https://api.github.com/repos/JetBrains/JetBrainsRuntime/releases?${requestArguments}`; if (_actions_core__WEBPACK_IMPORTED_MODULE_0__/* .isDebug */ ._o()) {
if (_actions_core__WEBPACK_IMPORTED_MODULE_0__/* .isDebug */ ._o() && page_index === 1) { _actions_core__WEBPACK_IMPORTED_MODULE_0__/* .debug */ .Yz(`Gathering available versions from '${releasesUrl}'`);
// url is identical except page_index so print it once for debug }
_actions_core__WEBPACK_IMPORTED_MODULE_0__/* .debug */ .Yz(`Gathering available versions from '${rawUrl}'`); while (releasesUrl) {
} pageCount++;
const paginationPageResult = (await this.http.getJson(rawUrl, requestHeaders)).result; const response = await this.http.getJson(releasesUrl, requestHeaders);
const paginationPageResult = response.result;
if (!paginationPageResult || paginationPageResult.length === 0) { if (!paginationPageResult || paginationPageResult.length === 0) {
// break infinity loop because we have reached end of pagination
break; break;
} }
const paginationPage = paginationPageResult.filter(version => this.stable ? !version.prerelease : version.prerelease); rawVersions.push(...paginationPageResult.filter(version => this.stable ? !version.prerelease : version.prerelease));
if (!paginationPage || paginationPage.length === 0) { const nextUrl = (0,_util_js__WEBPACK_IMPORTED_MODULE_5__/* .getNextPageUrlFromLinkHeader */ .rC)(response.headers);
// break infinity loop because we have reached end of pagination if (nextUrl && !(0,_util_js__WEBPACK_IMPORTED_MODULE_5__/* .validatePaginationUrl */ .SA)(nextUrl, GITHUB_API_ORIGIN)) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__/* .warning */ .$e(`Ignoring pagination link with unexpected origin: ${nextUrl}`);
releasesUrl = null;
}
else {
releasesUrl = nextUrl;
}
if (pageCount >= _util_js__WEBPACK_IMPORTED_MODULE_5__/* .MAX_PAGINATION_PAGES */ .Tp) {
if (releasesUrl) {
_actions_core__WEBPACK_IMPORTED_MODULE_0__/* .warning */ .$e(`Reached pagination safeguard limit (${_util_js__WEBPACK_IMPORTED_MODULE_5__/* .MAX_PAGINATION_PAGES */ .Tp} pages) while listing JetBrains Runtime releases.`);
}
break; break;
} }
rawVersions.push(...paginationPage);
page_index++;
} }
if (this.stable) { if (this.stable) {
// Add versions not available from the API but are downloadable // Add versions not available from the API but are downloadable
+48 -29
View File
@@ -11,10 +11,21 @@ import {
JavaInstallerOptions, JavaInstallerOptions,
JavaInstallerResults JavaInstallerResults
} from '../base-models.js'; } from '../base-models.js';
import {cacheJdkDir, extractJdkFile, isVersionSatisfies} from '../../util.js'; import {
cacheJdkDir,
extractJdkFile,
getNextPageUrlFromLinkHeader,
isVersionSatisfies,
MAX_PAGINATION_PAGES,
validatePaginationUrl
} from '../../util.js';
import {OutgoingHttpHeaders} from 'http'; import {OutgoingHttpHeaders} from 'http';
import {HttpCodes} from '@actions/http-client'; import {HttpCodes} from '@actions/http-client';
const JETBRAINS_RELEASES_URL =
'https://api.github.com/repos/JetBrains/JetBrainsRuntime/releases?per_page=100';
const GITHUB_API_ORIGIN = 'https://api.github.com';
export class JetBrainsDistribution extends JavaBase { export class JetBrainsDistribution extends JavaBase {
constructor(installerOptions: JavaInstallerOptions) { constructor(installerOptions: JavaInstallerOptions) {
super('JetBrains', installerOptions); super('JetBrains', installerOptions);
@@ -96,46 +107,54 @@ export class JetBrainsDistribution extends JavaBase {
console.time('Retrieving available versions for JBR took'); // eslint-disable-line no-console console.time('Retrieving available versions for JBR took'); // eslint-disable-line no-console
} }
// need to iterate through all pages to retrieve the list of all versions
// GitHub API doesn't provide way to retrieve the count of pages to iterate so infinity loop
let page_index = 1;
const rawVersions: IJetBrainsRawVersion[] = []; const rawVersions: IJetBrainsRawVersion[] = [];
const bearerToken = process.env.GITHUB_TOKEN; const bearerToken = process.env.GITHUB_TOKEN;
const requestHeaders: OutgoingHttpHeaders = {};
if (bearerToken) {
requestHeaders['Authorization'] = `Bearer ${bearerToken}`;
}
let releasesUrl: string | null = JETBRAINS_RELEASES_URL;
let pageCount = 0;
while (true) { if (core.isDebug()) {
const requestArguments = `per_page=100&page=${page_index}`; core.debug(`Gathering available versions from '${releasesUrl}'`);
const requestHeaders: OutgoingHttpHeaders = {}; }
if (bearerToken) { while (releasesUrl) {
requestHeaders['Authorization'] = `Bearer ${bearerToken}`; pageCount++;
} const response = await this.http.getJson<IJetBrainsRawVersion[]>(
releasesUrl,
const rawUrl = `https://api.github.com/repos/JetBrains/JetBrainsRuntime/releases?${requestArguments}`; requestHeaders
);
if (core.isDebug() && page_index === 1) { const paginationPageResult = response.result;
// url is identical except page_index so print it once for debug
core.debug(`Gathering available versions from '${rawUrl}'`);
}
const paginationPageResult = (
await this.http.getJson<IJetBrainsRawVersion[]>(rawUrl, requestHeaders)
).result;
if (!paginationPageResult || paginationPageResult.length === 0) { if (!paginationPageResult || paginationPageResult.length === 0) {
// break infinity loop because we have reached end of pagination
break; break;
} }
const paginationPage: IJetBrainsRawVersion[] = rawVersions.push(
paginationPageResult.filter(version => ...paginationPageResult.filter(version =>
this.stable ? !version.prerelease : version.prerelease this.stable ? !version.prerelease : version.prerelease
)
);
const nextUrl = getNextPageUrlFromLinkHeader(response.headers);
if (nextUrl && !validatePaginationUrl(nextUrl, GITHUB_API_ORIGIN)) {
core.warning(
`Ignoring pagination link with unexpected origin: ${nextUrl}`
); );
if (!paginationPage || paginationPage.length === 0) { releasesUrl = null;
// break infinity loop because we have reached end of pagination } else {
break; releasesUrl = nextUrl;
} }
rawVersions.push(...paginationPage); if (pageCount >= MAX_PAGINATION_PAGES) {
page_index++; if (releasesUrl) {
core.warning(
`Reached pagination safeguard limit (${MAX_PAGINATION_PAGES} pages) while listing JetBrains Runtime releases.`
);
}
break;
}
} }
if (this.stable) { if (this.stable) {