mirror of
https://github.com/actions/setup-java.git
synced 2026-08-06 17:12:58 +00:00
2b61aea53d
A floating Oracle JDK or Oracle GraalVM request now skips the tool-cache short-circuit entirely, so every job re-downloads and re-extracts the JDK even when the exact bytes the mutable URL currently serves are already installed locally. The JDK resolution cache is keyed on the artifact's checksum (or, failing that, its HTTP response fingerprint), so a hit proves which concrete version the URL is serving right now. Once it has vouched for that version, an existing tool-cache installation of exactly that version is the artifact the download would have produced, and can be reused. Reuse is therefore gated on the resolution cache hit, never on the requested major: an unidentified floating artifact still downloads, as does an explicit force-download. Co-authored-by: Bruno Borges <brunoborges@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f2d9a891-a680-4b35-9644-cf2450f76f6d
746 lines
26 KiB
TypeScript
746 lines
26 KiB
TypeScript
import * as tc from '@actions/tool-cache';
|
|
import * as core from '@actions/core';
|
|
import * as fs from 'fs';
|
|
import semver from 'semver';
|
|
import path from 'path';
|
|
import * as httpm from '@actions/http-client';
|
|
import {
|
|
convertVersionToSemver,
|
|
getToolcachePath,
|
|
isVersionSatisfies
|
|
} from '../util.js';
|
|
import {
|
|
ChecksumAlgorithm,
|
|
ChecksumMetadata,
|
|
JavaDownloadRelease,
|
|
JavaInstallerOptions,
|
|
JavaInstallerResults
|
|
} from './base-models.js';
|
|
import {MACOS_JAVA_CONTENT_POSTFIX} from '../constants.js';
|
|
import {RetryingHttpClient} from '../retrying-http-client.js';
|
|
import os from 'os';
|
|
import {expectedDigestLength, verifyChecksum} from '../checksum.js';
|
|
import {normalizeArchitecture} from './platform-types.js';
|
|
import type {JdkCache} from '../jdk-cache.js';
|
|
|
|
export abstract class JavaBase {
|
|
protected http: httpm.HttpClient;
|
|
protected version: string;
|
|
protected architecture: string;
|
|
protected packageType: string;
|
|
protected stable: boolean;
|
|
protected latest: boolean;
|
|
protected checkLatest: boolean;
|
|
protected forceDownload: boolean;
|
|
protected cacheJdk: boolean;
|
|
/**
|
|
* Whether the concrete version of a floating release has been established
|
|
* from the checksum-bound resolution cache. Until then the release version is
|
|
* only the requested major and says nothing about the bytes behind the URL.
|
|
*/
|
|
private floatingVersionVerified = false;
|
|
protected setDefault: boolean;
|
|
protected verifySignature: boolean;
|
|
protected verifySignaturePublicKey: string | undefined;
|
|
|
|
constructor(
|
|
protected distribution: string,
|
|
installerOptions: JavaInstallerOptions
|
|
) {
|
|
this.http = new RetryingHttpClient('actions/setup-java');
|
|
|
|
({
|
|
version: this.version,
|
|
stable: this.stable,
|
|
latest: this.latest
|
|
} = this.normalizeVersion(installerOptions.version));
|
|
this.architecture = normalizeArchitecture(
|
|
installerOptions.architecture || os.arch()
|
|
);
|
|
this.packageType = installerOptions.packageType;
|
|
this.checkLatest = installerOptions.checkLatest;
|
|
this.forceDownload = installerOptions.forceDownload ?? false;
|
|
this.cacheJdk = installerOptions.cacheJdk ?? false;
|
|
this.setDefault =
|
|
installerOptions.setDefault !== undefined
|
|
? installerOptions.setDefault
|
|
: true;
|
|
this.verifySignature = installerOptions.verifySignature ?? false;
|
|
this.verifySignaturePublicKey = installerOptions.verifySignaturePublicKey;
|
|
}
|
|
|
|
protected abstract downloadTool(
|
|
javaRelease: JavaDownloadRelease
|
|
): Promise<JavaInstallerResults>;
|
|
protected abstract findPackageForDownload(
|
|
range: string
|
|
): Promise<JavaDownloadRelease>;
|
|
|
|
protected async downloadAndVerify(
|
|
javaRelease: JavaDownloadRelease
|
|
): Promise<string> {
|
|
const archivePath = await tc.downloadTool(javaRelease.url);
|
|
const checksum = javaRelease.checksum;
|
|
if (!checksum || !checksum.value?.trim()) {
|
|
core.debug(
|
|
`No authoritative checksum is available for ${this.distribution} version ${javaRelease.version}; skipping checksum verification.`
|
|
);
|
|
return archivePath;
|
|
}
|
|
|
|
try {
|
|
await verifyChecksum(archivePath, checksum, {
|
|
distribution: this.distribution,
|
|
version: javaRelease.version
|
|
});
|
|
core.debug(
|
|
`Verified ${checksum.algorithm} checksum for ${this.distribution} version ${javaRelease.version}.`
|
|
);
|
|
return archivePath;
|
|
} catch (error) {
|
|
let cleanupError: unknown;
|
|
let cleanupFailed = false;
|
|
try {
|
|
await fs.promises.rm(archivePath, {force: true});
|
|
} catch (caughtCleanupError) {
|
|
cleanupError = caughtCleanupError;
|
|
cleanupFailed = true;
|
|
}
|
|
if (cleanupFailed) {
|
|
throw new Error(
|
|
`${(error as Error).message} Failed to remove the downloaded archive after verification failure: ${(cleanupError as Error).message}`,
|
|
{cause: error}
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
protected async fetchChecksum(
|
|
checksumUrl: string,
|
|
algorithm: ChecksumAlgorithm | ChecksumAlgorithm[]
|
|
): Promise<ChecksumMetadata | undefined> {
|
|
// Some vendors (e.g. JetBrains) publish a single, generically-named
|
|
// checksum sibling (`.checksum`) whose digest algorithm isn't disclosed
|
|
// by the URL and has changed across releases. Accepting a list of
|
|
// candidate algorithms lets callers pass every algorithm the vendor is
|
|
// known to use; the actual algorithm is then inferred from the length of
|
|
// the returned digest.
|
|
const algorithms = Array.isArray(algorithm) ? algorithm : [algorithm];
|
|
const algorithmLabel = algorithms.join(' or ');
|
|
|
|
const response = await this.http.get(checksumUrl);
|
|
const statusCode = response.message.statusCode;
|
|
const source = (() => {
|
|
try {
|
|
const url = new URL(checksumUrl);
|
|
return `${url.origin}${url.pathname}`;
|
|
} catch {
|
|
return 'an invalid checksum URL';
|
|
}
|
|
})();
|
|
|
|
if (statusCode === httpm.HttpCodes.NotFound) {
|
|
core.debug(
|
|
`No authoritative ${algorithmLabel} checksum is available for ${this.distribution} from ${source}; skipping checksum verification.`
|
|
);
|
|
return undefined;
|
|
}
|
|
|
|
if (statusCode !== httpm.HttpCodes.OK) {
|
|
throw new Error(
|
|
`Failed to fetch the authoritative ${algorithmLabel} checksum for ${this.distribution} from ${source} (HTTP ${statusCode}).`
|
|
);
|
|
}
|
|
|
|
const body = await response.readBody();
|
|
const value = body.trim().split(/\s+/, 1)[0] ?? '';
|
|
if (!value) {
|
|
throw new Error(
|
|
`Received an empty authoritative ${algorithmLabel} checksum for ${this.distribution} from ${source}.`
|
|
);
|
|
}
|
|
|
|
// Prefer the strongest algorithm whose digest length matches what was
|
|
// actually returned; fall back to the first candidate (preserving prior
|
|
// behavior/error messages) when the digest doesn't match any of them.
|
|
const resolvedAlgorithm =
|
|
algorithms.find(algo => value.length === expectedDigestLength(algo)) ??
|
|
algorithms[0];
|
|
|
|
return {algorithm: resolvedAlgorithm, value, source: checksumUrl};
|
|
}
|
|
|
|
public async setupJava(): Promise<JavaInstallerResults> {
|
|
if (this.verifySignature && !this.supportsSignatureVerification()) {
|
|
throw new Error(
|
|
`Input 'verify-signature' is not supported for distribution '${this.distribution}'.`
|
|
);
|
|
}
|
|
|
|
let foundJava = this.forceDownload ? null : this.findInToolcache();
|
|
if (
|
|
foundJava &&
|
|
!this.checkLatest &&
|
|
!this.latest &&
|
|
!this.requiresRemoteResolution()
|
|
) {
|
|
core.info(`Resolved Java ${foundJava.version} from tool-cache`);
|
|
} else {
|
|
core.info('Trying to resolve the latest version from remote');
|
|
try {
|
|
let javaRelease = await this.resolveJavaRelease();
|
|
core.info(`Resolved latest version as ${javaRelease.version}`);
|
|
if (javaRelease.floating) {
|
|
// A tool-cache entry has no source identity, and until the
|
|
// checksum-bound resolution cache maps the current artifact to a
|
|
// concrete version the release version is still just the requested
|
|
// major — so nothing already on the runner can be trusted. Once that
|
|
// mapping is known, an installation of exactly that version is the
|
|
// artifact we would otherwise download.
|
|
foundJava =
|
|
this.floatingVersionVerified && !this.forceDownload
|
|
? this.findConcreteVersionInToolcache(javaRelease.version)
|
|
: null;
|
|
}
|
|
if (!this.forceDownload && foundJava?.version === javaRelease.version) {
|
|
core.info(`Resolved Java ${foundJava.version} from tool-cache`);
|
|
} else {
|
|
let jdkCache =
|
|
this.cacheJdk &&
|
|
(!javaRelease.floating ||
|
|
(this.hasStableReleaseIdentity(javaRelease) &&
|
|
semver.valid(javaRelease.version)))
|
|
? await this.createJdkCache(javaRelease)
|
|
: undefined;
|
|
if (!this.forceDownload && jdkCache) {
|
|
const {restoreJdk} = await import('../jdk-cache.js');
|
|
const restored = await restoreJdk(jdkCache);
|
|
if (restored) {
|
|
const restoredPath = this.getRestoredJdkPath(javaRelease.version);
|
|
if (restoredPath) {
|
|
foundJava = {
|
|
version: javaRelease.version,
|
|
path: restoredPath
|
|
};
|
|
}
|
|
}
|
|
}
|
|
if (!foundJava || foundJava.version !== javaRelease.version) {
|
|
core.info('Trying to download...');
|
|
foundJava = await this.downloadTool(javaRelease);
|
|
core.info(`Java ${foundJava.version} was downloaded`);
|
|
if (javaRelease.floating) {
|
|
if (
|
|
!semver.valid(foundJava.version) ||
|
|
!isVersionSatisfies(this.version, foundJava.version)
|
|
) {
|
|
throw new Error(
|
|
`The downloaded ${this.distribution} artifact reported Java ${foundJava.version}, which does not satisfy '${this.version}'.`
|
|
);
|
|
}
|
|
javaRelease = {...javaRelease, version: foundJava.version};
|
|
await this.registerFloatingResolution(javaRelease);
|
|
jdkCache =
|
|
this.cacheJdk && this.hasStableReleaseIdentity(javaRelease)
|
|
? await this.createJdkCache(javaRelease)
|
|
: undefined;
|
|
}
|
|
if (jdkCache) {
|
|
// Register after the installation exists so its identity is
|
|
// captured; the post-job save refuses to upload a path whose
|
|
// installation was replaced afterwards.
|
|
const {registerJdk} = await import('../jdk-cache.js');
|
|
registerJdk(jdkCache);
|
|
}
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
this.logSetupError(error);
|
|
throw error;
|
|
}
|
|
}
|
|
if (!foundJava) {
|
|
throw new Error('Failed to resolve Java version');
|
|
}
|
|
// JDK folder may contain postfix "Contents/Home" on macOS
|
|
const macOSPostfixPath = path.join(
|
|
foundJava.path,
|
|
MACOS_JAVA_CONTENT_POSTFIX
|
|
);
|
|
if (process.platform === 'darwin' && fs.existsSync(macOSPostfixPath)) {
|
|
foundJava.path = macOSPostfixPath;
|
|
}
|
|
|
|
if (this.setDefault) {
|
|
core.info(`Setting Java ${foundJava.version} as the default`);
|
|
this.setJavaDefault(foundJava.version, foundJava.path);
|
|
} else {
|
|
core.info(
|
|
`Installing Java ${foundJava.version} (not setting as default)`
|
|
);
|
|
this.setJavaEnvironment(foundJava.version, foundJava.path);
|
|
}
|
|
|
|
return foundJava;
|
|
}
|
|
|
|
/**
|
|
* Resolves the release to install, preferring a cached resolution over the
|
|
* distribution's metadata API.
|
|
*
|
|
* Only Temurin is preinstalled on hosted runners, so for every other
|
|
* distribution the tool-cache lookup misses and the vendor API becomes a
|
|
* per-job dependency even when the JDK itself is already in the GitHub
|
|
* Actions cache. A cached resolution removes that dependency, and because it
|
|
* carries the download URL and checksum it also keeps a job working when the
|
|
* vendor API is unavailable but the JDK still has to be downloaded.
|
|
*/
|
|
private async resolveJavaRelease(): Promise<JavaDownloadRelease> {
|
|
if (
|
|
!this.cacheJdk ||
|
|
this.checkLatest ||
|
|
this.latest ||
|
|
this.forceDownload ||
|
|
this.requiresRemoteResolution()
|
|
) {
|
|
const release = await this.findPackageForDownload(this.version);
|
|
return this.restoreFloatingResolution(release);
|
|
}
|
|
|
|
const {restoreJdkResolution, registerJdkResolution} =
|
|
await import('../jdk-resolution-cache.js');
|
|
const request = {
|
|
distribution: this.distribution,
|
|
packageType: this.packageType,
|
|
architecture: this.architecture,
|
|
versionSpec: this.version,
|
|
stable: this.stable
|
|
};
|
|
|
|
const restored = await restoreJdkResolution(request);
|
|
if (restored?.fresh) {
|
|
core.info(
|
|
`Resolved ${this.distribution} ${restored.release.version} from the resolution cache`
|
|
);
|
|
return restored.release;
|
|
}
|
|
|
|
try {
|
|
const javaRelease = await this.findPackageForDownload(this.version);
|
|
if (!javaRelease.floating) {
|
|
registerJdkResolution(request, javaRelease);
|
|
}
|
|
return this.restoreFloatingResolution(javaRelease);
|
|
} catch (error) {
|
|
if (!restored) {
|
|
throw error;
|
|
}
|
|
// The cached resolution is older than the current bucket, but falling
|
|
// back to it is strictly better than failing the job because the vendor
|
|
// metadata API is down.
|
|
core.warning(
|
|
`Failed to resolve ${this.distribution} ${this.version} from remote (${
|
|
error instanceof Error ? error.message : String(error)
|
|
}); falling back to the cached resolution for ${restored.release.version}.`
|
|
);
|
|
return restored.release;
|
|
}
|
|
}
|
|
|
|
protected requiresRemoteResolution(): boolean {
|
|
return false;
|
|
}
|
|
|
|
private async createJdkCache(
|
|
javaRelease: JavaDownloadRelease
|
|
): Promise<JdkCache> {
|
|
const {getJdkVerificationIdentity} = await import('../jdk-cache.js');
|
|
return {
|
|
distribution: this.distribution,
|
|
packageType: this.packageType,
|
|
architecture: this.architecture,
|
|
version: javaRelease.version,
|
|
source: this.getJdkReleaseIdentity(javaRelease),
|
|
verification: getJdkVerificationIdentity(
|
|
this.verifySignature,
|
|
this.verifySignaturePublicKey
|
|
),
|
|
path: this.getJdkCachePath(javaRelease.version)
|
|
};
|
|
}
|
|
|
|
private async restoreFloatingResolution(
|
|
javaRelease: JavaDownloadRelease
|
|
): Promise<JavaDownloadRelease> {
|
|
if (
|
|
!javaRelease.floating ||
|
|
!this.hasStableReleaseIdentity(javaRelease) ||
|
|
!this.cacheJdk ||
|
|
this.forceDownload
|
|
) {
|
|
return javaRelease;
|
|
}
|
|
|
|
const {restoreJdkResolution} = await import('../jdk-resolution-cache.js');
|
|
const restored = await restoreJdkResolution(
|
|
this.getFloatingResolutionRequest(javaRelease)
|
|
);
|
|
if (!restored) {
|
|
return javaRelease;
|
|
}
|
|
if (
|
|
!semver.valid(restored.release.version) ||
|
|
!isVersionSatisfies(this.version, restored.release.version)
|
|
) {
|
|
core.debug(
|
|
`Ignoring the cached concrete version '${restored.release.version}' for ${this.distribution} ${this.version}.`
|
|
);
|
|
return javaRelease;
|
|
}
|
|
|
|
core.info(
|
|
`Resolved ${this.distribution} ${restored.release.version} for the current floating artifact`
|
|
);
|
|
this.floatingVersionVerified = true;
|
|
return {...javaRelease, version: restored.release.version};
|
|
}
|
|
|
|
private async registerFloatingResolution(
|
|
javaRelease: JavaDownloadRelease
|
|
): Promise<void> {
|
|
if (
|
|
!this.hasStableReleaseIdentity(javaRelease) ||
|
|
!this.cacheJdk ||
|
|
this.forceDownload
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const {registerJdkResolution} = await import('../jdk-resolution-cache.js');
|
|
registerJdkResolution(
|
|
this.getFloatingResolutionRequest(javaRelease),
|
|
javaRelease
|
|
);
|
|
}
|
|
|
|
private getFloatingResolutionRequest(javaRelease: JavaDownloadRelease) {
|
|
return {
|
|
distribution: this.distribution,
|
|
packageType: this.packageType,
|
|
architecture: this.architecture,
|
|
versionSpec: this.version,
|
|
stable: this.stable,
|
|
source: this.getJdkReleaseIdentity(javaRelease)
|
|
};
|
|
}
|
|
|
|
private logSetupError(error: any): void {
|
|
const httpStatusCode =
|
|
error instanceof tc.HTTPError
|
|
? error.httpStatusCode
|
|
: error instanceof httpm.HttpClientError
|
|
? error.statusCode
|
|
: undefined;
|
|
|
|
if (httpStatusCode) {
|
|
if (httpStatusCode === 403) {
|
|
core.error('HTTP 403: Permission denied or access restricted.');
|
|
} else if (httpStatusCode === 429) {
|
|
core.warning('HTTP 429: Rate limit exceeded. Please retry later.');
|
|
} else {
|
|
core.error(`HTTP ${httpStatusCode}: ${error.message}`);
|
|
}
|
|
} else if (error && error.errors && Array.isArray(error.errors)) {
|
|
core.error(`Java setup failed due to network or configuration error(s)`);
|
|
if (error instanceof Error && error.stack) {
|
|
core.debug(error.stack);
|
|
}
|
|
for (const err of error.errors) {
|
|
const endpoint = err?.address || err?.hostname || '';
|
|
const port = err?.port ? `:${err.port}` : '';
|
|
const message = err?.message || 'Aggregate error';
|
|
const endpointInfo = !message.includes(endpoint)
|
|
? ` ${endpoint}${port}`
|
|
: '';
|
|
const localInfo =
|
|
err.localAddress && err.localPort
|
|
? ` - Local (${err.localAddress}:${err.localPort})`
|
|
: '';
|
|
const logMessage = `${message}${endpointInfo}${localInfo}`;
|
|
core.error(logMessage);
|
|
core.debug(`${err.stack || err.message}`);
|
|
Object.entries(err).forEach(([key, value]) => {
|
|
core.debug(`"${key}": ${JSON.stringify(value)}`);
|
|
});
|
|
}
|
|
} else {
|
|
const message =
|
|
error instanceof Error ? error.message : JSON.stringify(error);
|
|
core.error(`Java setup process failed due to: ${message}`);
|
|
if (typeof error?.code === 'string') {
|
|
core.debug(error.stack);
|
|
}
|
|
const errorDetails = {
|
|
name: error.name,
|
|
message: error.message,
|
|
...Object.getOwnPropertyNames(error)
|
|
.filter(prop => !['name', 'message', 'stack'].includes(prop))
|
|
.reduce<{[key: string]: any}>((acc, prop) => {
|
|
acc[prop] = error[prop];
|
|
return acc;
|
|
}, {})
|
|
};
|
|
Object.entries(errorDetails).forEach(([key, value]) => {
|
|
core.debug(`"${key}": ${JSON.stringify(value)}`);
|
|
});
|
|
}
|
|
}
|
|
|
|
protected get toolcacheFolderName(): string {
|
|
return `Java_${this.distribution}_${this.packageType}`;
|
|
}
|
|
|
|
protected supportsSignatureVerification(): boolean {
|
|
return false;
|
|
}
|
|
|
|
protected getToolcacheVersionName(version: string): string {
|
|
if (!this.stable) {
|
|
if (version.includes('+')) {
|
|
return version.replace('+', '-ea.');
|
|
} else {
|
|
return `${version}-ea`;
|
|
}
|
|
}
|
|
|
|
// Kotlin and some Java dependencies don't work properly when Java path contains "+" sign
|
|
// so replace "/hostedtoolcache/Java/11.0.3+4/x64" to "/hostedtoolcache/Java/11.0.3-4/x64" when saves to cache
|
|
// related issue: https://github.com/actions/virtual-environments/issues/3014
|
|
return version.replace('+', '-');
|
|
}
|
|
|
|
protected getJdkCachePath(version: string): string {
|
|
const toolCache = process.env['RUNNER_TOOL_CACHE'];
|
|
if (!toolCache) {
|
|
return '';
|
|
}
|
|
return path.join(
|
|
toolCache,
|
|
this.toolcacheFolderName,
|
|
this.getToolcacheVersionName(version)
|
|
);
|
|
}
|
|
|
|
protected getRestoredJdkPath(version: string): string | null {
|
|
const basePath = this.getJdkCachePath(version);
|
|
if (!basePath) {
|
|
return null;
|
|
}
|
|
const architecturePath = path.join(basePath, this.architecture);
|
|
return fs.existsSync(architecturePath) &&
|
|
fs.existsSync(`${architecturePath}.complete`)
|
|
? architecturePath
|
|
: null;
|
|
}
|
|
|
|
/**
|
|
* Locates an installation of an exact version in the tool cache, unlike
|
|
* `findInToolcache()` which returns the newest entry satisfying the requested
|
|
* range. Used to reuse a JDK the runner already holds instead of downloading
|
|
* the identical artifact again.
|
|
*/
|
|
private findConcreteVersionInToolcache(
|
|
version: string
|
|
): JavaInstallerResults | null {
|
|
if (!semver.valid(version)) {
|
|
return null;
|
|
}
|
|
const installedPath = this.getRestoredJdkPath(version);
|
|
return installedPath ? {version, path: installedPath} : null;
|
|
}
|
|
|
|
private getJdkReleaseIdentity(javaRelease: JavaDownloadRelease): string {
|
|
if (javaRelease.checksum) {
|
|
return `${javaRelease.checksum.algorithm}:${javaRelease.checksum.value}`;
|
|
}
|
|
if (javaRelease.fingerprint) {
|
|
return javaRelease.fingerprint;
|
|
}
|
|
try {
|
|
const url = new URL(javaRelease.url);
|
|
return `${url.origin}${url.pathname}`;
|
|
} catch {
|
|
return javaRelease.url;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether the release identity pins the exact bytes behind `url`. A floating
|
|
* URL is a constant string, so it only becomes a safe cache identity once a
|
|
* checksum or a response validator distinguishes one published build from the
|
|
* next.
|
|
*/
|
|
private hasStableReleaseIdentity(javaRelease: JavaDownloadRelease): boolean {
|
|
return Boolean(javaRelease.checksum ?? javaRelease.fingerprint);
|
|
}
|
|
|
|
protected findInToolcache(): JavaInstallerResults | null {
|
|
// we can't use tc.find directly because firstly, we need to filter versions by stability flag
|
|
// if *-ea is provided, take only ea versions from toolcache, otherwise - only stable versions
|
|
const availableVersions = tc
|
|
.findAllVersions(this.toolcacheFolderName, this.architecture)
|
|
.map(item => {
|
|
return {
|
|
version: item
|
|
.replace('-ea.', '+')
|
|
.replace(/-ea$/, '')
|
|
// Kotlin and some Java dependencies don't work properly when Java path contains "+" sign
|
|
// so replace "/hostedtoolcache/Java/11.0.3-4/x64" to "/hostedtoolcache/Java/11.0.3+4/x64" when retrieves to cache
|
|
// related issue: https://github.com/actions/virtual-environments/issues/3014
|
|
.replace('-', '+'),
|
|
path:
|
|
getToolcachePath(
|
|
this.toolcacheFolderName,
|
|
item,
|
|
this.architecture
|
|
) || '',
|
|
stable: !item.includes('-ea')
|
|
};
|
|
})
|
|
.filter(item => item.stable === this.stable);
|
|
|
|
const satisfiedVersions = availableVersions
|
|
.filter(item => isVersionSatisfies(this.version, item.version))
|
|
.filter(item => item.path)
|
|
.sort((a, b) => {
|
|
return -semver.compareBuild(a.version, b.version);
|
|
});
|
|
if (!satisfiedVersions || satisfiedVersions.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
version: satisfiedVersions[0].version,
|
|
path: satisfiedVersions[0].path
|
|
};
|
|
}
|
|
|
|
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.
|
|
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;
|
|
} else if (version.includes('-ea.')) {
|
|
// transform '11.0.3-ea.2' -> '11.0.3+2'
|
|
version = version.replace('-ea.', '+');
|
|
stable = false;
|
|
}
|
|
|
|
// Java uses a versioning scheme (JEP 322) that can contain more numeric
|
|
// fields than SemVer allows, e.g. '18.0.1.1' or '11.0.9.1'. Convert such
|
|
// exact versions to SemVer build notation ('18.0.1+1') so they are
|
|
// accepted. Ranges and versions that already carry build metadata are
|
|
// left untouched.
|
|
if (/^\d+(\.\d+){3,}$/.test(version)) {
|
|
version = convertVersionToSemver(version);
|
|
}
|
|
|
|
if (!semver.validRange(version)) {
|
|
throw new Error(
|
|
`The string '${version}' is not valid SemVer notation for a Java version. Please check README file for code snippets and more detailed information`
|
|
);
|
|
}
|
|
|
|
return {
|
|
version,
|
|
stable,
|
|
latest
|
|
};
|
|
}
|
|
|
|
protected createVersionNotFoundError(
|
|
versionOrRange: string,
|
|
availableVersions?: string[],
|
|
additionalContext?: string
|
|
): Error {
|
|
const parts = [
|
|
`No matching version found for SemVer '${versionOrRange}'.`,
|
|
`Distribution: ${this.distribution}`,
|
|
`Package type: ${this.packageType}`,
|
|
`Architecture: ${this.architecture}`
|
|
];
|
|
|
|
// Add additional context if provided (e.g., platform/OS info)
|
|
if (additionalContext) {
|
|
parts.push(additionalContext);
|
|
}
|
|
|
|
if (availableVersions && availableVersions.length > 0) {
|
|
const maxVersionsToShow = core.isDebug() ? availableVersions.length : 50;
|
|
const versionsToShow = availableVersions.slice(0, maxVersionsToShow);
|
|
const truncated = availableVersions.length > maxVersionsToShow;
|
|
|
|
parts.push(
|
|
`Available versions: ${versionsToShow.join(', ')}${truncated ? ', ...' : ''}`
|
|
);
|
|
|
|
if (truncated) {
|
|
parts.push(
|
|
`(showing first ${maxVersionsToShow} of ${availableVersions.length} versions, enable debug mode to see all)`
|
|
);
|
|
}
|
|
}
|
|
|
|
const error = new Error(parts.join('\n'));
|
|
error.name = 'VersionNotFoundError';
|
|
return error;
|
|
}
|
|
|
|
protected setJavaDefault(version: string, toolPath: string) {
|
|
core.exportVariable('JAVA_HOME', toolPath);
|
|
core.addPath(path.join(toolPath, 'bin'));
|
|
this.setJavaEnvironment(version, toolPath);
|
|
}
|
|
|
|
protected setJavaEnvironment(version: string, toolPath: string) {
|
|
const majorVersion = version.split('.')[0];
|
|
core.setOutput('distribution', this.distribution);
|
|
core.setOutput('path', toolPath);
|
|
core.setOutput('version', version);
|
|
core.exportVariable(
|
|
`JAVA_HOME_${majorVersion}_${this.architecture.toUpperCase()}`,
|
|
toolPath
|
|
);
|
|
}
|
|
|
|
protected distributionArchitecture(): string {
|
|
return this.architecture;
|
|
}
|
|
}
|