From 43120bc3c3d8598724a29b0c856f33e1151591f3 Mon Sep 17 00:00:00 2001 From: John <1615532+johnoliver@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:50:16 +0100 Subject: [PATCH 1/2] Implement pagination with link headers for Adoptium based apis (#1014) * Use Link headers for Adoptium pagination * Fix nullable pagination URL types and rebuild dist * Add 1000-page safeguard for JetBrains pagination * Adjust plan for pagination safeguard scope * Move pagination safeguard to non-JetBrains installers * Add 1000-page safeguard to Adopt Temurin and Semeru pagination * Fix Prettier formatting in adopt, semeru, and temurin installer files * Fix CI audit failure by updating vulnerable transitive deps * Address PR review: RFC-compliant Link parsing, SSRF validation, centralized constant - Make getNextPageUrlFromLinkHeader RFC 8288 compliant by splitting link-values and checking for rel=next anywhere in the parameters, not just as the first parameter after the semicolon. - Add validatePaginationUrl utility to reject pagination URLs that point to unexpected origins (SSRF mitigation). - Centralize MAX_PAGINATION_PAGES in util.ts instead of duplicating across Adopt, Semeru, and Temurin installers. - Add tests for rel not being the first parameter, and for URL origin validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address code review feedback on pagination implementation - Tighten rel regex with word boundary to prevent false positives (e.g., rel="nextsomething" no longer matches). - Use parsed.origin comparison in validatePaginationUrl to correctly handle explicit default ports (e.g., :443 for HTTPS). - Fix pagination safeguard tests to use same-origin URLs so they actually exercise the 1000-page limit instead of being rejected by origin validation on the first request. - Add test for rel="nextsomething" not matching. - Add test for explicit default port acceptance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix prettier formatting in util.test.ts * Rebuild dist/ to fix check-dist CI failure --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../distributors/adopt-installer.test.ts | 40 ++++- .../distributors/semeru-installer.test.ts | 37 ++++- .../distributors/temurin-installer.test.ts | 40 ++++- __tests__/util.test.ts | 76 ++++++++- dist/cleanup/index.js | 43 +++++- dist/setup/index.js | 145 +++++++++++++----- package-lock.json | 15 ++ src/distributions/adopt/installer.ts | 53 +++++-- src/distributions/semeru/installer.ts | 53 ++++--- src/distributions/temurin/installer.ts | 54 ++++--- src/util.ts | 49 ++++++ 11 files changed, 495 insertions(+), 110 deletions(-) diff --git a/__tests__/distributors/adopt-installer.test.ts b/__tests__/distributors/adopt-installer.test.ts index ff477be05..55649255b 100644 --- a/__tests__/distributors/adopt-installer.test.ts +++ b/__tests__/distributors/adopt-installer.test.ts @@ -14,6 +14,7 @@ import * as core from '@actions/core'; describe('getAvailableVersions', () => { let spyHttpClient: jest.SpyInstance; let spyCoreError: jest.SpyInstance; + let spyCoreWarning: jest.SpyInstance; beforeEach(() => { spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); @@ -26,6 +27,8 @@ describe('getAvailableVersions', () => { // Mock core.error to suppress error logs spyCoreError = jest.spyOn(core, 'error'); spyCoreError.mockImplementation(() => {}); + spyCoreWarning = jest.spyOn(core, 'warning'); + spyCoreWarning.mockImplementation(() => {}); }); afterEach(() => { @@ -136,22 +139,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 +166,34 @@ describe('getAvailableVersions', () => { const availableVersions = await distribution['getAvailableVersions'](); expect(availableVersions).not.toBeNull(); expect(availableVersions.length).toBe(manifestData.length * 2); + expect(spyHttpClient).toHaveBeenNthCalledWith(2, nextPageUrl); + }); + + it('stops pagination after 1000 pages as a safeguard', async () => { + const nextPageUrl = + 'https://api.adoptopenjdk.net/v3/assets/version/%5B1.0,100.0%5D?page=2&page_size=20'; + spyHttpClient.mockReturnValue({ + statusCode: 200, + headers: {link: `<${nextPageUrl}>; rel="next"`}, + result: [{version_data: {semver: '17.0.1'}, binaries: []}] as any + }); + + const distribution = new AdoptDistribution( + { + version: '11', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }, + AdoptImplementation.Hotspot + ); + + await distribution['getAvailableVersions'](); + + expect(spyHttpClient).toHaveBeenCalledTimes(1000); + expect(spyCoreWarning).toHaveBeenCalledWith( + expect.stringContaining('Reached pagination safeguard limit (1000 pages)') + ); }); it.each([ diff --git a/__tests__/distributors/semeru-installer.test.ts b/__tests__/distributors/semeru-installer.test.ts index 1c26c79a3..03b08b728 100644 --- a/__tests__/distributors/semeru-installer.test.ts +++ b/__tests__/distributors/semeru-installer.test.ts @@ -9,6 +9,7 @@ import * as core from '@actions/core'; describe('getAvailableVersions', () => { let spyHttpClient: jest.SpyInstance; let spyCoreError: jest.SpyInstance; + let spyCoreWarning: jest.SpyInstance; beforeEach(() => { spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); @@ -20,6 +21,8 @@ describe('getAvailableVersions', () => { // Mock core.error to suppress error logs spyCoreError = jest.spyOn(core, 'error'); spyCoreError.mockImplementation(() => {}); + spyCoreWarning = jest.spyOn(core, 'warning'); + spyCoreWarning.mockImplementation(() => {}); }); afterEach(() => { @@ -82,22 +85,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 +109,31 @@ describe('getAvailableVersions', () => { const availableVersions = await distribution['getAvailableVersions'](); expect(availableVersions).not.toBeNull(); expect(availableVersions.length).toBe(manifestData.length * 2); + expect(spyHttpClient).toHaveBeenNthCalledWith(2, nextPageUrl); + }); + + it('stops pagination after 1000 pages as a safeguard', async () => { + const nextPageUrl = + 'https://api.adoptopenjdk.net/v3/assets/version/%5B1.0,100.0%5D?page=2&page_size=20'; + spyHttpClient.mockReturnValue({ + statusCode: 200, + headers: {link: `<${nextPageUrl}>; rel="next"`}, + result: [{version_data: {semver: '17.0.1'}, binaries: []}] as any + }); + + const distribution = new SemeruDistribution({ + version: '8', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + + await distribution['getAvailableVersions'](); + + expect(spyHttpClient).toHaveBeenCalledTimes(1000); + expect(spyCoreWarning).toHaveBeenCalledWith( + expect.stringContaining('Reached pagination safeguard limit (1000 pages)') + ); }); it.each([ diff --git a/__tests__/distributors/temurin-installer.test.ts b/__tests__/distributors/temurin-installer.test.ts index 0c6ef3f50..161a2d087 100644 --- a/__tests__/distributors/temurin-installer.test.ts +++ b/__tests__/distributors/temurin-installer.test.ts @@ -12,6 +12,7 @@ import * as core from '@actions/core'; describe('getAvailableVersions', () => { let spyHttpClient: jest.SpyInstance; let spyCoreError: jest.SpyInstance; + let spyCoreWarning: jest.SpyInstance; beforeEach(() => { spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); @@ -23,6 +24,8 @@ describe('getAvailableVersions', () => { // Mock core.error to suppress error logs spyCoreError = jest.spyOn(core, 'error'); spyCoreError.mockImplementation(() => {}); + spyCoreWarning = jest.spyOn(core, 'warning'); + spyCoreWarning.mockImplementation(() => {}); }); afterEach(() => { @@ -93,22 +96,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 +123,34 @@ describe('getAvailableVersions', () => { const availableVersions = await distribution['getAvailableVersions'](); expect(availableVersions).not.toBeNull(); expect(availableVersions.length).toBe(manifestData.length * 2); + expect(spyHttpClient).toHaveBeenNthCalledWith(2, nextPageUrl); + }); + + it('stops pagination after 1000 pages as a safeguard', async () => { + const nextPageUrl = + 'https://api.adoptium.net/v3/assets/version/%5B1.0,100.0%5D?page=2&page_size=20'; + spyHttpClient.mockReturnValue({ + statusCode: 200, + headers: {link: `<${nextPageUrl}>; rel="next"`}, + result: [{version_data: {semver: '17.0.1'}, binaries: []}] as any + }); + + const distribution = new TemurinDistribution( + { + version: '8', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }, + TemurinImplementation.Hotspot + ); + + await distribution['getAvailableVersions'](); + + expect(spyHttpClient).toHaveBeenCalledTimes(1000); + expect(spyCoreWarning).toHaveBeenCalledWith( + expect.stringContaining('Reached pagination safeguard limit (1000 pages)') + ); }); it.each([ diff --git a/__tests__/util.test.ts b/__tests__/util.test.ts index 85b76069e..f41d2c918 100644 --- a/__tests__/util.test.ts +++ b/__tests__/util.test.ts @@ -4,10 +4,12 @@ import * as fs from 'fs'; import * as path from 'path'; import { convertVersionToSemver, + getNextPageUrlFromLinkHeader, getVersionFromFileContent, isVersionSatisfies, isCacheFeatureAvailable, - isGhes + isGhes, + validatePaginationUrl } from '../src/util'; jest.mock('@actions/cache'); @@ -85,6 +87,78 @@ 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: '; type="application/json"; rel="next"' + }, + 'https://api.adoptium.net/v3/versions?page=3' + ], + [{link: '; rel="last"'}, null], + [{link: '; rel="nextsomething"'}, null], + [undefined, null] + ])('returns %s -> %s', (headers, expected) => { + expect(getNextPageUrlFromLinkHeader(headers)).toBe(expected); + }); +}); + +describe('validatePaginationUrl', () => { + it('accepts URL with matching origin', () => { + expect( + validatePaginationUrl( + 'https://api.adoptium.net/v3/assets?page=2', + 'https://api.adoptium.net' + ) + ).toBe(true); + }); + + it('rejects URL with different host', () => { + expect( + validatePaginationUrl( + 'https://evil.example.com/steal?data=1', + 'https://api.adoptium.net' + ) + ).toBe(false); + }); + + it('rejects URL with different protocol', () => { + expect( + validatePaginationUrl( + 'http://api.adoptium.net/v3/assets?page=2', + 'https://api.adoptium.net' + ) + ).toBe(false); + }); + + it('returns false for invalid URL', () => { + expect(validatePaginationUrl('not-a-url', 'https://api.adoptium.net')).toBe( + false + ); + }); + + it('accepts URL with explicit default port', () => { + expect( + validatePaginationUrl( + 'https://api.adoptium.net:443/v3/assets?page=2', + 'https://api.adoptium.net' + ) + ).toBe(true); + }); +}); + describe('getVersionFromFileContent', () => { describe('.sdkmanrc', () => { it.each([ diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js index 9b11cf71e..eff7dac80 100644 --- a/dist/cleanup/index.js +++ b/dist/cleanup/index.js @@ -52134,7 +52134,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.renameWinArchive = exports.getGitHubHttpHeaders = exports.convertVersionToSemver = exports.getVersionFromFileContent = exports.isCacheFeatureAvailable = exports.isGhes = exports.isJobStatusSuccess = exports.getToolcachePath = exports.isVersionSatisfies = exports.getDownloadArchiveExtension = exports.extractJdkFile = exports.getVersionFromToolcachePath = exports.getBooleanInput = exports.getTempDir = void 0; +exports.renameWinArchive = exports.validatePaginationUrl = exports.getNextPageUrlFromLinkHeader = exports.MAX_PAGINATION_PAGES = exports.getGitHubHttpHeaders = exports.convertVersionToSemver = exports.getVersionFromFileContent = exports.isCacheFeatureAvailable = exports.isGhes = exports.isJobStatusSuccess = exports.getToolcachePath = exports.isVersionSatisfies = exports.getDownloadArchiveExtension = exports.extractJdkFile = exports.getVersionFromToolcachePath = exports.getBooleanInput = exports.getTempDir = void 0; const os_1 = __importDefault(__nccwpck_require__(22037)); const path_1 = __importDefault(__nccwpck_require__(71017)); const fs = __importStar(__nccwpck_require__(57147)); @@ -52301,6 +52301,47 @@ function getGitHubHttpHeaders() { return headers; } exports.getGitHubHttpHeaders = getGitHubHttpHeaders; +exports.MAX_PAGINATION_PAGES = 1000; +function getNextPageUrlFromLinkHeader(headers) { + var _a; + if (!headers) { + return null; + } + const linkHeader = (_a = headers.link) !== null && _a !== void 0 ? _a : headers.Link; + if (!linkHeader) { + return null; + } + const normalizedLinkHeader = Array.isArray(linkHeader) + ? linkHeader.join(',') + : linkHeader; + // Split into individual link-values and find the one with rel="next" + // RFC 8288 allows rel to appear anywhere among the parameters + const linkValues = normalizedLinkHeader.split(/,(?=\s*<)/); + for (const linkValue of linkValues) { + const urlMatch = linkValue.match(/<([^>]+)>/); + if (!urlMatch) + continue; + const params = linkValue.slice(urlMatch[0].length); + // Use word boundary to match "next" as a standalone relation type + // RFC 8288 allows space-separated relation types like rel="next prev" + if (/;\s*rel="?[^"]*\bnext\b/i.test(params)) { + return urlMatch[1]; + } + } + return null; +} +exports.getNextPageUrlFromLinkHeader = getNextPageUrlFromLinkHeader; +function validatePaginationUrl(url, allowedOrigin) { + try { + const parsed = new URL(url); + const allowed = new URL(allowedOrigin); + return parsed.origin === allowed.origin; + } + catch (_a) { + return false; + } +} +exports.validatePaginationUrl = validatePaginationUrl; // 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. diff --git a/dist/setup/index.js b/dist/setup/index.js index eb6cab20b..16fca2a58 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -77896,24 +77896,34 @@ class AdoptDistribution extends base_installer_1.JavaBase { `release_type=${releaseType}`, `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 = []; - 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}'`); + let pageCount = 0; + if (core.isDebug()) { + core.debug(`Gathering available versions from '${availableVersionsUrl}'`); + } + while (availableVersionsUrl) { + pageCount++; + const response = yield this.http.getJson(availableVersionsUrl); + const paginationPage = response.result; + const nextUrl = (0, util_1.getNextPageUrlFromLinkHeader)(response.headers); + if (nextUrl && + !(0, util_1.validatePaginationUrl)(nextUrl, 'https://api.adoptopenjdk.net')) { + core.warning(`Ignoring pagination link with unexpected origin: ${nextUrl}`); + availableVersionsUrl = null; + } + else { + availableVersionsUrl = nextUrl; } - const paginationPage = (yield 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 (pageCount >= util_1.MAX_PAGINATION_PAGES) { + core.warning(`Reached pagination safeguard limit (${util_1.MAX_PAGINATION_PAGES} pages) while listing Adopt releases.`); + break; + } } if (core.isDebug()) { core.startGroup('Print information about available versions'); @@ -80071,24 +80081,34 @@ class SemeruDistribution extends base_installer_1.JavaBase { `release_type=${releaseType}`, `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 = []; - 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}'`); + let pageCount = 0; + if (core.isDebug()) { + core.debug(`Gathering available versions from '${availableVersionsUrl}'`); + } + while (availableVersionsUrl) { + pageCount++; + const response = yield this.http.getJson(availableVersionsUrl); + const paginationPage = response.result; + const nextUrl = (0, util_1.getNextPageUrlFromLinkHeader)(response.headers); + if (nextUrl && + !(0, util_1.validatePaginationUrl)(nextUrl, 'https://api.adoptopenjdk.net')) { + core.warning(`Ignoring pagination link with unexpected origin: ${nextUrl}`); + availableVersionsUrl = null; + } + else { + availableVersionsUrl = nextUrl; } - const paginationPage = (yield 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 (pageCount >= util_1.MAX_PAGINATION_PAGES) { + core.warning(`Reached pagination safeguard limit (${util_1.MAX_PAGINATION_PAGES} pages) while listing Semeru releases.`); + break; + } } if (core.isDebug()) { core.startGroup('Print information about available versions'); @@ -80245,24 +80265,34 @@ class TemurinDistribution extends base_installer_1.JavaBase { `release_type=${releaseType}`, `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 = []; - 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}'`); + let pageCount = 0; + if (core.isDebug()) { + core.debug(`Gathering available versions from '${availableVersionsUrl}'`); + } + while (availableVersionsUrl) { + pageCount++; + const response = yield this.http.getJson(availableVersionsUrl); + const paginationPage = response.result; + const nextUrl = (0, util_1.getNextPageUrlFromLinkHeader)(response.headers); + if (nextUrl && + !(0, util_1.validatePaginationUrl)(nextUrl, 'https://api.adoptium.net')) { + core.warning(`Ignoring pagination link with unexpected origin: ${nextUrl}`); + availableVersionsUrl = null; + } + else { + availableVersionsUrl = nextUrl; } - const paginationPage = (yield 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 (pageCount >= util_1.MAX_PAGINATION_PAGES) { + core.warning(`Reached pagination safeguard limit (${util_1.MAX_PAGINATION_PAGES} pages) while listing Temurin releases.`); + break; + } } if (core.isDebug()) { core.startGroup('Print information about available versions'); @@ -80893,7 +80923,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.renameWinArchive = exports.getGitHubHttpHeaders = exports.convertVersionToSemver = exports.getVersionFromFileContent = exports.isCacheFeatureAvailable = exports.isGhes = exports.isJobStatusSuccess = exports.getToolcachePath = exports.isVersionSatisfies = exports.getDownloadArchiveExtension = exports.extractJdkFile = exports.getVersionFromToolcachePath = exports.getBooleanInput = exports.getTempDir = void 0; +exports.renameWinArchive = exports.validatePaginationUrl = exports.getNextPageUrlFromLinkHeader = exports.MAX_PAGINATION_PAGES = exports.getGitHubHttpHeaders = exports.convertVersionToSemver = exports.getVersionFromFileContent = exports.isCacheFeatureAvailable = exports.isGhes = exports.isJobStatusSuccess = exports.getToolcachePath = exports.isVersionSatisfies = exports.getDownloadArchiveExtension = exports.extractJdkFile = exports.getVersionFromToolcachePath = exports.getBooleanInput = exports.getTempDir = void 0; const os_1 = __importDefault(__nccwpck_require__(22037)); const path_1 = __importDefault(__nccwpck_require__(71017)); const fs = __importStar(__nccwpck_require__(57147)); @@ -81060,6 +81090,47 @@ function getGitHubHttpHeaders() { return headers; } exports.getGitHubHttpHeaders = getGitHubHttpHeaders; +exports.MAX_PAGINATION_PAGES = 1000; +function getNextPageUrlFromLinkHeader(headers) { + var _a; + if (!headers) { + return null; + } + const linkHeader = (_a = headers.link) !== null && _a !== void 0 ? _a : headers.Link; + if (!linkHeader) { + return null; + } + const normalizedLinkHeader = Array.isArray(linkHeader) + ? linkHeader.join(',') + : linkHeader; + // Split into individual link-values and find the one with rel="next" + // RFC 8288 allows rel to appear anywhere among the parameters + const linkValues = normalizedLinkHeader.split(/,(?=\s*<)/); + for (const linkValue of linkValues) { + const urlMatch = linkValue.match(/<([^>]+)>/); + if (!urlMatch) + continue; + const params = linkValue.slice(urlMatch[0].length); + // Use word boundary to match "next" as a standalone relation type + // RFC 8288 allows space-separated relation types like rel="next prev" + if (/;\s*rel="?[^"]*\bnext\b/i.test(params)) { + return urlMatch[1]; + } + } + return null; +} +exports.getNextPageUrlFromLinkHeader = getNextPageUrlFromLinkHeader; +function validatePaginationUrl(url, allowedOrigin) { + try { + const parsed = new URL(url); + const allowed = new URL(allowedOrigin); + return parsed.origin === allowed.origin; + } + catch (_a) { + return false; + } +} +exports.validatePaginationUrl = validatePaginationUrl; // 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. diff --git a/package-lock.json b/package-lock.json index 208a2e9e2..b910a8f01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6470,6 +6470,21 @@ "node": ">=16.0.0" } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xmlbuilder2": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz", diff --git a/src/distributions/adopt/installer.ts b/src/distributions/adopt/installer.ts index 34c1716cd..b6393f726 100644 --- a/src/distributions/adopt/installer.ts +++ b/src/distributions/adopt/installer.ts @@ -14,9 +14,12 @@ import { } from '../base-models'; import { extractJdkFile, + getNextPageUrlFromLinkHeader, getDownloadArchiveExtension, isVersionSatisfies, - renameWinArchive + renameWinArchive, + MAX_PAGINATION_PAGES, + validatePaginationUrl } from '../../util'; export enum AdoptImplementation { @@ -125,30 +128,46 @@ 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: string | null = + `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}'` + let pageCount = 0; + if (core.isDebug()) { + core.debug(`Gathering available versions from '${availableVersionsUrl}'`); + } + + while (availableVersionsUrl) { + pageCount++; + const response = + await this.http.getJson( + availableVersionsUrl ); + const paginationPage = response.result; + const nextUrl = getNextPageUrlFromLinkHeader(response.headers); + if ( + nextUrl && + !validatePaginationUrl(nextUrl, 'https://api.adoptopenjdk.net') + ) { + core.warning( + `Ignoring pagination link with unexpected origin: ${nextUrl}` + ); + availableVersionsUrl = null; + } else { + availableVersionsUrl = nextUrl; } - - 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 (pageCount >= MAX_PAGINATION_PAGES) { + core.warning( + `Reached pagination safeguard limit (${MAX_PAGINATION_PAGES} pages) while listing Adopt releases.` + ); + break; + } } if (core.isDebug()) { diff --git a/src/distributions/semeru/installer.ts b/src/distributions/semeru/installer.ts index edb294803..a043f16e4 100644 --- a/src/distributions/semeru/installer.ts +++ b/src/distributions/semeru/installer.ts @@ -7,9 +7,12 @@ import { import semver from 'semver'; import { extractJdkFile, + getNextPageUrlFromLinkHeader, getDownloadArchiveExtension, isVersionSatisfies, - renameWinArchive + renameWinArchive, + MAX_PAGINATION_PAGES, + validatePaginationUrl } from '../../util'; import * as core from '@actions/core'; import * as tc from '@actions/tool-cache'; @@ -155,32 +158,46 @@ 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: string | null = + `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}'` - ); - } + let pageCount = 0; + if (core.isDebug()) { + core.debug(`Gathering available versions from '${availableVersionsUrl}'`); + } - const paginationPage = ( + while (availableVersionsUrl) { + pageCount++; + const response = await this.http.getJson( availableVersionsUrl - ) - ).result; + ); + const paginationPage = response.result; + const nextUrl = getNextPageUrlFromLinkHeader(response.headers); + if ( + nextUrl && + !validatePaginationUrl(nextUrl, 'https://api.adoptopenjdk.net') + ) { + core.warning( + `Ignoring pagination link with unexpected origin: ${nextUrl}` + ); + availableVersionsUrl = null; + } else { + availableVersionsUrl = nextUrl; + } if (paginationPage === null || paginationPage.length === 0) { - // break infinity loop because we have reached end of pagination break; } availableVersions.push(...paginationPage); - page_index++; + + if (pageCount >= MAX_PAGINATION_PAGES) { + core.warning( + `Reached pagination safeguard limit (${MAX_PAGINATION_PAGES} pages) while listing Semeru releases.` + ); + break; + } } if (core.isDebug()) { diff --git a/src/distributions/temurin/installer.ts b/src/distributions/temurin/installer.ts index 51d523f6e..7e5617cb1 100644 --- a/src/distributions/temurin/installer.ts +++ b/src/distributions/temurin/installer.ts @@ -14,9 +14,12 @@ import { } from '../base-models'; import { extractJdkFile, + getNextPageUrlFromLinkHeader, getDownloadArchiveExtension, isVersionSatisfies, - renameWinArchive + renameWinArchive, + MAX_PAGINATION_PAGES, + validatePaginationUrl } from '../../util'; export enum TemurinImplementation { @@ -123,32 +126,47 @@ 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: string | null = + `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}'` - ); - } + let pageCount = 0; + if (core.isDebug()) { + core.debug(`Gathering available versions from '${availableVersionsUrl}'`); + } - const paginationPage = ( + while (availableVersionsUrl) { + pageCount++; + const response = await this.http.getJson( availableVersionsUrl - ) - ).result; + ); + const paginationPage = response.result; + const nextUrl = getNextPageUrlFromLinkHeader(response.headers); + if ( + nextUrl && + !validatePaginationUrl(nextUrl, 'https://api.adoptium.net') + ) { + core.warning( + `Ignoring pagination link with unexpected origin: ${nextUrl}` + ); + availableVersionsUrl = null; + } else { + availableVersionsUrl = nextUrl; + } + if (paginationPage === null || paginationPage.length === 0) { - // break infinity loop because we have reached end of pagination break; } availableVersions.push(...paginationPage); - page_index++; + + if (pageCount >= MAX_PAGINATION_PAGES) { + core.warning( + `Reached pagination safeguard limit (${MAX_PAGINATION_PAGES} pages) while listing Temurin releases.` + ); + break; + } } if (core.isDebug()) { diff --git a/src/util.ts b/src/util.ts index 0325f7f42..5fe84c520 100644 --- a/src/util.ts +++ b/src/util.ts @@ -201,6 +201,55 @@ export function getGitHubHttpHeaders(): OutgoingHttpHeaders { return headers; } +export const MAX_PAGINATION_PAGES = 1000; + +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; + + // Split into individual link-values and find the one with rel="next" + // RFC 8288 allows rel to appear anywhere among the parameters + const linkValues = normalizedLinkHeader.split(/,(?=\s*<)/); + for (const linkValue of linkValues) { + const urlMatch = linkValue.match(/<([^>]+)>/); + if (!urlMatch) continue; + + const params = linkValue.slice(urlMatch[0].length); + // Use word boundary to match "next" as a standalone relation type + // RFC 8288 allows space-separated relation types like rel="next prev" + if (/;\s*rel="?[^"]*\bnext\b/i.test(params)) { + return urlMatch[1]; + } + } + + return null; +} + +export function validatePaginationUrl( + url: string, + allowedOrigin: string +): boolean { + try { + const parsed = new URL(url); + const allowed = new URL(allowedOrigin); + return parsed.origin === allowed.origin; + } catch { + return false; + } +} + // 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. From b24df5bba5e9b8a1fcf822c1f0517b7bcc070757 Mon Sep 17 00:00:00 2001 From: John <1615532+johnoliver@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:30:59 +0100 Subject: [PATCH 2/2] Make the Adoptopenjdk package type look at the Temurin repo first for latest assets (#522) * Make the Adoptopenjdk package type look at the Temurin repo first for latest assets * Address Copilot code review comments - Use strict equality (===, !==) instead of loose equality (==, !=) for all comparisons - Properly handle caught errors with instanceof type narrowing before accessing properties - Only fall back to legacy AdoptOpenJDK for specific version-not-found errors - Rethrow unexpected errors to avoid masking real issues (network failures, rate limits, etc.) - Fix error message check to match actual error text ('No matching version found') - Remove unnecessary undefined check since method return type is never undefined - Add @internal JSDoc annotation to TemurinDistribution.findPackageForDownload() - Update tests to properly mock Temurin lookup failures for fallback behavior testing - Rebuild dist files * Always fall back to legacy AdoptOpenJDK but log all Temurin failures - Change error handling to gracefully fall back for all errors, not just version-not-found - Log version-not-found errors as notices with migration guidance - Log other Temurin failures as debug messages for troubleshooting - Improves resilience: users always get a result even if Temurin API has issues - Maintains visibility: failures are still logged for debugging * Fixes from review * Fixes from review * Fixes from review * Regenerate dist --- .../distributors/adopt-installer.test.ts | 53 ++++++++++++++++ dist/setup/index.js | 46 +++++++++++++- package-lock.json | 15 ----- src/distributions/adopt/installer.ts | 60 ++++++++++++++++++- src/distributions/base-installer.ts | 4 +- src/distributions/temurin/installer.ts | 5 +- 6 files changed, 163 insertions(+), 20 deletions(-) diff --git a/__tests__/distributors/adopt-installer.test.ts b/__tests__/distributors/adopt-installer.test.ts index 55649255b..05be8d53c 100644 --- a/__tests__/distributors/adopt-installer.test.ts +++ b/__tests__/distributors/adopt-installer.test.ts @@ -4,6 +4,7 @@ import { AdoptDistribution, AdoptImplementation } from '../../src/distributions/adopt/installer'; +import {TemurinDistribution} from '../../src/distributions/temurin/installer'; import {JavaInstallerOptions} from '../../src/distributions/base-models'; import os from 'os'; @@ -256,6 +257,38 @@ describe('getAvailableVersions', () => { }); describe('findPackageForDownload', () => { + it('returns Temurin result and does not query Adopt API when Temurin succeeds', async () => { + const temurinRelease = { + version: '11.0.31+11', + url: 'https://example.test/temurin-11.tar.gz' + }; + const temurinFindPackageForDownload = jest + .fn() + .mockResolvedValue(temurinRelease); + const temurinDistribution = { + findPackageForDownload: temurinFindPackageForDownload + } as unknown as TemurinDistribution; + + const distribution = new AdoptDistribution( + { + version: '11', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }, + AdoptImplementation.Hotspot, + temurinDistribution + ); + const adoptLookupSpy = jest.fn(); + distribution['getAvailableVersions'] = adoptLookupSpy; + + const resolvedVersion = await distribution['findPackageForDownload']('11'); + + expect(resolvedVersion).toEqual(temurinRelease); + expect(temurinFindPackageForDownload).toHaveBeenCalledWith('11'); + expect(adoptLookupSpy).not.toHaveBeenCalled(); + }); + it.each([ ['9', '9.0.7+10'], ['15', '15.0.2+7'], @@ -278,6 +311,11 @@ describe('findPackageForDownload', () => { }, AdoptImplementation.Hotspot ); + // Mock Temurin to fail so fallback to AdoptOpenJDK is tested + distribution['temurinDistribution']!['findPackageForDownload'] = + async () => { + throw new Error('No matching version found for SemVer'); + }; distribution['getAvailableVersions'] = async () => manifestData as any; const resolvedVersion = await distribution['findPackageForDownload'](input); expect(resolvedVersion.version).toBe(expected); @@ -293,6 +331,11 @@ describe('findPackageForDownload', () => { }, AdoptImplementation.Hotspot ); + // Mock Temurin to fail so fallback to AdoptOpenJDK is tested + distribution['temurinDistribution']!['findPackageForDownload'] = + async () => { + throw new Error('No matching version found for SemVer'); + }; distribution['getAvailableVersions'] = async () => manifestData as any; await expect( distribution['findPackageForDownload']('9.0.8') @@ -309,6 +352,11 @@ describe('findPackageForDownload', () => { }, AdoptImplementation.Hotspot ); + // Mock Temurin to fail so fallback to AdoptOpenJDK is tested + distribution['temurinDistribution']!['findPackageForDownload'] = + async () => { + throw new Error('No matching version found for SemVer'); + }; distribution['getAvailableVersions'] = async () => manifestData as any; await expect(distribution['findPackageForDownload']('7.x')).rejects.toThrow( /No matching version found for SemVer */ @@ -325,6 +373,11 @@ describe('findPackageForDownload', () => { }, AdoptImplementation.Hotspot ); + // Mock Temurin to fail so fallback to AdoptOpenJDK is tested + distribution['temurinDistribution']!['findPackageForDownload'] = + async () => { + throw new Error('No matching version found for SemVer'); + }; distribution['getAvailableVersions'] = async () => []; await expect(distribution['findPackageForDownload']('11')).rejects.toThrow( /No matching version found for SemVer */ diff --git a/dist/setup/index.js b/dist/setup/index.js index 16fca2a58..a2efc14ae 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -77815,17 +77815,54 @@ const path_1 = __importDefault(__nccwpck_require__(71017)); const semver_1 = __importDefault(__nccwpck_require__(11383)); const base_installer_1 = __nccwpck_require__(59741); const util_1 = __nccwpck_require__(92629); +const installer_1 = __nccwpck_require__(18579); var AdoptImplementation; (function (AdoptImplementation) { AdoptImplementation["Hotspot"] = "Hotspot"; AdoptImplementation["OpenJ9"] = "OpenJ9"; })(AdoptImplementation || (exports.AdoptImplementation = AdoptImplementation = {})); class AdoptDistribution extends base_installer_1.JavaBase { - constructor(installerOptions, jvmImpl) { + constructor(installerOptions, jvmImpl, temurinDistribution = null) { super(`Adopt-${jvmImpl}`, installerOptions); this.jvmImpl = jvmImpl; + if (temurinDistribution !== null && + jvmImpl !== AdoptImplementation.Hotspot) { + throw new Error('Only Hotspot JVM is supported by Temurin.'); + } + // Only use the temurin repo for Hotspot JVMs + this.temurinDistribution = + temurinDistribution !== null && temurinDistribution !== void 0 ? temurinDistribution : (jvmImpl === AdoptImplementation.Hotspot + ? new installer_1.TemurinDistribution(installerOptions, installer_1.TemurinImplementation.Hotspot) + : null); } findPackageForDownload(version) { + return __awaiter(this, void 0, void 0, function* () { + if (this.jvmImpl === AdoptImplementation.Hotspot) { + core.notice("AdoptOpenJDK has moved to Eclipse Temurin https://github.com/actions/setup-java#supported-distributions please consider changing to the 'temurin' distribution type in your setup-java configuration."); + } + if (this.jvmImpl === AdoptImplementation.Hotspot && + this.temurinDistribution !== null) { + try { + return yield this.temurinDistribution.findPackageForDownload(version); + } + catch (error) { + // Log the failure but always fall back to legacy AdoptOpenJDK for resilience + const errorMessage = error instanceof Error ? error.message : String(error); + if (error instanceof Error && error.name === 'VersionNotFoundError') { + core.notice('The JVM you are looking for could not be found in the Temurin repository, this likely indicates ' + + 'that you are using an out of date version of Java, consider updating and moving to using the Temurin distribution type in setup-java.'); + } + else { + // Log other errors for debugging but gracefully fall back + core.debug(`Temurin lookup failed: ${errorMessage}. Falling back to AdoptOpenJDK API.`); + } + } + } + // failed to find a Temurin version, so fall back to AdoptOpenJDK + return this.findPackageForDownloadOldAdoptOpenJdk(version); + }); + } + findPackageForDownloadOldAdoptOpenJdk(version) { return __awaiter(this, void 0, void 0, function* () { const availableVersionsRaw = yield this.getAvailableVersions(); const availableVersionsWithBinaries = availableVersionsRaw @@ -78223,7 +78260,9 @@ class JavaBase { parts.push(`(showing first ${maxVersionsToShow} of ${availableVersions.length} versions, enable debug mode to see all)`); } } - return new Error(parts.join('\n')); + const error = new Error(parts.join('\n')); + error.name = 'VersionNotFoundError'; + return error; } setJavaDefault(version, toolPath) { const majorVersion = version.split('.')[0]; @@ -80195,6 +80234,9 @@ class TemurinDistribution extends base_installer_1.JavaBase { super(`Temurin-${jvmImpl}`, installerOptions); this.jvmImpl = jvmImpl; } + /** + * @internal For cross-distribution reuse only. Not intended as a public API. + */ findPackageForDownload(version) { return __awaiter(this, void 0, void 0, function* () { const availableVersionsRaw = yield this.getAvailableVersions(); diff --git a/package-lock.json b/package-lock.json index b910a8f01..208a2e9e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6470,21 +6470,6 @@ "node": ">=16.0.0" } }, - "node_modules/xml-naming": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", - "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/xmlbuilder2": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz", diff --git a/src/distributions/adopt/installer.ts b/src/distributions/adopt/installer.ts index b6393f726..78798bfc1 100644 --- a/src/distributions/adopt/installer.ts +++ b/src/distributions/adopt/installer.ts @@ -21,6 +21,7 @@ import { MAX_PAGINATION_PAGES, validatePaginationUrl } from '../../util'; +import {TemurinDistribution, TemurinImplementation} from '../temurin/installer'; export enum AdoptImplementation { Hotspot = 'Hotspot', @@ -28,15 +29,72 @@ export enum AdoptImplementation { } export class AdoptDistribution extends JavaBase { + private readonly temurinDistribution: TemurinDistribution | null; + constructor( installerOptions: JavaInstallerOptions, - private readonly jvmImpl: AdoptImplementation + private readonly jvmImpl: AdoptImplementation, + temurinDistribution: TemurinDistribution | null = null ) { super(`Adopt-${jvmImpl}`, installerOptions); + + if ( + temurinDistribution !== null && + jvmImpl !== AdoptImplementation.Hotspot + ) { + throw new Error('Only Hotspot JVM is supported by Temurin.'); + } + + // Only use the temurin repo for Hotspot JVMs + this.temurinDistribution = + temurinDistribution ?? + (jvmImpl === AdoptImplementation.Hotspot + ? new TemurinDistribution( + installerOptions, + TemurinImplementation.Hotspot + ) + : null); } protected async findPackageForDownload( version: string + ): Promise { + if (this.jvmImpl === AdoptImplementation.Hotspot) { + core.notice( + "AdoptOpenJDK has moved to Eclipse Temurin https://github.com/actions/setup-java#supported-distributions please consider changing to the 'temurin' distribution type in your setup-java configuration." + ); + } + + if ( + this.jvmImpl === AdoptImplementation.Hotspot && + this.temurinDistribution !== null + ) { + try { + return await this.temurinDistribution.findPackageForDownload(version); + } catch (error) { + // Log the failure but always fall back to legacy AdoptOpenJDK for resilience + const errorMessage = + error instanceof Error ? error.message : String(error); + if (error instanceof Error && error.name === 'VersionNotFoundError') { + core.notice( + 'The JVM you are looking for could not be found in the Temurin repository, this likely indicates ' + + 'that you are using an out of date version of Java, consider updating and moving to using the Temurin distribution type in setup-java.' + ); + } else { + // Log other errors for debugging but gracefully fall back + core.debug( + `Temurin lookup failed: ${errorMessage}. Falling back to AdoptOpenJDK API.` + ); + } + } + } + + // failed to find a Temurin version, so fall back to AdoptOpenJDK + return this.findPackageForDownloadOldAdoptOpenJdk(version); + } + + private async findPackageForDownloadOldAdoptOpenJdk( + version: string ): Promise { const availableVersionsRaw = await this.getAvailableVersions(); const availableVersionsWithBinaries = availableVersionsRaw diff --git a/src/distributions/base-installer.ts b/src/distributions/base-installer.ts index 10e37d996..5d9f3c82a 100644 --- a/src/distributions/base-installer.ts +++ b/src/distributions/base-installer.ts @@ -292,7 +292,9 @@ export abstract class JavaBase { } } - return new Error(parts.join('\n')); + const error = new Error(parts.join('\n')); + error.name = 'VersionNotFoundError'; + return error; } protected setJavaDefault(version: string, toolPath: string) { diff --git a/src/distributions/temurin/installer.ts b/src/distributions/temurin/installer.ts index 7e5617cb1..109c2d413 100644 --- a/src/distributions/temurin/installer.ts +++ b/src/distributions/temurin/installer.ts @@ -34,7 +34,10 @@ export class TemurinDistribution extends JavaBase { super(`Temurin-${jvmImpl}`, installerOptions); } - protected async findPackageForDownload( + /** + * @internal For cross-distribution reuse only. Not intended as a public API. + */ + public async findPackageForDownload( version: string ): Promise { const availableVersionsRaw = await this.getAvailableVersions();