Skip to content

Commit 174d4b5

Browse files
brunoborgesCopilotCopilot
authored
Support pinning java-version as "latest" (#1093)
* 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> * Clarify latest note for oracle/graalvm version resolution vs download Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * 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> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * 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> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 86ce8e6 commit 174d4b5

18 files changed

Lines changed: 566 additions & 25 deletions

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,14 @@ The `java-version` input supports an exact version or a version range using [Sem
106106
- more specific versions: `8.0.282+8`, `8.0.232`, `11.0`, `11.0.4`, `17.0`
107107
- multi-field Java versions (JEP 322), such as: `11.0.9.1`, `18.0.1.1`
108108
- early access (EA) versions: `15-ea`, `15.0.0-ea`
109+
- the `latest` alias, which floats to the newest available stable (GA) release
110+
111+
> [!NOTE]
112+
> - `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).
113+
> - `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`.
114+
> - The `jdkfile` distribution does not support `latest`, as it installs from a local file.
115+
> - 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`.
116+
> - 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.
109117

110118
#### Supported distributions
111119
Currently, the following distributions are supported:

__tests__/distributors/base-installer.test.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,29 @@ describe('setupJava', () => {
420420
expect(spyCoreInfo).not.toHaveBeenCalledWith('Trying to download...');
421421
});
422422

423+
it('should resolve the latest version from remote when java-version is "latest", even if a version is cached', async () => {
424+
mockJavaBase = new EmptyJavaBase({
425+
version: 'latest',
426+
architecture: 'x86',
427+
packageType: 'jdk',
428+
checkLatest: false
429+
});
430+
431+
await expect(mockJavaBase.setupJava()).resolves.toEqual({
432+
version: actualJavaVersion,
433+
path: javaPathInstalled
434+
});
435+
436+
// `latest` must bypass the tool-cache short-circuit and always resolve remotely
437+
expect(spyCoreInfo).toHaveBeenCalledWith(
438+
'Trying to resolve the latest version from remote'
439+
);
440+
expect(spyCoreInfo).toHaveBeenCalledWith('Trying to download...');
441+
expect(spyCoreInfo).not.toHaveBeenCalledWith(
442+
`Resolved Java ${installedJavaVersion} from tool-cache`
443+
);
444+
});
445+
423446
it.each([
424447
[
425448
{
@@ -744,15 +767,18 @@ describe('normalizeVersion', () => {
744767
const DummyJavaBase = JavaBase as any;
745768

746769
it.each([
747-
['11', {version: '11', stable: true}],
748-
['11.0', {version: '11.0', stable: true}],
749-
['11.0.10', {version: '11.0.10', stable: true}],
750-
['11-ea', {version: '11', stable: false}],
751-
['11.0.2-ea', {version: '11.0.2', stable: false}],
752-
['18.0.1.1', {version: '18.0.1+1', stable: true}],
753-
['11.0.9.1', {version: '11.0.9+1', stable: true}],
754-
['12.0.2.1.0', {version: '12.0.2+1.0', stable: true}],
755-
['18.0.1.1-ea', {version: '18.0.1+1', stable: false}]
770+
['11', {version: '11', stable: true, latest: false}],
771+
['11.0', {version: '11.0', stable: true, latest: false}],
772+
['11.0.10', {version: '11.0.10', stable: true, latest: false}],
773+
['11-ea', {version: '11', stable: false, latest: false}],
774+
['11.0.2-ea', {version: '11.0.2', stable: false, latest: false}],
775+
['18.0.1.1', {version: '18.0.1+1', stable: true, latest: false}],
776+
['11.0.9.1', {version: '11.0.9+1', stable: true, latest: false}],
777+
['12.0.2.1.0', {version: '12.0.2+1.0', stable: true, latest: false}],
778+
['18.0.1.1-ea', {version: '18.0.1+1', stable: false, latest: false}],
779+
['latest', {version: 'x', stable: true, latest: true}],
780+
['LATEST', {version: 'x', stable: true, latest: true}],
781+
[' Latest ', {version: 'x', stable: true, latest: true}]
756782
])('normalizeVersion from %s to %s', (input, expected) => {
757783
expect(DummyJavaBase.prototype.normalizeVersion.call(null, input)).toEqual(
758784
expected
@@ -767,6 +793,17 @@ describe('normalizeVersion', () => {
767793
`The string '${version}' is not valid SemVer notation for a Java version. Please check README file for code snippets and more detailed information`
768794
);
769795
});
796+
797+
it.each(['latest-ea', 'latest.1', 'LATEST-EA', ' latest-ea '])(
798+
'normalizeVersion should throw a targeted error for latest combined with a qualifier (%s)',
799+
version => {
800+
expect(
801+
DummyJavaBase.prototype.normalizeVersion.bind(null, version)
802+
).toThrow(
803+
`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.`
804+
);
805+
}
806+
);
770807
});
771808

772809
describe('createVersionNotFoundError', () => {

__tests__/distributors/corretto-installer.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,24 @@ describe('getAvailableVersions', () => {
204204
expect(availableVersion.url).toBe(expectedLink);
205205
});
206206

207+
it('with latest resolves to the newest available major version', async () => {
208+
const distribution = new CorrettoDistribution({
209+
version: 'latest',
210+
architecture: 'x64',
211+
packageType: 'jdk',
212+
checkLatest: false
213+
});
214+
mockPlatform(distribution, 'linux');
215+
216+
const availableVersion =
217+
await distribution['findPackageForDownload']('x');
218+
expect(availableVersion).not.toBeNull();
219+
// 18 is the newest major present in the mocked Corretto index
220+
expect(availableVersion.url).toBe(
221+
'https://corretto.aws/downloads/resources/18.0.0.37.1/amazon-corretto-18.0.0.37.1-linux-x64.tar.gz'
222+
);
223+
});
224+
207225
it('with unstable version expect to throw not supported error', async () => {
208226
const version = '18.0.1-ea';
209227
const distribution = new CorrettoDistribution({

__tests__/distributors/graalvm-installer.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,61 @@ describe('GraalVMDistribution', () => {
417417
);
418418
});
419419

420+
describe('latest alias', () => {
421+
it('resolves the newest major version from the Adoptium API', async () => {
422+
const latestDistribution = new GraalVMDistribution({
423+
...defaultOptions,
424+
version: 'latest'
425+
});
426+
(latestDistribution as any).http = mockHttpClient;
427+
jest
428+
.spyOn(latestDistribution, 'getPlatform')
429+
.mockReturnValue('linux');
430+
mockHttpClient.getJson.mockResolvedValue({
431+
statusCode: 200,
432+
result: {most_recent_feature_release: 25},
433+
headers: {}
434+
});
435+
mockHttpClient.head.mockResolvedValue({
436+
message: {statusCode: 200}
437+
});
438+
439+
const result = await (
440+
latestDistribution as any
441+
).findPackageForDownload('x');
442+
443+
expect(result).toEqual({
444+
url: 'https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-x64_bin.tar.gz',
445+
version: '25'
446+
});
447+
});
448+
449+
it('throws an actionable error when the latest major is not yet available', async () => {
450+
const latestDistribution = new GraalVMDistribution({
451+
...defaultOptions,
452+
version: 'latest'
453+
});
454+
(latestDistribution as any).http = mockHttpClient;
455+
jest
456+
.spyOn(latestDistribution, 'getPlatform')
457+
.mockReturnValue('linux');
458+
mockHttpClient.getJson.mockResolvedValue({
459+
statusCode: 200,
460+
result: {most_recent_feature_release: 25},
461+
headers: {}
462+
});
463+
mockHttpClient.head.mockResolvedValue({
464+
message: {statusCode: 404}
465+
});
466+
467+
await expect(
468+
(latestDistribution as any).findPackageForDownload('x')
469+
).rejects.toThrow(
470+
/is not yet available for the GraalVM distribution/
471+
);
472+
});
473+
});
474+
420475
it('should throw error for JDK versions less than 17', async () => {
421476
await expect(
422477
(distribution as any).findPackageForDownload('11')
@@ -1115,6 +1170,60 @@ describe('GraalVMDistribution', () => {
11151170
});
11161171
});
11171172

1173+
it('resolves latest to the newest GA across all Community majors without calling Adoptium', async () => {
1174+
const latestCommunity = new GraalVMCommunityDistribution({
1175+
...defaultOptions,
1176+
version: 'latest'
1177+
});
1178+
(latestCommunity as any).http = mockHttpClient;
1179+
jest.spyOn(latestCommunity, 'getPlatform').mockReturnValue('linux');
1180+
1181+
mockHttpClient.getJson.mockResolvedValue({
1182+
result: [
1183+
{
1184+
draft: false,
1185+
prerelease: false,
1186+
assets: [
1187+
{
1188+
name: 'graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz',
1189+
browser_download_url:
1190+
'https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz'
1191+
}
1192+
]
1193+
},
1194+
{
1195+
draft: false,
1196+
prerelease: false,
1197+
assets: [
1198+
{
1199+
name: 'graalvm-community-jdk-24.0.1_linux-x64_bin.tar.gz',
1200+
browser_download_url:
1201+
'https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-24.0.1/graalvm-community-jdk-24.0.1_linux-x64_bin.tar.gz'
1202+
}
1203+
]
1204+
}
1205+
],
1206+
statusCode: 200,
1207+
headers: {}
1208+
});
1209+
1210+
const result = await (latestCommunity as any).findPackageForDownload(
1211+
'x'
1212+
);
1213+
1214+
expect(result).toEqual({
1215+
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',
1216+
version: '24.0.1'
1217+
});
1218+
// The Community release list is authoritative, so the Adoptium
1219+
// most_recent_feature_release endpoint must not be consulted.
1220+
expect(mockHttpClient.getJson).toHaveBeenCalledTimes(1);
1221+
expect(mockHttpClient.getJson).toHaveBeenCalledWith(
1222+
expect.stringContaining('graalvm-ce-builds/releases'),
1223+
expect.anything()
1224+
);
1225+
});
1226+
11181227
it('should reject GraalVM Community early access requests', async () => {
11191228
(communityDistribution as any).stable = false;
11201229

__tests__/distributors/local-installer.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,20 @@ describe('setupJava', () => {
170170
jest.restoreAllMocks();
171171
});
172172

173+
it('throws for the latest alias since jdkfile has no version list', async () => {
174+
const inputs = {
175+
version: 'latest',
176+
architecture: 'x86',
177+
packageType: 'jdk',
178+
checkLatest: false
179+
};
180+
181+
mockJavaBase = new LocalDistribution(inputs, expectedJdkFile);
182+
await expect(mockJavaBase.setupJava()).rejects.toThrow(
183+
"The 'latest' version alias is not supported for the 'jdkfile' distribution. Please specify a concrete version."
184+
);
185+
});
186+
173187
it('java is resolved from toolcache, jdkfile is untouched', async () => {
174188
const inputs = {
175189
version: actualJavaVersion,

__tests__/distributors/oracle-installer.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,59 @@ describe('findPackageForDownload', () => {
174174
);
175175
});
176176
});
177+
describe('findPackageForDownload with latest', () => {
178+
let spyHttpClientHead: any;
179+
let spyHttpClientGetJson: any;
180+
181+
beforeEach(() => {
182+
(core.debug as jest.Mock).mockImplementation(() => {});
183+
(core.error as jest.Mock).mockImplementation(() => {});
184+
spyHttpClientGetJson = jest.spyOn(HttpClient.prototype, 'getJson');
185+
spyHttpClientGetJson.mockResolvedValue({
186+
statusCode: 200,
187+
result: {most_recent_feature_release: 25},
188+
headers: {}
189+
});
190+
});
191+
192+
afterEach(() => {
193+
jest.restoreAllMocks();
194+
});
195+
196+
it('resolves the newest major version from the Adoptium API', async () => {
197+
spyHttpClientHead = jest.spyOn(HttpClient.prototype, 'head');
198+
spyHttpClientHead.mockResolvedValue({message: {statusCode: 200}});
199+
200+
const distribution = new OracleDistribution({
201+
version: 'latest',
202+
architecture: 'x64',
203+
packageType: 'jdk',
204+
checkLatest: false
205+
});
206+
207+
const result = await distribution['findPackageForDownload']('x');
208+
const osType = distribution.getPlatform();
209+
const archiveType = getDownloadArchiveExtension();
210+
211+
expect(result.version).toBe('25');
212+
expect(result.url).toBe(
213+
`https://download.oracle.com/java/25/latest/jdk-25_${osType}-x64_bin.${archiveType}`
214+
);
215+
});
216+
217+
it('throws an actionable error when the latest major is not yet available', async () => {
218+
spyHttpClientHead = jest.spyOn(HttpClient.prototype, 'head');
219+
spyHttpClientHead.mockResolvedValue({message: {statusCode: 404}});
220+
221+
const distribution = new OracleDistribution({
222+
version: 'latest',
223+
architecture: 'x64',
224+
packageType: 'jdk',
225+
checkLatest: false
226+
});
227+
228+
await expect(distribution['findPackageForDownload']('x')).rejects.toThrow(
229+
/is not yet available for the Oracle JDK distribution/
230+
);
231+
});
232+
});

__tests__/distributors/temurin-installer.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,24 @@ describe('findPackageForDownload', () => {
312312
expect(resolvedVersion.signatureUrl).toBeDefined();
313313
});
314314

315+
it('version "latest" is normalized to the newest available version', async () => {
316+
const distribution = new TemurinDistribution(
317+
{
318+
version: 'latest',
319+
architecture: 'x64',
320+
packageType: 'jdk',
321+
checkLatest: false
322+
},
323+
TemurinImplementation.Hotspot
324+
);
325+
distribution['getAvailableVersions'] = async () => manifestData as any;
326+
// normalizeVersion turns `latest` into the wildcard carried on `this.version`
327+
const resolvedVersion = await distribution['findPackageForDownload'](
328+
distribution['version']
329+
);
330+
expect(resolvedVersion.version).toBe('16.0.2+7');
331+
});
332+
315333
it('version is found but binaries list is empty', async () => {
316334
const distribution = new TemurinDistribution(
317335
{

__tests__/util.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ const {
5656
isVersionSatisfies,
5757
isCacheFeatureAvailable,
5858
isGhes,
59-
validatePaginationUrl
59+
validatePaginationUrl,
60+
getLatestMajorVersion
6061
} = await import('../src/util.js');
6162

6263
describe('isVersionSatisfies', () => {
@@ -400,3 +401,33 @@ describe('isGhes', () => {
400401
expect(isGhes()).toBeTruthy();
401402
});
402403
});
404+
405+
describe('getLatestMajorVersion', () => {
406+
const makeHttp = (getJson: jest.Mock) =>
407+
({getJson}) as unknown as import('@actions/http-client').HttpClient;
408+
409+
it('returns most_recent_feature_release from the Adoptium API', async () => {
410+
const getJson = jest.fn(async () => ({
411+
statusCode: 200,
412+
result: {most_recent_feature_release: 25},
413+
headers: {}
414+
}));
415+
416+
await expect(getLatestMajorVersion(makeHttp(getJson))).resolves.toBe(25);
417+
expect(getJson).toHaveBeenCalledWith(
418+
'https://api.adoptium.net/v3/info/available_releases'
419+
);
420+
});
421+
422+
it('throws when the response does not contain a usable value', async () => {
423+
const getJson = jest.fn(async () => ({
424+
statusCode: 200,
425+
result: {},
426+
headers: {}
427+
}));
428+
429+
await expect(getLatestMajorVersion(makeHttp(getJson))).rejects.toThrow(
430+
'Could not determine the latest available Java major version'
431+
);
432+
});
433+
});

action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: 'Set up a specific version of the Java JDK and add the
44
author: 'GitHub'
55
inputs:
66
java-version:
7-
description: 'The Java version to set up. Takes a whole or semver Java version. See examples of supported syntax in README file'
7+
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'
88
required: false
99
java-version-file:
1010
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'

0 commit comments

Comments
 (0)