Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
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 <[email protected]>
  • Loading branch information
brunoborges and Copilot committed Jul 9, 2026
commit 71ee2e9aa6ab3e0ef98c05d887de335f90f1da4b
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
36 changes: 31 additions & 5 deletions __tests__/distributors/base-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
[
{
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions __tests__/distributors/corretto-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
55 changes: 55 additions & 0 deletions __tests__/distributors/graalvm-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
14 changes: 14 additions & 0 deletions __tests__/distributors/local-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
56 changes: 56 additions & 0 deletions __tests__/distributors/oracle-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/
);
});
});
18 changes: 18 additions & 0 deletions __tests__/distributors/temurin-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand Down
33 changes: 32 additions & 1 deletion __tests__/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ const {
isVersionSatisfies,
isCacheFeatureAvailable,
isGhes,
validatePaginationUrl
validatePaginationUrl,
getLatestMajorVersion
} = await import('../src/util.js');

describe('isVersionSatisfies', () => {
Expand Down Expand Up @@ -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'
);
});
});
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
16 changes: 16 additions & 0 deletions dist/cleanup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading