Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ The `java-version` input supports an exact version or a version range using [Sem
- more specific versions: `8.0.282+8`, `8.0.232`, `11.0`, `11.0.4`, `17.0`
- multi-field Java versions (JEP 322), such as: `11.0.9.1`, `18.0.1.1`
- 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` (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:
Expand Down
55 changes: 46 additions & 9 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,15 +767,18 @@ 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}],
['18.0.1.1', {version: '18.0.1+1', stable: true}],
['11.0.9.1', {version: '11.0.9+1', stable: true}],
['12.0.2.1.0', {version: '12.0.2+1.0', stable: true}],
['18.0.1.1-ea', {version: '18.0.1+1', 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}],
['18.0.1.1', {version: '18.0.1+1', stable: true, latest: false}],
['11.0.9.1', {version: '11.0.9+1', stable: true, latest: false}],
['12.0.2.1.0', {version: '12.0.2+1.0', stable: true, latest: false}],
['18.0.1.1-ea', {version: '18.0.1+1', 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 All @@ -767,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', () => {
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
109 changes: 109 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 Expand Up @@ -1115,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;

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
Loading
Loading