From 71ee2e9aa6ab3e0ef98c05d887de335f90f1da4b Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Thu, 9 Jul 2026 15:20:59 -0400 Subject: [PATCH 1/5] Support pinning java-version as "latest" Add a `latest` alias for the `java-version` input that floats to the newest available stable (GA) release. It is normalized to the SemVer wildcard at the base-installer layer and always resolves from remote (like `check-latest: true`). List-based distributions resolve it automatically via the existing newest-first matching. Corretto selects its newest available major; Oracle and GraalVM look up the newest GA major via the Adoptium API and request it, failing with an actionable error if that major isn't published yet. The jdkfile distribution rejects `latest`. Closes #832 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 7 ++ __tests__/distributors/base-installer.test.ts | 36 ++++++-- .../distributors/corretto-installer.test.ts | 18 ++++ .../distributors/graalvm-installer.test.ts | 55 ++++++++++++ .../distributors/local-installer.test.ts | 14 +++ .../distributors/oracle-installer.test.ts | 56 ++++++++++++ .../distributors/temurin-installer.test.ts | 18 ++++ __tests__/util.test.ts | 33 ++++++- action.yml | 2 +- dist/cleanup/index.js | 16 ++++ dist/setup/index.js | 87 +++++++++++++++++-- src/distributions/base-installer.ts | 26 ++++-- src/distributions/corretto/installer.ts | 14 ++- src/distributions/graalvm/installer.ts | 18 ++++ src/distributions/local/installer.ts | 6 ++ src/distributions/oracle/installer.ts | 15 ++++ src/setup-java.ts | 8 +- src/util.ts | 31 +++++++ 18 files changed, 441 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 7c47806c5..f56d02883 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,13 @@ The `java-version` input supports an exact version or a version range using [Sem - major versions, such as: `8`, `11`, `16`, `17`, `21`, `25` - more specific versions: `8.0.282+8`, `8.0.232`, `11.0`, `11.0.4`, `17.0` - early access (EA) versions: `15-ea`, `15.0.0-ea` +- the `latest` alias, which floats to the newest available stable (GA) release + +> [!NOTE] +> - `latest` always resolves the newest version from the distribution's remote metadata (it behaves like `check-latest: true`), so it ignores any older version already present in the runner tool cache. This has the same performance trade-off described in [Check latest](#check-latest). +> - `latest` is only supported through the `java-version` input, not through `java-version-file`, and it resolves stable (GA) releases only — it cannot be combined with `-ea`. +> - The `jdkfile` distribution does not support `latest`, as it installs from a local file. +> - For `oracle` and `graalvm`, `latest` resolves the newest GA major version via the Adoptium API and then requests it from the distribution. If that distribution hasn't published the newest major yet, the action fails and asks you to specify a concrete version. Note the Oracle JDK license caveat below still applies to a floating `latest`. #### Supported distributions Currently, the following distributions are supported: diff --git a/__tests__/distributors/base-installer.test.ts b/__tests__/distributors/base-installer.test.ts index a4a62d5d1..b73f7d73d 100644 --- a/__tests__/distributors/base-installer.test.ts +++ b/__tests__/distributors/base-installer.test.ts @@ -420,6 +420,29 @@ describe('setupJava', () => { expect(spyCoreInfo).not.toHaveBeenCalledWith('Trying to download...'); }); + it('should resolve the latest version from remote when java-version is "latest", even if a version is cached', async () => { + mockJavaBase = new EmptyJavaBase({ + version: 'latest', + architecture: 'x86', + packageType: 'jdk', + checkLatest: false + }); + + await expect(mockJavaBase.setupJava()).resolves.toEqual({ + version: actualJavaVersion, + path: javaPathInstalled + }); + + // `latest` must bypass the tool-cache short-circuit and always resolve remotely + expect(spyCoreInfo).toHaveBeenCalledWith( + 'Trying to resolve the latest version from remote' + ); + expect(spyCoreInfo).toHaveBeenCalledWith('Trying to download...'); + expect(spyCoreInfo).not.toHaveBeenCalledWith( + `Resolved Java ${installedJavaVersion} from tool-cache` + ); + }); + it.each([ [ { @@ -744,11 +767,14 @@ describe('normalizeVersion', () => { const DummyJavaBase = JavaBase as any; it.each([ - ['11', {version: '11', stable: true}], - ['11.0', {version: '11.0', stable: true}], - ['11.0.10', {version: '11.0.10', stable: true}], - ['11-ea', {version: '11', stable: false}], - ['11.0.2-ea', {version: '11.0.2', stable: false}] + ['11', {version: '11', stable: true, latest: false}], + ['11.0', {version: '11.0', stable: true, latest: false}], + ['11.0.10', {version: '11.0.10', stable: true, latest: false}], + ['11-ea', {version: '11', stable: false, latest: false}], + ['11.0.2-ea', {version: '11.0.2', stable: false, latest: false}], + ['latest', {version: 'x', stable: true, latest: true}], + ['LATEST', {version: 'x', stable: true, latest: true}], + [' Latest ', {version: 'x', stable: true, latest: true}] ])('normalizeVersion from %s to %s', (input, expected) => { expect(DummyJavaBase.prototype.normalizeVersion.call(null, input)).toEqual( expected diff --git a/__tests__/distributors/corretto-installer.test.ts b/__tests__/distributors/corretto-installer.test.ts index 48ffcc34f..57e660818 100644 --- a/__tests__/distributors/corretto-installer.test.ts +++ b/__tests__/distributors/corretto-installer.test.ts @@ -204,6 +204,24 @@ describe('getAvailableVersions', () => { expect(availableVersion.url).toBe(expectedLink); }); + it('with latest resolves to the newest available major version', async () => { + const distribution = new CorrettoDistribution({ + version: 'latest', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + mockPlatform(distribution, 'linux'); + + const availableVersion = + await distribution['findPackageForDownload']('x'); + expect(availableVersion).not.toBeNull(); + // 18 is the newest major present in the mocked Corretto index + expect(availableVersion.url).toBe( + 'https://corretto.aws/downloads/resources/18.0.0.37.1/amazon-corretto-18.0.0.37.1-linux-x64.tar.gz' + ); + }); + it('with unstable version expect to throw not supported error', async () => { const version = '18.0.1-ea'; const distribution = new CorrettoDistribution({ diff --git a/__tests__/distributors/graalvm-installer.test.ts b/__tests__/distributors/graalvm-installer.test.ts index 3a64096be..7af2b81e5 100644 --- a/__tests__/distributors/graalvm-installer.test.ts +++ b/__tests__/distributors/graalvm-installer.test.ts @@ -417,6 +417,61 @@ describe('GraalVMDistribution', () => { ); }); + describe('latest alias', () => { + it('resolves the newest major version from the Adoptium API', async () => { + const latestDistribution = new GraalVMDistribution({ + ...defaultOptions, + version: 'latest' + }); + (latestDistribution as any).http = mockHttpClient; + jest + .spyOn(latestDistribution, 'getPlatform') + .mockReturnValue('linux'); + mockHttpClient.getJson.mockResolvedValue({ + statusCode: 200, + result: {most_recent_feature_release: 25}, + headers: {} + }); + mockHttpClient.head.mockResolvedValue({ + message: {statusCode: 200} + }); + + const result = await ( + latestDistribution as any + ).findPackageForDownload('x'); + + expect(result).toEqual({ + url: 'https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-x64_bin.tar.gz', + version: '25' + }); + }); + + it('throws an actionable error when the latest major is not yet available', async () => { + const latestDistribution = new GraalVMDistribution({ + ...defaultOptions, + version: 'latest' + }); + (latestDistribution as any).http = mockHttpClient; + jest + .spyOn(latestDistribution, 'getPlatform') + .mockReturnValue('linux'); + mockHttpClient.getJson.mockResolvedValue({ + statusCode: 200, + result: {most_recent_feature_release: 25}, + headers: {} + }); + mockHttpClient.head.mockResolvedValue({ + message: {statusCode: 404} + }); + + await expect( + (latestDistribution as any).findPackageForDownload('x') + ).rejects.toThrow( + /is not yet available for the GraalVM distribution/ + ); + }); + }); + it('should throw error for JDK versions less than 17', async () => { await expect( (distribution as any).findPackageForDownload('11') diff --git a/__tests__/distributors/local-installer.test.ts b/__tests__/distributors/local-installer.test.ts index 0edbe50b8..3266f8a89 100644 --- a/__tests__/distributors/local-installer.test.ts +++ b/__tests__/distributors/local-installer.test.ts @@ -170,6 +170,20 @@ describe('setupJava', () => { jest.restoreAllMocks(); }); + it('throws for the latest alias since jdkfile has no version list', async () => { + const inputs = { + version: 'latest', + architecture: 'x86', + packageType: 'jdk', + checkLatest: false + }; + + mockJavaBase = new LocalDistribution(inputs, expectedJdkFile); + await expect(mockJavaBase.setupJava()).rejects.toThrow( + "The 'latest' version alias is not supported for the 'jdkfile' distribution. Please specify a concrete version." + ); + }); + it('java is resolved from toolcache, jdkfile is untouched', async () => { const inputs = { version: actualJavaVersion, diff --git a/__tests__/distributors/oracle-installer.test.ts b/__tests__/distributors/oracle-installer.test.ts index 429454999..b5cb7989b 100644 --- a/__tests__/distributors/oracle-installer.test.ts +++ b/__tests__/distributors/oracle-installer.test.ts @@ -174,3 +174,59 @@ describe('findPackageForDownload', () => { ); }); }); +describe('findPackageForDownload with latest', () => { + let spyHttpClientHead: any; + let spyHttpClientGetJson: any; + + beforeEach(() => { + (core.debug as jest.Mock).mockImplementation(() => {}); + (core.error as jest.Mock).mockImplementation(() => {}); + spyHttpClientGetJson = jest.spyOn(HttpClient.prototype, 'getJson'); + spyHttpClientGetJson.mockResolvedValue({ + statusCode: 200, + result: {most_recent_feature_release: 25}, + headers: {} + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('resolves the newest major version from the Adoptium API', async () => { + spyHttpClientHead = jest.spyOn(HttpClient.prototype, 'head'); + spyHttpClientHead.mockResolvedValue({message: {statusCode: 200}}); + + const distribution = new OracleDistribution({ + version: 'latest', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + + const result = await distribution['findPackageForDownload']('x'); + const osType = distribution.getPlatform(); + const archiveType = getDownloadArchiveExtension(); + + expect(result.version).toBe('25'); + expect(result.url).toBe( + `https://download.oracle.com/java/25/latest/jdk-25_${osType}-x64_bin.${archiveType}` + ); + }); + + it('throws an actionable error when the latest major is not yet available', async () => { + spyHttpClientHead = jest.spyOn(HttpClient.prototype, 'head'); + spyHttpClientHead.mockResolvedValue({message: {statusCode: 404}}); + + const distribution = new OracleDistribution({ + version: 'latest', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + + await expect(distribution['findPackageForDownload']('x')).rejects.toThrow( + /is not yet available for the Oracle JDK distribution/ + ); + }); +}); diff --git a/__tests__/distributors/temurin-installer.test.ts b/__tests__/distributors/temurin-installer.test.ts index 5fee7cefc..dd2d73a56 100644 --- a/__tests__/distributors/temurin-installer.test.ts +++ b/__tests__/distributors/temurin-installer.test.ts @@ -312,6 +312,24 @@ describe('findPackageForDownload', () => { expect(resolvedVersion.signatureUrl).toBeDefined(); }); + it('version "latest" is normalized to the newest available version', async () => { + const distribution = new TemurinDistribution( + { + version: 'latest', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }, + TemurinImplementation.Hotspot + ); + distribution['getAvailableVersions'] = async () => manifestData as any; + // normalizeVersion turns `latest` into the wildcard carried on `this.version` + const resolvedVersion = await distribution['findPackageForDownload']( + distribution['version'] + ); + expect(resolvedVersion.version).toBe('16.0.2+7'); + }); + it('version is found but binaries list is empty', async () => { const distribution = new TemurinDistribution( { diff --git a/__tests__/util.test.ts b/__tests__/util.test.ts index 239df0e80..d19ee52f2 100644 --- a/__tests__/util.test.ts +++ b/__tests__/util.test.ts @@ -56,7 +56,8 @@ const { isVersionSatisfies, isCacheFeatureAvailable, isGhes, - validatePaginationUrl + validatePaginationUrl, + getLatestMajorVersion } = await import('../src/util.js'); describe('isVersionSatisfies', () => { @@ -400,3 +401,33 @@ describe('isGhes', () => { expect(isGhes()).toBeTruthy(); }); }); + +describe('getLatestMajorVersion', () => { + const makeHttp = (getJson: jest.Mock) => + ({getJson}) as unknown as import('@actions/http-client').HttpClient; + + it('returns most_recent_feature_release from the Adoptium API', async () => { + const getJson = jest.fn(async () => ({ + statusCode: 200, + result: {most_recent_feature_release: 25}, + headers: {} + })); + + await expect(getLatestMajorVersion(makeHttp(getJson))).resolves.toBe(25); + expect(getJson).toHaveBeenCalledWith( + 'https://api.adoptium.net/v3/info/available_releases' + ); + }); + + it('throws when the response does not contain a usable value', async () => { + const getJson = jest.fn(async () => ({ + statusCode: 200, + result: {}, + headers: {} + })); + + await expect(getLatestMajorVersion(makeHttp(getJson))).rejects.toThrow( + 'Could not determine the latest available Java major version' + ); + }); +}); diff --git a/action.yml b/action.yml index 5237f1a8f..f7027a831 100644 --- a/action.yml +++ b/action.yml @@ -4,7 +4,7 @@ description: 'Set up a specific version of the Java JDK and add the author: 'GitHub' inputs: java-version: - description: 'The Java version to set up. Takes a whole or semver Java version. See examples of supported syntax in README file' + description: 'The Java version to set up. Takes a whole or semver Java version, or the "latest" alias to use the newest available stable release. See examples of supported syntax in README file' required: false java-version-file: description: 'The path to a file containing the Java version to set up (.java-version, .tool-versions, .sdkmanrc). Used when java-version is not set. See examples of supported syntax in README file' diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js index 41634dd33..3e52658a0 100644 --- a/dist/cleanup/index.js +++ b/dist/cleanup/index.js @@ -96154,6 +96154,22 @@ function renameWinArchive(javaArchivePath) { fs.renameSync(javaArchivePath, javaArchivePathRenamed); return javaArchivePathRenamed; } +// Resolve the newest available stable/GA feature (major) release. +// +// Some distributions (e.g. Oracle, GraalVM) construct their download URLs from a +// concrete major version and don't expose an endpoint to list every available +// release, so a bare `latest` alias can't be resolved from their own metadata. +// The Adoptium (Temurin) API is used as a proxy for "what is the newest GA major +// version out there", which those distributions typically publish at the same time. +async function getLatestMajorVersion(http) { + const availableReleasesUrl = 'https://api.adoptium.net/v3/info/available_releases'; + const response = await http.getJson(availableReleasesUrl); + const mostRecent = response.result?.most_recent_feature_release; + if (!mostRecent || Number.isNaN(Number(mostRecent))) { + throw new Error(`Could not determine the latest available Java major version from ${availableReleasesUrl}`); + } + return Number(mostRecent); +} ;// CONCATENATED MODULE: ./src/gpg.ts diff --git a/dist/setup/index.js b/dist/setup/index.js index e5759d2d5..ea93d60c2 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -127146,6 +127146,22 @@ function renameWinArchive(javaArchivePath) { external_fs_namespaceObject.renameSync(javaArchivePath, javaArchivePathRenamed); return javaArchivePathRenamed; } +// Resolve the newest available stable/GA feature (major) release. +// +// Some distributions (e.g. Oracle, GraalVM) construct their download URLs from a +// concrete major version and don't expose an endpoint to list every available +// release, so a bare `latest` alias can't be resolved from their own metadata. +// The Adoptium (Temurin) API is used as a proxy for "what is the newest GA major +// version out there", which those distributions typically publish at the same time. +async function getLatestMajorVersion(http) { + const availableReleasesUrl = 'https://api.adoptium.net/v3/info/available_releases'; + const response = await http.getJson(availableReleasesUrl); + const mostRecent = response.result?.most_recent_feature_release; + if (!mostRecent || Number.isNaN(Number(mostRecent))) { + throw new Error(`Could not determine the latest available Java major version from ${availableReleasesUrl}`); + } + return Number(mostRecent); +} ;// CONCATENATED MODULE: ./src/gpg.ts @@ -131045,6 +131061,7 @@ class JavaBase { architecture; packageType; stable; + latest; checkLatest; setDefault; verifySignature; @@ -131055,7 +131072,11 @@ class JavaBase { allowRetries: true, maxRetries: 3 }); - ({ version: this.version, stable: this.stable } = this.normalizeVersion(installerOptions.version)); + ({ + version: this.version, + stable: this.stable, + latest: this.latest + } = this.normalizeVersion(installerOptions.version)); this.architecture = installerOptions.architecture || external_os_default().arch(); this.packageType = installerOptions.packageType; this.checkLatest = installerOptions.checkLatest; @@ -131071,7 +131092,7 @@ class JavaBase { throw new Error(`Input 'verify-signature' is not supported for distribution '${this.distribution}'.`); } let foundJava = this.findInToolcache(); - if (foundJava && !this.checkLatest) { + if (foundJava && !this.checkLatest && !this.latest) { info(`Resolved Java ${foundJava.version} from tool-cache`); } else { @@ -131249,6 +131270,17 @@ class JavaBase { } normalizeVersion(version) { let stable = true; + const latest = false; + // Support the `latest` alias (case-insensitive), which floats to the newest + // available stable/GA release. It is translated to the SemVer wildcard `x` + // so the existing "newest satisfying version wins" resolution applies. + if (version.trim().toLowerCase() === 'latest') { + return { + version: 'x', + stable: true, + latest: true + }; + } if (version.endsWith('-ea')) { version = version.replace(/-ea$/, ''); stable = false; @@ -131263,7 +131295,8 @@ class JavaBase { } return { version, - stable + stable, + latest }; } createVersionNotFoundError(versionOrRange, availableVersions, additionalContext) { @@ -131337,6 +131370,9 @@ class LocalDistribution extends JavaBase { this.jdkFile = jdkFile; } async setupJava() { + if (this.latest) { + throw new Error("The 'latest' version alias is not supported for the 'jdkfile' distribution. Please specify a concrete version."); + } let foundJava = this.findInToolcache(); if (foundJava) { info(`Resolved Java ${foundJava.version} from tool-cache`); @@ -132352,10 +132388,20 @@ class CorrettoDistribution extends JavaBase { if (!this.stable) { throw new Error('Early access versions are not supported'); } + const availableVersions = await this.getAvailableVersions(); + // The `latest` alias is normalized to the SemVer wildcard, but Corretto + // matches on an exact major version, so resolve it to the newest available + // major from Corretto's own list. + if (this.latest) { + const latestMajor = availableVersions + .map(item => parseInt(item.version, 10)) + .filter(major => !Number.isNaN(major)) + .reduce((max, current) => (current > max ? current : max), 0); + version = latestMajor.toString(); + } if (version.includes('.')) { throw new Error('Only major versions are supported'); } - const availableVersions = await this.getAvailableVersions(); const matchingVersions = availableVersions .filter(item => item.version == version) .map(item => { @@ -132483,6 +132529,13 @@ class OracleDistribution extends JavaBase { } const platform = this.getPlatform(); const extension = getDownloadArchiveExtension(); + // The `latest` alias is normalized to the SemVer wildcard. Oracle builds its + // download URLs from a concrete major and has no endpoint to list releases, + // so resolve the newest available GA major from the Adoptium API and use it. + if (this.latest) { + const latestMajor = await getLatestMajorVersion(this.http); + range = latestMajor.toString(); + } const isOnlyMajorProvided = !range.includes('.'); const major = isOnlyMajorProvided ? range : range.split('.')[0]; const possibleUrls = []; @@ -132509,6 +132562,11 @@ class OracleDistribution extends JavaBase { throw new Error(`Http request for Oracle JDK failed with status code: ${response.message.statusCode}`); } } + if (this.latest) { + const error = this.createVersionNotFoundError(range); + error.message += `\nThe latest Java major version (${range}) is not yet available for the Oracle JDK distribution. Please specify a concrete version instead of 'latest'.`; + throw error; + } throw this.createVersionNotFoundError(range); } getPlatform(platform = process.platform) { @@ -132907,6 +132965,12 @@ class GraalVMDistribution extends JavaBase { if (!this.stable) { return this.findEABuildDownloadUrl(`${range}-ea`); } + // The `latest` alias is normalized to the SemVer wildcard. Oracle GraalVM + // builds its download URLs from a concrete major and has no endpoint to list + // releases, so resolve the newest available GA major from the Adoptium API. + if (this.latest) { + range = (await getLatestMajorVersion(this.http)).toString(); + } const { platform, extension, major } = this.validateStableBuildRequest(range); const fileUrl = this.constructFileUrl(range, major, platform, arch, extension); const response = await this.http.head(fileUrl); @@ -132955,6 +133019,9 @@ class GraalVMDistribution extends JavaBase { if (statusCode === HttpCodes.NotFound) { // Create the standard error with additional hint about checking the download URL const error = this.createVersionNotFoundError(range); + if (this.latest) { + error.message += `\nThe latest Java major version (${range}) is not yet available for the ${this.distribution} distribution. Please specify a concrete version instead of 'latest'.`; + } error.message += `\nPlease check if this version is available at ${GRAALVM_DOWNLOAD_URL} . Pick a version from the list.`; throw error; } @@ -133041,6 +133108,12 @@ class GraalVMCommunityDistribution extends GraalVMDistribution { if (!this.stable) { throw new Error('GraalVM Community does not provide early access builds'); } + // The `latest` alias is normalized to the SemVer wildcard. Resolve the newest + // available GA major from the Adoptium API so it can be matched against the + // published GraalVM Community releases. + if (this.latest) { + range = (await getLatestMajorVersion(this.http)).toString(); + } const arch = this.getSupportedArchitecture(); const { platform, extension } = this.validateStableBuildRequest(range); // GraalVM Community asset names embed the platform, architecture and @@ -133664,7 +133737,11 @@ async function installVersion(version, options, toolchainId = 0) { throw new Error(`No supported distribution was found for input ${distributionName}`); } const result = await distribution.setupJava(); - await configureToolchains(version, distributionName, result.path, toolchainIds[toolchainId]); + // When the `latest` alias is used, the literal input isn't a real version, so + // pass the resolved version to the toolchains configuration instead. + const isLatest = version.trim().toLowerCase() === 'latest'; + const toolchainVersion = isLatest ? result.version : version; + await configureToolchains(toolchainVersion, distributionName, result.path, toolchainIds[toolchainId]); info(''); info('Java configuration:'); info(` Distribution: ${distributionName}`); diff --git a/src/distributions/base-installer.ts b/src/distributions/base-installer.ts index c3c23ce9b..e80bfffe4 100644 --- a/src/distributions/base-installer.ts +++ b/src/distributions/base-installer.ts @@ -19,6 +19,7 @@ export abstract class JavaBase { protected architecture: string; protected packageType: string; protected stable: boolean; + protected latest: boolean; protected checkLatest: boolean; protected setDefault: boolean; protected verifySignature: boolean; @@ -33,9 +34,11 @@ export abstract class JavaBase { maxRetries: 3 }); - ({version: this.version, stable: this.stable} = this.normalizeVersion( - installerOptions.version - )); + ({ + version: this.version, + stable: this.stable, + latest: this.latest + } = this.normalizeVersion(installerOptions.version)); this.architecture = installerOptions.architecture || os.arch(); this.packageType = installerOptions.packageType; this.checkLatest = installerOptions.checkLatest; @@ -62,7 +65,7 @@ export abstract class JavaBase { } let foundJava = this.findInToolcache(); - if (foundJava && !this.checkLatest) { + if (foundJava && !this.checkLatest && !this.latest) { core.info(`Resolved Java ${foundJava.version} from tool-cache`); } else { core.info('Trying to resolve the latest version from remote'); @@ -263,6 +266,18 @@ export abstract class JavaBase { protected normalizeVersion(version: string) { let stable = true; + const latest = false; + + // Support the `latest` alias (case-insensitive), which floats to the newest + // available stable/GA release. It is translated to the SemVer wildcard `x` + // so the existing "newest satisfying version wins" resolution applies. + if (version.trim().toLowerCase() === 'latest') { + return { + version: 'x', + stable: true, + latest: true + }; + } if (version.endsWith('-ea')) { version = version.replace(/-ea$/, ''); @@ -281,7 +296,8 @@ export abstract class JavaBase { return { version, - stable + stable, + latest }; } diff --git a/src/distributions/corretto/installer.ts b/src/distributions/corretto/installer.ts index 27d47a506..ca68ed21c 100644 --- a/src/distributions/corretto/installer.ts +++ b/src/distributions/corretto/installer.ts @@ -59,10 +59,22 @@ export class CorrettoDistribution extends JavaBase { if (!this.stable) { throw new Error('Early access versions are not supported'); } + const availableVersions = await this.getAvailableVersions(); + + // The `latest` alias is normalized to the SemVer wildcard, but Corretto + // matches on an exact major version, so resolve it to the newest available + // major from Corretto's own list. + if (this.latest) { + const latestMajor = availableVersions + .map(item => parseInt(item.version, 10)) + .filter(major => !Number.isNaN(major)) + .reduce((max, current) => (current > max ? current : max), 0); + version = latestMajor.toString(); + } + if (version.includes('.')) { throw new Error('Only major versions are supported'); } - const availableVersions = await this.getAvailableVersions(); const matchingVersions = availableVersions .filter(item => item.version == version) .map(item => { diff --git a/src/distributions/graalvm/installer.ts b/src/distributions/graalvm/installer.ts index f6dbfb080..a55375071 100644 --- a/src/distributions/graalvm/installer.ts +++ b/src/distributions/graalvm/installer.ts @@ -16,6 +16,7 @@ import { extractJdkFile, getDownloadArchiveExtension, getGitHubHttpHeaders, + getLatestMajorVersion, getNextPageUrlFromLinkHeader, isVersionSatisfies, MAX_PAGINATION_PAGES, @@ -119,6 +120,13 @@ export class GraalVMDistribution extends JavaBase { return this.findEABuildDownloadUrl(`${range}-ea`); } + // The `latest` alias is normalized to the SemVer wildcard. Oracle GraalVM + // builds its download URLs from a concrete major and has no endpoint to list + // releases, so resolve the newest available GA major from the Adoptium API. + if (this.latest) { + range = (await getLatestMajorVersion(this.http)).toString(); + } + const {platform, extension, major} = this.validateStableBuildRequest(range); const fileUrl = this.constructFileUrl( @@ -203,6 +211,9 @@ export class GraalVMDistribution extends JavaBase { if (statusCode === HttpCodes.NotFound) { // Create the standard error with additional hint about checking the download URL const error = this.createVersionNotFoundError(range); + if (this.latest) { + error.message += `\nThe latest Java major version (${range}) is not yet available for the ${this.distribution} distribution. Please specify a concrete version instead of 'latest'.`; + } error.message += `\nPlease check if this version is available at ${GRAALVM_DOWNLOAD_URL} . Pick a version from the list.`; throw error; } @@ -353,6 +364,13 @@ export class GraalVMCommunityDistribution extends GraalVMDistribution { throw new Error('GraalVM Community does not provide early access builds'); } + // The `latest` alias is normalized to the SemVer wildcard. Resolve the newest + // available GA major from the Adoptium API so it can be matched against the + // published GraalVM Community releases. + if (this.latest) { + range = (await getLatestMajorVersion(this.http)).toString(); + } + const arch = this.getSupportedArchitecture(); const {platform, extension} = this.validateStableBuildRequest(range); // GraalVM Community asset names embed the platform, architecture and diff --git a/src/distributions/local/installer.ts b/src/distributions/local/installer.ts index 9a73d1510..6447c4293 100644 --- a/src/distributions/local/installer.ts +++ b/src/distributions/local/installer.ts @@ -22,6 +22,12 @@ export class LocalDistribution extends JavaBase { } public async setupJava(): Promise { + if (this.latest) { + throw new Error( + "The 'latest' version alias is not supported for the 'jdkfile' distribution. Please specify a concrete version." + ); + } + let foundJava = this.findInToolcache(); if (foundJava) { diff --git a/src/distributions/oracle/installer.ts b/src/distributions/oracle/installer.ts index 2a0991931..559718704 100644 --- a/src/distributions/oracle/installer.ts +++ b/src/distributions/oracle/installer.ts @@ -13,6 +13,7 @@ import { import { extractJdkFile, getDownloadArchiveExtension, + getLatestMajorVersion, renameWinArchive } from '../../util.js'; import {HttpCodes} from '@actions/http-client'; @@ -73,6 +74,14 @@ export class OracleDistribution extends JavaBase { const platform = this.getPlatform(); const extension = getDownloadArchiveExtension(); + // The `latest` alias is normalized to the SemVer wildcard. Oracle builds its + // download URLs from a concrete major and has no endpoint to list releases, + // so resolve the newest available GA major from the Adoptium API and use it. + if (this.latest) { + const latestMajor = await getLatestMajorVersion(this.http); + range = latestMajor.toString(); + } + const isOnlyMajorProvided = !range.includes('.'); const major = isOnlyMajorProvided ? range : range.split('.')[0]; @@ -113,6 +122,12 @@ export class OracleDistribution extends JavaBase { } } + if (this.latest) { + const error = this.createVersionNotFoundError(range); + error.message += `\nThe latest Java major version (${range}) is not yet available for the Oracle JDK distribution. Please specify a concrete version instead of 'latest'.`; + throw error; + } + throw this.createVersionNotFoundError(range); } diff --git a/src/setup-java.ts b/src/setup-java.ts index e4161d2db..1f5e60cde 100644 --- a/src/setup-java.ts +++ b/src/setup-java.ts @@ -186,8 +186,14 @@ async function installVersion( } const result = await distribution.setupJava(); + + // When the `latest` alias is used, the literal input isn't a real version, so + // pass the resolved version to the toolchains configuration instead. + const isLatest = version.trim().toLowerCase() === 'latest'; + const toolchainVersion = isLatest ? result.version : version; + await toolchains.configureToolchains( - version, + toolchainVersion, distributionName, result.path, toolchainIds[toolchainId] diff --git a/src/util.ts b/src/util.ts index 58a37dd1e..74b739681 100644 --- a/src/util.ts +++ b/src/util.ts @@ -6,6 +6,7 @@ import * as cache from '@actions/cache'; import * as core from '@actions/core'; import * as tc from '@actions/tool-cache'; +import * as httpm from '@actions/http-client'; import { INPUT_JOB_STATUS, DISTRIBUTIONS_ONLY_MAJOR_VERSION @@ -382,3 +383,33 @@ export function renameWinArchive(javaArchivePath: string): string { fs.renameSync(javaArchivePath, javaArchivePathRenamed); return javaArchivePathRenamed; } + +interface IAdoptiumAvailableReleases { + most_recent_feature_release: number; +} + +// Resolve the newest available stable/GA feature (major) release. +// +// Some distributions (e.g. Oracle, GraalVM) construct their download URLs from a +// concrete major version and don't expose an endpoint to list every available +// release, so a bare `latest` alias can't be resolved from their own metadata. +// The Adoptium (Temurin) API is used as a proxy for "what is the newest GA major +// version out there", which those distributions typically publish at the same time. +export async function getLatestMajorVersion( + http: httpm.HttpClient +): Promise { + const availableReleasesUrl = + 'https://api.adoptium.net/v3/info/available_releases'; + + const response = + await http.getJson(availableReleasesUrl); + + const mostRecent = response.result?.most_recent_feature_release; + if (!mostRecent || Number.isNaN(Number(mostRecent))) { + throw new Error( + `Could not determine the latest available Java major version from ${availableReleasesUrl}` + ); + } + + return Number(mostRecent); +} From 7155e1ca9012679a3b7683ccaccf65c502fc22f3 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Thu, 9 Jul 2026 15:30:23 -0400 Subject: [PATCH 2/5] Clarify latest note for oracle/graalvm version resolution vs download Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f56d02883..5746cda71 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ The `java-version` input supports an exact version or a version range using [Sem > - `latest` always resolves the newest version from the distribution's remote metadata (it behaves like `check-latest: true`), so it ignores any older version already present in the runner tool cache. This has the same performance trade-off described in [Check latest](#check-latest). > - `latest` is only supported through the `java-version` input, not through `java-version-file`, and it resolves stable (GA) releases only — it cannot be combined with `-ea`. > - The `jdkfile` distribution does not support `latest`, as it installs from a local file. -> - For `oracle` and `graalvm`, `latest` resolves the newest GA major version via the Adoptium API and then requests it from the distribution. If that distribution hasn't published the newest major yet, the action fails and asks you to specify a concrete version. Note the Oracle JDK license caveat below still applies to a floating `latest`. +> - For `oracle` and `graalvm`, `latest` only uses the Adoptium API to determine the newest GA **major version number** — the JDK binary itself is still downloaded from the Oracle / GraalVM servers for that major. If those servers haven't published the resolved major yet, the action fails and asks you to specify a concrete version. Note the Oracle JDK license caveat below still applies to a floating `latest`. #### Supported distributions Currently, the following distributions are supported: From 94eba1b8f50ec5a88d1c5cabc40af82a98e799ff Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Thu, 9 Jul 2026 15:44:30 -0400 Subject: [PATCH 3/5] graalvm-community: float latest to its own newest GA release GraalVM Community publishes its releases on GitHub, so the `latest` alias now matches against that real release list using the SemVer wildcard instead of asking the Adoptium API for the newest GA major. This prevents `latest` from hard-failing when GraalVM lags behind a freshly released Java major (e.g. Adoptium reports 26 before GraalVM ships it). Oracle GraalVM has no listing endpoint, so it keeps deriving the newest major from Adoptium and errors clearly if that major is not yet published. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 3 +- .../distributors/graalvm-installer.test.ts | 54 +++++++++++++++++++ dist/setup/index.js | 23 +++++--- src/distributions/graalvm/installer.ts | 25 ++++++--- 4 files changed, 92 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5746cda71..daa1b7b23 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,8 @@ The `java-version` input supports an exact version or a version range using [Sem > - `latest` always resolves the newest version from the distribution's remote metadata (it behaves like `check-latest: true`), so it ignores any older version already present in the runner tool cache. This has the same performance trade-off described in [Check latest](#check-latest). > - `latest` is only supported through the `java-version` input, not through `java-version-file`, and it resolves stable (GA) releases only — it cannot be combined with `-ea`. > - The `jdkfile` distribution does not support `latest`, as it installs from a local file. -> - For `oracle` and `graalvm`, `latest` only uses the Adoptium API to determine the newest GA **major version number** — the JDK binary itself is still downloaded from the Oracle / GraalVM servers for that major. If those servers haven't published the resolved major yet, the action fails and asks you to specify a concrete version. Note the Oracle JDK license caveat below still applies to a floating `latest`. +> - For `oracle` and `graalvm` (Oracle GraalVM), `latest` uses the Adoptium API only to determine the newest GA **major version number** — the JDK binary itself is still downloaded from the Oracle / GraalVM servers for that major. Because these distributions have no endpoint to list their own releases, if their servers haven't published the resolved major yet, the action fails and asks you to specify a concrete version. Note the Oracle JDK license caveat below still applies to a floating `latest`. +> - For `graalvm-community`, `latest` floats to the newest GA release published on GitHub, so it never depends on the Adoptium API and always resolves to the newest major that GraalVM Community actually ships. #### Supported distributions Currently, the following distributions are supported: diff --git a/__tests__/distributors/graalvm-installer.test.ts b/__tests__/distributors/graalvm-installer.test.ts index 7af2b81e5..c2e303d99 100644 --- a/__tests__/distributors/graalvm-installer.test.ts +++ b/__tests__/distributors/graalvm-installer.test.ts @@ -1170,6 +1170,60 @@ describe('GraalVMDistribution', () => { }); }); + it('resolves latest to the newest GA across all Community majors without calling Adoptium', async () => { + const latestCommunity = new GraalVMCommunityDistribution({ + ...defaultOptions, + version: 'latest' + }); + (latestCommunity as any).http = mockHttpClient; + jest.spyOn(latestCommunity, 'getPlatform').mockReturnValue('linux'); + + mockHttpClient.getJson.mockResolvedValue({ + result: [ + { + draft: false, + prerelease: false, + assets: [ + { + name: 'graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz', + browser_download_url: + 'https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz' + } + ] + }, + { + draft: false, + prerelease: false, + assets: [ + { + name: 'graalvm-community-jdk-24.0.1_linux-x64_bin.tar.gz', + browser_download_url: + 'https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-24.0.1/graalvm-community-jdk-24.0.1_linux-x64_bin.tar.gz' + } + ] + } + ], + statusCode: 200, + headers: {} + }); + + const result = await (latestCommunity as any).findPackageForDownload( + 'x' + ); + + expect(result).toEqual({ + url: 'https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-24.0.1/graalvm-community-jdk-24.0.1_linux-x64_bin.tar.gz', + version: '24.0.1' + }); + // The Community release list is authoritative, so the Adoptium + // most_recent_feature_release endpoint must not be consulted. + expect(mockHttpClient.getJson).toHaveBeenCalledTimes(1); + expect(mockHttpClient.getJson).toHaveBeenCalledWith( + expect.stringContaining('graalvm-ce-builds/releases'), + expect.anything() + ); + }); + it('should reject GraalVM Community early access requests', async () => { (communityDistribution as any).stable = false; diff --git a/dist/setup/index.js b/dist/setup/index.js index ea93d60c2..b907944d1 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -133108,14 +133108,25 @@ class GraalVMCommunityDistribution extends GraalVMDistribution { if (!this.stable) { throw new Error('GraalVM Community does not provide early access builds'); } - // The `latest` alias is normalized to the SemVer wildcard. Resolve the newest - // available GA major from the Adoptium API so it can be matched against the - // published GraalVM Community releases. + const arch = this.getSupportedArchitecture(); + // GraalVM Community publishes its releases on GitHub, so the `latest` alias + // (normalized to the SemVer wildcard `x`) can float to the newest GA it + // actually ships. Unlike Oracle GraalVM (which has no listing endpoint and + // must derive the newest major from the Adoptium API), we match against the + // real release list here, so `latest` never fails when GraalVM lags behind a + // brand-new Java major. + let platform; + let extension; if (this.latest) { - range = (await getLatestMajorVersion(this.http)).toString(); + if (this.packageType !== 'jdk') { + throw new Error(`${this.distribution} provides only the \`jdk\` package type`); + } + platform = this.getPlatform(); + extension = getDownloadArchiveExtension(); + } + else { + ({ platform, extension } = this.validateStableBuildRequest(range)); } - const arch = this.getSupportedArchitecture(); - const { platform, extension } = this.validateStableBuildRequest(range); // GraalVM Community asset names embed the platform, architecture and // archive type, e.g. `graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz`. const assetSuffix = `_${platform}-${arch}_bin.${extension}`; diff --git a/src/distributions/graalvm/installer.ts b/src/distributions/graalvm/installer.ts index a55375071..ccbb5b441 100644 --- a/src/distributions/graalvm/installer.ts +++ b/src/distributions/graalvm/installer.ts @@ -364,15 +364,28 @@ export class GraalVMCommunityDistribution extends GraalVMDistribution { throw new Error('GraalVM Community does not provide early access builds'); } - // The `latest` alias is normalized to the SemVer wildcard. Resolve the newest - // available GA major from the Adoptium API so it can be matched against the - // published GraalVM Community releases. + const arch = this.getSupportedArchitecture(); + + // GraalVM Community publishes its releases on GitHub, so the `latest` alias + // (normalized to the SemVer wildcard `x`) can float to the newest GA it + // actually ships. Unlike Oracle GraalVM (which has no listing endpoint and + // must derive the newest major from the Adoptium API), we match against the + // real release list here, so `latest` never fails when GraalVM lags behind a + // brand-new Java major. + let platform: OsVersions; + let extension: string; if (this.latest) { - range = (await getLatestMajorVersion(this.http)).toString(); + if (this.packageType !== 'jdk') { + throw new Error( + `${this.distribution} provides only the \`jdk\` package type` + ); + } + platform = this.getPlatform(); + extension = getDownloadArchiveExtension(); + } else { + ({platform, extension} = this.validateStableBuildRequest(range)); } - const arch = this.getSupportedArchitecture(); - const {platform, extension} = this.validateStableBuildRequest(range); // GraalVM Community asset names embed the platform, architecture and // archive type, e.g. `graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz`. const assetSuffix = `_${platform}-${arch}_bin.${extension}`; From 28eade3106500c4a674896ca88e7e23ab0edd923 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Thu, 9 Jul 2026 17:17:13 -0400 Subject: [PATCH 4/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/distributions/corretto/installer.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/distributions/corretto/installer.ts b/src/distributions/corretto/installer.ts index ca68ed21c..66516a220 100644 --- a/src/distributions/corretto/installer.ts +++ b/src/distributions/corretto/installer.ts @@ -65,11 +65,17 @@ export class CorrettoDistribution extends JavaBase { // matches on an exact major version, so resolve it to the newest available // major from Corretto's own list. if (this.latest) { - const latestMajor = availableVersions + const majors = availableVersions .map(item => parseInt(item.version, 10)) - .filter(major => !Number.isNaN(major)) - .reduce((max, current) => (current > max ? current : max), 0); - version = latestMajor.toString(); + .filter(major => Number.isFinite(major) && major > 0); + + if (majors.length === 0) { + throw new Error( + 'Could not determine the latest available Corretto major version from remote metadata' + ); + } + + version = Math.max(...majors).toString(); } if (version.includes('.')) { From da3bbf55d272afbaf4fa76603f81e835b2936b04 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Thu, 9 Jul 2026 17:19:16 -0400 Subject: [PATCH 5/5] Give latest+qualifier inputs (e.g. latest-ea) a targeted error Inputs like 'latest-ea' had their '-ea' suffix stripped and fell through to the generic SemVer validation, failing with a confusing "'latest' is not valid SemVer" message even though 'latest' is supported. Add an explicit guard so any 'latest*' value other than exactly 'latest' throws a targeted error explaining that 'latest' resolves GA releases only and cannot be combined with '-ea' or other qualifiers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- __tests__/distributors/base-installer.test.ts | 11 ++++++++++ dist/setup/index.js | 21 ++++++++++++++----- src/distributions/base-installer.ts | 14 ++++++++++++- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/__tests__/distributors/base-installer.test.ts b/__tests__/distributors/base-installer.test.ts index a3cef10c5..ad3b0afc5 100644 --- a/__tests__/distributors/base-installer.test.ts +++ b/__tests__/distributors/base-installer.test.ts @@ -793,6 +793,17 @@ describe('normalizeVersion', () => { `The string '${version}' is not valid SemVer notation for a Java version. Please check README file for code snippets and more detailed information` ); }); + + it.each(['latest-ea', 'latest.1', 'LATEST-EA', ' latest-ea '])( + 'normalizeVersion should throw a targeted error for latest combined with a qualifier (%s)', + version => { + expect( + DummyJavaBase.prototype.normalizeVersion.bind(null, version) + ).toThrow( + `The 'latest' alias resolves stable (GA) releases only and cannot be combined with '-ea' or other qualifiers (received '${version}'). Use 'latest' on its own, or specify a concrete version.` + ); + } + ); }); describe('createVersionNotFoundError', () => { diff --git a/dist/setup/index.js b/dist/setup/index.js index f6e215971..b3e2cb671 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -131274,13 +131274,22 @@ class JavaBase { // Support the `latest` alias (case-insensitive), which floats to the newest // available stable/GA release. It is translated to the SemVer wildcard `x` // so the existing "newest satisfying version wins" resolution applies. - if (version.trim().toLowerCase() === 'latest') { + const normalized = version.trim().toLowerCase(); + if (normalized === 'latest') { return { version: 'x', stable: true, latest: true }; } + // Reject `latest` combined with any qualifier (e.g. `latest-ea`). Such inputs + // would otherwise have their `-ea` suffix stripped and fall through to the + // generic SemVer check, which fails with a confusing "'latest' is not valid + // SemVer" message even though `latest` is a supported value. Fail early with a + // targeted explanation instead. + if (normalized.startsWith('latest')) { + throw new Error(`The 'latest' alias resolves stable (GA) releases only and cannot be combined with '-ea' or other qualifiers (received '${version}'). Use 'latest' on its own, or specify a concrete version.`); + } if (version.endsWith('-ea')) { version = version.replace(/-ea$/, ''); stable = false; @@ -132401,11 +132410,13 @@ class CorrettoDistribution extends JavaBase { // matches on an exact major version, so resolve it to the newest available // major from Corretto's own list. if (this.latest) { - const latestMajor = availableVersions + const majors = availableVersions .map(item => parseInt(item.version, 10)) - .filter(major => !Number.isNaN(major)) - .reduce((max, current) => (current > max ? current : max), 0); - version = latestMajor.toString(); + .filter(major => Number.isFinite(major) && major > 0); + if (majors.length === 0) { + throw new Error('Could not determine the latest available Corretto major version from remote metadata'); + } + version = Math.max(...majors).toString(); } if (version.includes('.')) { throw new Error('Only major versions are supported'); diff --git a/src/distributions/base-installer.ts b/src/distributions/base-installer.ts index 9ed68600b..07e7c444b 100644 --- a/src/distributions/base-installer.ts +++ b/src/distributions/base-installer.ts @@ -275,7 +275,8 @@ export abstract class JavaBase { // Support the `latest` alias (case-insensitive), which floats to the newest // available stable/GA release. It is translated to the SemVer wildcard `x` // so the existing "newest satisfying version wins" resolution applies. - if (version.trim().toLowerCase() === 'latest') { + const normalized = version.trim().toLowerCase(); + if (normalized === 'latest') { return { version: 'x', stable: true, @@ -283,6 +284,17 @@ export abstract class JavaBase { }; } + // Reject `latest` combined with any qualifier (e.g. `latest-ea`). Such inputs + // would otherwise have their `-ea` suffix stripped and fall through to the + // generic SemVer check, which fails with a confusing "'latest' is not valid + // SemVer" message even though `latest` is a supported value. Fail early with a + // targeted explanation instead. + if (normalized.startsWith('latest')) { + throw new Error( + `The 'latest' alias resolves stable (GA) releases only and cannot be combined with '-ea' or other qualifiers (received '${version}'). Use 'latest' on its own, or specify a concrete version.` + ); + } + if (version.endsWith('-ea')) { version = version.replace(/-ea$/, ''); stable = false;