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>
This commit is contained in:
Bruno Borges
2026-07-09 15:20:59 -04:00
parent 548a822bee
commit 71ee2e9aa6
18 changed files with 441 additions and 19 deletions

View File

@@ -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` - 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` - 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` - 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 #### Supported distributions
Currently, the following distributions are supported: Currently, the following distributions are supported:

View File

@@ -420,6 +420,29 @@ describe('setupJava', () => {
expect(spyCoreInfo).not.toHaveBeenCalledWith('Trying to download...'); 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([ it.each([
[ [
{ {
@@ -744,11 +767,14 @@ describe('normalizeVersion', () => {
const DummyJavaBase = JavaBase as any; const DummyJavaBase = JavaBase as any;
it.each([ it.each([
['11', {version: '11', stable: true}], ['11', {version: '11', stable: true, latest: false}],
['11.0', {version: '11.0', stable: true}], ['11.0', {version: '11.0', stable: true, latest: false}],
['11.0.10', {version: '11.0.10', stable: true}], ['11.0.10', {version: '11.0.10', stable: true, latest: false}],
['11-ea', {version: '11', stable: false}], ['11-ea', {version: '11', stable: false, latest: false}],
['11.0.2-ea', {version: '11.0.2', stable: 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) => { ])('normalizeVersion from %s to %s', (input, expected) => {
expect(DummyJavaBase.prototype.normalizeVersion.call(null, input)).toEqual( expect(DummyJavaBase.prototype.normalizeVersion.call(null, input)).toEqual(
expected expected

View File

@@ -204,6 +204,24 @@ describe('getAvailableVersions', () => {
expect(availableVersion.url).toBe(expectedLink); 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 () => { it('with unstable version expect to throw not supported error', async () => {
const version = '18.0.1-ea'; const version = '18.0.1-ea';
const distribution = new CorrettoDistribution({ const distribution = new CorrettoDistribution({

View File

@@ -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 () => { it('should throw error for JDK versions less than 17', async () => {
await expect( await expect(
(distribution as any).findPackageForDownload('11') (distribution as any).findPackageForDownload('11')

View File

@@ -170,6 +170,20 @@ describe('setupJava', () => {
jest.restoreAllMocks(); 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 () => { it('java is resolved from toolcache, jdkfile is untouched', async () => {
const inputs = { const inputs = {
version: actualJavaVersion, version: actualJavaVersion,

View File

@@ -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/
);
});
});

View File

@@ -312,6 +312,24 @@ describe('findPackageForDownload', () => {
expect(resolvedVersion.signatureUrl).toBeDefined(); 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 () => { it('version is found but binaries list is empty', async () => {
const distribution = new TemurinDistribution( const distribution = new TemurinDistribution(
{ {

View File

@@ -56,7 +56,8 @@ const {
isVersionSatisfies, isVersionSatisfies,
isCacheFeatureAvailable, isCacheFeatureAvailable,
isGhes, isGhes,
validatePaginationUrl validatePaginationUrl,
getLatestMajorVersion
} = await import('../src/util.js'); } = await import('../src/util.js');
describe('isVersionSatisfies', () => { describe('isVersionSatisfies', () => {
@@ -400,3 +401,33 @@ describe('isGhes', () => {
expect(isGhes()).toBeTruthy(); 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'
);
});
});

View File

@@ -4,7 +4,7 @@ description: 'Set up a specific version of the Java JDK and add the
author: 'GitHub' author: 'GitHub'
inputs: inputs:
java-version: 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 required: false
java-version-file: 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' 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'

16
dist/cleanup/index.js vendored
View File

@@ -96154,6 +96154,22 @@ function renameWinArchive(javaArchivePath) {
fs.renameSync(javaArchivePath, javaArchivePathRenamed); fs.renameSync(javaArchivePath, javaArchivePathRenamed);
return 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 ;// CONCATENATED MODULE: ./src/gpg.ts

87
dist/setup/index.js vendored
View File

@@ -127146,6 +127146,22 @@ function renameWinArchive(javaArchivePath) {
external_fs_namespaceObject.renameSync(javaArchivePath, javaArchivePathRenamed); external_fs_namespaceObject.renameSync(javaArchivePath, javaArchivePathRenamed);
return 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 ;// CONCATENATED MODULE: ./src/gpg.ts
@@ -131045,6 +131061,7 @@ class JavaBase {
architecture; architecture;
packageType; packageType;
stable; stable;
latest;
checkLatest; checkLatest;
setDefault; setDefault;
verifySignature; verifySignature;
@@ -131055,7 +131072,11 @@ class JavaBase {
allowRetries: true, allowRetries: true,
maxRetries: 3 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.architecture = installerOptions.architecture || external_os_default().arch();
this.packageType = installerOptions.packageType; this.packageType = installerOptions.packageType;
this.checkLatest = installerOptions.checkLatest; this.checkLatest = installerOptions.checkLatest;
@@ -131071,7 +131092,7 @@ class JavaBase {
throw new Error(`Input 'verify-signature' is not supported for distribution '${this.distribution}'.`); throw new Error(`Input 'verify-signature' is not supported for distribution '${this.distribution}'.`);
} }
let foundJava = this.findInToolcache(); let foundJava = this.findInToolcache();
if (foundJava && !this.checkLatest) { if (foundJava && !this.checkLatest && !this.latest) {
info(`Resolved Java ${foundJava.version} from tool-cache`); info(`Resolved Java ${foundJava.version} from tool-cache`);
} }
else { else {
@@ -131249,6 +131270,17 @@ class JavaBase {
} }
normalizeVersion(version) { normalizeVersion(version) {
let stable = true; 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')) { if (version.endsWith('-ea')) {
version = version.replace(/-ea$/, ''); version = version.replace(/-ea$/, '');
stable = false; stable = false;
@@ -131263,7 +131295,8 @@ class JavaBase {
} }
return { return {
version, version,
stable stable,
latest
}; };
} }
createVersionNotFoundError(versionOrRange, availableVersions, additionalContext) { createVersionNotFoundError(versionOrRange, availableVersions, additionalContext) {
@@ -131337,6 +131370,9 @@ class LocalDistribution extends JavaBase {
this.jdkFile = jdkFile; this.jdkFile = jdkFile;
} }
async setupJava() { 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(); let foundJava = this.findInToolcache();
if (foundJava) { if (foundJava) {
info(`Resolved Java ${foundJava.version} from tool-cache`); info(`Resolved Java ${foundJava.version} from tool-cache`);
@@ -132352,10 +132388,20 @@ class CorrettoDistribution extends JavaBase {
if (!this.stable) { if (!this.stable) {
throw new Error('Early access versions are not supported'); 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('.')) { if (version.includes('.')) {
throw new Error('Only major versions are supported'); throw new Error('Only major versions are supported');
} }
const availableVersions = await this.getAvailableVersions();
const matchingVersions = availableVersions const matchingVersions = availableVersions
.filter(item => item.version == version) .filter(item => item.version == version)
.map(item => { .map(item => {
@@ -132483,6 +132529,13 @@ class OracleDistribution extends JavaBase {
} }
const platform = this.getPlatform(); const platform = this.getPlatform();
const extension = getDownloadArchiveExtension(); 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 isOnlyMajorProvided = !range.includes('.');
const major = isOnlyMajorProvided ? range : range.split('.')[0]; const major = isOnlyMajorProvided ? range : range.split('.')[0];
const possibleUrls = []; 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}`); 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); throw this.createVersionNotFoundError(range);
} }
getPlatform(platform = process.platform) { getPlatform(platform = process.platform) {
@@ -132907,6 +132965,12 @@ class GraalVMDistribution extends JavaBase {
if (!this.stable) { if (!this.stable) {
return this.findEABuildDownloadUrl(`${range}-ea`); 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 { platform, extension, major } = this.validateStableBuildRequest(range);
const fileUrl = this.constructFileUrl(range, major, platform, arch, extension); const fileUrl = this.constructFileUrl(range, major, platform, arch, extension);
const response = await this.http.head(fileUrl); const response = await this.http.head(fileUrl);
@@ -132955,6 +133019,9 @@ class GraalVMDistribution extends JavaBase {
if (statusCode === HttpCodes.NotFound) { if (statusCode === HttpCodes.NotFound) {
// Create the standard error with additional hint about checking the download URL // Create the standard error with additional hint about checking the download URL
const error = this.createVersionNotFoundError(range); 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.`; error.message += `\nPlease check if this version is available at ${GRAALVM_DOWNLOAD_URL} . Pick a version from the list.`;
throw error; throw error;
} }
@@ -133041,6 +133108,12 @@ class GraalVMCommunityDistribution extends GraalVMDistribution {
if (!this.stable) { if (!this.stable) {
throw new Error('GraalVM Community does not provide early access builds'); 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 arch = this.getSupportedArchitecture();
const { platform, extension } = this.validateStableBuildRequest(range); const { platform, extension } = this.validateStableBuildRequest(range);
// GraalVM Community asset names embed the platform, architecture and // 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}`); throw new Error(`No supported distribution was found for input ${distributionName}`);
} }
const result = await distribution.setupJava(); 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('');
info('Java configuration:'); info('Java configuration:');
info(` Distribution: ${distributionName}`); info(` Distribution: ${distributionName}`);

View File

@@ -19,6 +19,7 @@ export abstract class JavaBase {
protected architecture: string; protected architecture: string;
protected packageType: string; protected packageType: string;
protected stable: boolean; protected stable: boolean;
protected latest: boolean;
protected checkLatest: boolean; protected checkLatest: boolean;
protected setDefault: boolean; protected setDefault: boolean;
protected verifySignature: boolean; protected verifySignature: boolean;
@@ -33,9 +34,11 @@ export abstract class JavaBase {
maxRetries: 3 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.architecture = installerOptions.architecture || os.arch();
this.packageType = installerOptions.packageType; this.packageType = installerOptions.packageType;
this.checkLatest = installerOptions.checkLatest; this.checkLatest = installerOptions.checkLatest;
@@ -62,7 +65,7 @@ export abstract class JavaBase {
} }
let foundJava = this.findInToolcache(); let foundJava = this.findInToolcache();
if (foundJava && !this.checkLatest) { if (foundJava && !this.checkLatest && !this.latest) {
core.info(`Resolved Java ${foundJava.version} from tool-cache`); core.info(`Resolved Java ${foundJava.version} from tool-cache`);
} else { } else {
core.info('Trying to resolve the latest version from remote'); core.info('Trying to resolve the latest version from remote');
@@ -263,6 +266,18 @@ export abstract class JavaBase {
protected normalizeVersion(version: string) { protected normalizeVersion(version: string) {
let stable = true; 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')) { if (version.endsWith('-ea')) {
version = version.replace(/-ea$/, ''); version = version.replace(/-ea$/, '');
@@ -281,7 +296,8 @@ export abstract class JavaBase {
return { return {
version, version,
stable stable,
latest
}; };
} }

View File

@@ -59,10 +59,22 @@ export class CorrettoDistribution extends JavaBase {
if (!this.stable) { if (!this.stable) {
throw new Error('Early access versions are not supported'); 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('.')) { if (version.includes('.')) {
throw new Error('Only major versions are supported'); throw new Error('Only major versions are supported');
} }
const availableVersions = await this.getAvailableVersions();
const matchingVersions = availableVersions const matchingVersions = availableVersions
.filter(item => item.version == version) .filter(item => item.version == version)
.map(item => { .map(item => {

View File

@@ -16,6 +16,7 @@ import {
extractJdkFile, extractJdkFile,
getDownloadArchiveExtension, getDownloadArchiveExtension,
getGitHubHttpHeaders, getGitHubHttpHeaders,
getLatestMajorVersion,
getNextPageUrlFromLinkHeader, getNextPageUrlFromLinkHeader,
isVersionSatisfies, isVersionSatisfies,
MAX_PAGINATION_PAGES, MAX_PAGINATION_PAGES,
@@ -119,6 +120,13 @@ export class GraalVMDistribution extends JavaBase {
return this.findEABuildDownloadUrl(`${range}-ea`); 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 {platform, extension, major} = this.validateStableBuildRequest(range);
const fileUrl = this.constructFileUrl( const fileUrl = this.constructFileUrl(
@@ -203,6 +211,9 @@ export class GraalVMDistribution extends JavaBase {
if (statusCode === HttpCodes.NotFound) { if (statusCode === HttpCodes.NotFound) {
// Create the standard error with additional hint about checking the download URL // Create the standard error with additional hint about checking the download URL
const error = this.createVersionNotFoundError(range); 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.`; error.message += `\nPlease check if this version is available at ${GRAALVM_DOWNLOAD_URL} . Pick a version from the list.`;
throw error; throw error;
} }
@@ -353,6 +364,13 @@ export class GraalVMCommunityDistribution extends GraalVMDistribution {
throw new Error('GraalVM Community does not provide early access builds'); 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 arch = this.getSupportedArchitecture();
const {platform, extension} = this.validateStableBuildRequest(range); const {platform, extension} = this.validateStableBuildRequest(range);
// GraalVM Community asset names embed the platform, architecture and // GraalVM Community asset names embed the platform, architecture and

View File

@@ -22,6 +22,12 @@ export class LocalDistribution extends JavaBase {
} }
public async setupJava(): Promise<JavaInstallerResults> { public async setupJava(): Promise<JavaInstallerResults> {
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(); let foundJava = this.findInToolcache();
if (foundJava) { if (foundJava) {

View File

@@ -13,6 +13,7 @@ import {
import { import {
extractJdkFile, extractJdkFile,
getDownloadArchiveExtension, getDownloadArchiveExtension,
getLatestMajorVersion,
renameWinArchive renameWinArchive
} from '../../util.js'; } from '../../util.js';
import {HttpCodes} from '@actions/http-client'; import {HttpCodes} from '@actions/http-client';
@@ -73,6 +74,14 @@ export class OracleDistribution extends JavaBase {
const platform = this.getPlatform(); const platform = this.getPlatform();
const extension = getDownloadArchiveExtension(); 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 isOnlyMajorProvided = !range.includes('.');
const major = isOnlyMajorProvided ? range : range.split('.')[0]; 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); throw this.createVersionNotFoundError(range);
} }

View File

@@ -186,8 +186,14 @@ async function installVersion(
} }
const result = await distribution.setupJava(); 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( await toolchains.configureToolchains(
version, toolchainVersion,
distributionName, distributionName,
result.path, result.path,
toolchainIds[toolchainId] toolchainIds[toolchainId]

View File

@@ -6,6 +6,7 @@ import * as cache from '@actions/cache';
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as tc from '@actions/tool-cache'; import * as tc from '@actions/tool-cache';
import * as httpm from '@actions/http-client';
import { import {
INPUT_JOB_STATUS, INPUT_JOB_STATUS,
DISTRIBUTIONS_ONLY_MAJOR_VERSION DISTRIBUTIONS_ONLY_MAJOR_VERSION
@@ -382,3 +383,33 @@ export function renameWinArchive(javaArchivePath: string): string {
fs.renameSync(javaArchivePath, javaArchivePathRenamed); fs.renameSync(javaArchivePath, javaArchivePathRenamed);
return 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<number> {
const availableReleasesUrl =
'https://api.adoptium.net/v3/info/available_releases';
const response =
await http.getJson<IAdoptiumAvailableReleases>(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);
}