mirror of
https://github.com/actions/setup-java.git
synced 2026-07-31 14:41:36 +08:00
* Optimize Temurin tool-cache fast path - Lazy-load distribution installers so only the selected distro module is initialized - Defer cache feature/cache module loading until cache input is provided - Start cache restore early and await it safely alongside Java setup flow - Update orchestration and lazy-loading tests; regenerate dist artifacts Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: cc2c0256-c55e-4a35-a584-5bed7e8b11ad * Address PR review comments - Lazy-load cache save in cleanup path so no-cache runs avoid cache module init in post action - Stage dist/setup/package.json in release script for chunked setup bundle completeness Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: cc2c0256-c55e-4a35-a584-5bed7e8b11ad * Fix CodeQL comment tag filter finding Patch is-unsafe's XML comment-close detector during builds so generated bundles recognize both HTML comment end forms and satisfy CodeQL until the dependency publishes a fix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 277302b1-aa95-4012-817b-9752cdaee14e * Rebuild generated dist bundles Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: cc2c0256-c55e-4a35-a584-5bed7e8b11ad --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: cc2c0256-c55e-4a35-a584-5bed7e8b11ad Copilot-Session: 277302b1-aa95-4012-817b-9752cdaee14e
410 lines
12 KiB
TypeScript
410 lines
12 KiB
TypeScript
import os from 'os';
|
|
import path from 'path';
|
|
import * as fs from 'fs';
|
|
import * as semver from 'semver';
|
|
import * as core from '@actions/core';
|
|
|
|
import * as tc from '@actions/tool-cache';
|
|
import * as httpm from '@actions/http-client';
|
|
import {
|
|
INPUT_JOB_STATUS,
|
|
DISTRIBUTIONS_ONLY_MAJOR_VERSION
|
|
} from './constants.js';
|
|
import {OutgoingHttpHeaders} from 'http';
|
|
|
|
export function getTempDir() {
|
|
const tempDirectory = process.env['RUNNER_TEMP'] || os.tmpdir();
|
|
|
|
return tempDirectory;
|
|
}
|
|
|
|
export function getBooleanInput(inputName: string, defaultValue = false) {
|
|
const inputValue = core.getInput(inputName);
|
|
const normalizedValue = inputValue.trim().toLowerCase();
|
|
|
|
if (!normalizedValue) {
|
|
return defaultValue;
|
|
}
|
|
if (normalizedValue === 'true') {
|
|
return true;
|
|
}
|
|
if (normalizedValue === 'false') {
|
|
return false;
|
|
}
|
|
|
|
throw new Error(
|
|
`Invalid value '${inputValue}' for boolean input '${inputName}'. Expected 'true' or 'false'.`
|
|
);
|
|
}
|
|
|
|
export function getVersionFromToolcachePath(toolPath: string) {
|
|
if (toolPath) {
|
|
return path.basename(path.dirname(toolPath));
|
|
}
|
|
|
|
return toolPath;
|
|
}
|
|
|
|
export async function extractJdkFile(toolPath: string, extension?: string) {
|
|
if (!extension) {
|
|
extension = toolPath.endsWith('.tar.gz')
|
|
? 'tar.gz'
|
|
: path.extname(toolPath);
|
|
if (extension.startsWith('.')) {
|
|
extension = extension.substring(1);
|
|
}
|
|
}
|
|
|
|
switch (extension) {
|
|
case 'tar.gz':
|
|
case 'tar':
|
|
return await tc.extractTar(toolPath);
|
|
case 'zip':
|
|
return await tc.extractZip(toolPath);
|
|
default:
|
|
return await tc.extract7z(toolPath);
|
|
}
|
|
}
|
|
|
|
export function getDownloadArchiveExtension() {
|
|
return process.platform === 'win32' ? 'zip' : 'tar.gz';
|
|
}
|
|
|
|
export function isVersionSatisfies(range: string, version: string): boolean {
|
|
// Some distributions (e.g. JetBrains Runtime) publish 4-segment versions
|
|
// like '17.0.8.1+1080.1' that semver rejects. If the candidate version
|
|
// isn't valid semver, it can't match — bail out rather than letting
|
|
// compareBuild / satisfies throw.
|
|
if (!semver.valid(version)) {
|
|
return false;
|
|
}
|
|
|
|
if (semver.valid(range)) {
|
|
// if full version with build digit is provided as a range (such as '1.2.3+4')
|
|
// we should check for exact equal via compareBuild
|
|
// since semver.satisfies doesn't handle 4th digit
|
|
const semRange = semver.parse(range);
|
|
if (semRange && semRange.build?.length > 0) {
|
|
return semver.compareBuild(range, version) === 0;
|
|
}
|
|
}
|
|
|
|
return semver.satisfies(version, range);
|
|
}
|
|
|
|
export function getToolcachePath(
|
|
toolName: string,
|
|
version: string,
|
|
architecture: string
|
|
) {
|
|
const toolcacheRoot = process.env['RUNNER_TOOL_CACHE'] ?? '';
|
|
const fullPath = path.join(toolcacheRoot, toolName, version, architecture);
|
|
if (fs.existsSync(fullPath)) {
|
|
return fullPath;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function isJobStatusSuccess() {
|
|
const jobStatus = core.getInput(INPUT_JOB_STATUS);
|
|
|
|
return jobStatus === 'success';
|
|
}
|
|
|
|
export function isGhes(): boolean {
|
|
const ghUrl = new URL(
|
|
process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
|
);
|
|
|
|
const hostname = ghUrl.hostname.trimEnd().toUpperCase();
|
|
const isGitHubHost = hostname === 'GITHUB.COM';
|
|
const isGitHubEnterpriseCloudHost = hostname.endsWith('.GHE.COM');
|
|
const isLocalHost = hostname.endsWith('.LOCALHOST');
|
|
|
|
return !isGitHubHost && !isGitHubEnterpriseCloudHost && !isLocalHost;
|
|
}
|
|
|
|
export interface VersionInfo {
|
|
version: string;
|
|
distribution?: string;
|
|
}
|
|
|
|
export function getVersionFromFileContent(
|
|
content: string,
|
|
distributionName: string,
|
|
versionFile: string
|
|
): VersionInfo | null {
|
|
let javaVersionRegExp: RegExp;
|
|
let extractedDistribution: string | undefined;
|
|
|
|
function getFileName(versionFile: string) {
|
|
return path.basename(versionFile);
|
|
}
|
|
|
|
const versionFileName = getFileName(versionFile);
|
|
if (versionFileName == '.tool-versions') {
|
|
// Capture an optional asdf-java vendor prefix (e.g. `temurin-`, `corretto-`)
|
|
// in the `distribution` group so it can be mapped to a setup-java distribution.
|
|
javaVersionRegExp =
|
|
/^java\s+(?:(?<distribution>\S*)-)?(?<version>\d+(?:\.\d+)*([+_.-](?:openj9[-._]?\d[\w.-]*|java\d+|jre[-_\w]*|OpenJDK\d+[\w_.-]*|[a-z0-9]+))*)/im;
|
|
} else if (versionFileName == '.sdkmanrc') {
|
|
// Match both version and optional distribution identifier
|
|
javaVersionRegExp =
|
|
/^java\s*=\s*(?<version>[^-\s]+)(?:-(?<distribution>[a-z0-9]+))?/m;
|
|
} else {
|
|
javaVersionRegExp = /(?<version>(?<=(^|\s|-))(\d+\S*))(\s|$)/;
|
|
}
|
|
|
|
const match = content.match(javaVersionRegExp);
|
|
const capturedVersion = match?.groups?.version
|
|
? (match.groups.version as string)
|
|
: '';
|
|
|
|
// Extract distribution from .sdkmanrc file
|
|
if (versionFileName == '.sdkmanrc' && match?.groups?.distribution) {
|
|
const sdkmanDist = match.groups.distribution;
|
|
extractedDistribution = mapSdkmanDistribution(sdkmanDist);
|
|
core.debug(
|
|
`Parsed distribution '${extractedDistribution}' from SDKMAN identifier '${sdkmanDist}'`
|
|
);
|
|
}
|
|
|
|
// Extract distribution from asdf .tool-versions file
|
|
if (versionFileName == '.tool-versions' && match?.groups?.distribution) {
|
|
const asdfDist = match.groups.distribution;
|
|
extractedDistribution = mapAsdfDistribution(asdfDist);
|
|
if (extractedDistribution) {
|
|
core.debug(
|
|
`Parsed distribution '${extractedDistribution}' from asdf identifier '${asdfDist}'`
|
|
);
|
|
}
|
|
}
|
|
|
|
core.debug(
|
|
`Parsed version '${capturedVersion}' from file '${versionFileName}'`
|
|
);
|
|
if (!capturedVersion) {
|
|
return null;
|
|
}
|
|
|
|
const tentativeVersion = avoidOldNotation(capturedVersion);
|
|
const rawVersion = tentativeVersion.split('-')[0];
|
|
|
|
let version = semver.validRange(rawVersion)
|
|
? tentativeVersion
|
|
: semver.coerce(tentativeVersion);
|
|
|
|
core.debug(`Range version from file is '${version}'`);
|
|
|
|
if (!version) {
|
|
return null;
|
|
}
|
|
|
|
// Apply DISTRIBUTIONS_ONLY_MAJOR_VERSION logic whenever the effective distribution
|
|
// (either explicitly provided or extracted from the version file) is in the list.
|
|
if (
|
|
DISTRIBUTIONS_ONLY_MAJOR_VERSION.includes(
|
|
extractedDistribution || distributionName
|
|
)
|
|
) {
|
|
const coerceVersion = semver.coerce(version) ?? version;
|
|
version = semver.major(coerceVersion).toString();
|
|
}
|
|
|
|
return {
|
|
version: version.toString(),
|
|
distribution: extractedDistribution
|
|
};
|
|
}
|
|
|
|
// Map SDKMAN distribution identifiers to setup-java distribution names
|
|
function mapSdkmanDistribution(sdkmanDist: string): string | undefined {
|
|
const distributionMap: Record<string, string> = {
|
|
tem: 'temurin',
|
|
sem: 'semeru',
|
|
albba: 'dragonwell',
|
|
zulu: 'zulu',
|
|
amzn: 'corretto',
|
|
graal: 'graalvm',
|
|
graalce: 'graalvm',
|
|
librca: 'liberica',
|
|
ms: 'microsoft',
|
|
oracle: 'oracle',
|
|
sapmchn: 'sapmachine',
|
|
jbr: 'jetbrains',
|
|
dragonwell: 'dragonwell',
|
|
kona: 'kona'
|
|
};
|
|
|
|
const mapped = distributionMap[sdkmanDist.toLowerCase()];
|
|
if (!mapped) {
|
|
core.warning(
|
|
`Unknown SDKMAN distribution identifier '${sdkmanDist}'. Please specify the distribution explicitly.`
|
|
);
|
|
}
|
|
return mapped;
|
|
}
|
|
|
|
// Map asdf-java (.tool-versions) vendor identifiers to setup-java distribution names.
|
|
// asdf-java encodes the vendor as a prefix on the version string, e.g.
|
|
// `java temurin-17.0.3+7` or `java semeru-openj9-11.0.25+9`. Packaging variants
|
|
// (`-jre`, `-musl`, `-openj9`, `-crac`, `-javafx`, ...) are collapsed onto the
|
|
// base vendor since setup-java does not distinguish them here.
|
|
function mapAsdfDistribution(asdfDist: string): string | undefined {
|
|
const normalized = asdfDist.toLowerCase();
|
|
|
|
// Multi-segment vendors that map to a distinct setup-java distribution.
|
|
if (normalized.startsWith('graalvm-community')) {
|
|
return 'graalvm-community';
|
|
}
|
|
if (normalized.startsWith('oracle-graalvm')) {
|
|
return 'graalvm';
|
|
}
|
|
|
|
const baseVendor = normalized.split('-')[0];
|
|
const distributionMap: Record<string, string> = {
|
|
temurin: 'temurin',
|
|
adoptopenjdk: 'temurin',
|
|
zulu: 'zulu',
|
|
corretto: 'corretto',
|
|
liberica: 'liberica',
|
|
microsoft: 'microsoft',
|
|
semeru: 'semeru',
|
|
ibm: 'semeru',
|
|
dragonwell: 'dragonwell',
|
|
graalvm: 'graalvm',
|
|
oracle: 'oracle',
|
|
sapmachine: 'sapmachine',
|
|
kona: 'kona',
|
|
jetbrains: 'jetbrains'
|
|
};
|
|
|
|
const mapped = distributionMap[baseVendor];
|
|
if (!mapped) {
|
|
core.warning(
|
|
`Unknown asdf distribution identifier '${asdfDist}'. Please specify the distribution explicitly.`
|
|
);
|
|
}
|
|
return mapped;
|
|
}
|
|
|
|
// By convention, action expects version 8 in the format `8.*` instead of `1.8`
|
|
function avoidOldNotation(content: string): string {
|
|
return content.startsWith('1.') ? content.substring(2) : content;
|
|
}
|
|
|
|
export function convertVersionToSemver(version: number[] | string) {
|
|
// Some distributions may use semver-like notation (12.10.2.1, 12.10.2.1.1)
|
|
const versionArray = Array.isArray(version) ? version : version.split('.');
|
|
const mainVersion = versionArray.slice(0, 3).join('.');
|
|
if (versionArray.length > 3) {
|
|
return `${mainVersion}+${versionArray.slice(3).join('.')}`;
|
|
}
|
|
return mainVersion;
|
|
}
|
|
|
|
export function getGitHubHttpHeaders(): OutgoingHttpHeaders {
|
|
const resolvedToken = core.getInput('token') || process.env.GITHUB_TOKEN;
|
|
const auth = !resolvedToken ? undefined : `token ${resolvedToken}`;
|
|
|
|
const headers: OutgoingHttpHeaders = {
|
|
accept: 'application/vnd.github.VERSION.raw'
|
|
};
|
|
|
|
if (auth) {
|
|
headers.authorization = auth;
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
export const MAX_PAGINATION_PAGES = 1000;
|
|
|
|
export function getNextPageUrlFromLinkHeader(
|
|
headers?: Record<string, string | string[] | undefined>
|
|
): string | null {
|
|
if (!headers) {
|
|
return null;
|
|
}
|
|
|
|
const linkHeader = headers.link ?? headers.Link;
|
|
if (!linkHeader) {
|
|
return null;
|
|
}
|
|
|
|
const normalizedLinkHeader = Array.isArray(linkHeader)
|
|
? linkHeader.join(',')
|
|
: linkHeader;
|
|
|
|
// Split into individual link-values and find the one with rel="next"
|
|
// RFC 8288 allows rel to appear anywhere among the parameters
|
|
const linkValues = normalizedLinkHeader.split(/,(?=\s*<)/);
|
|
for (const linkValue of linkValues) {
|
|
const urlMatch = linkValue.match(/<([^>]+)>/);
|
|
if (!urlMatch) continue;
|
|
|
|
const params = linkValue.slice(urlMatch[0].length);
|
|
// Use word boundary to match "next" as a standalone relation type
|
|
// RFC 8288 allows space-separated relation types like rel="next prev"
|
|
if (/;\s*rel="?[^"]*\bnext\b/i.test(params)) {
|
|
return urlMatch[1];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function validatePaginationUrl(
|
|
url: string,
|
|
allowedOrigin: string
|
|
): boolean {
|
|
try {
|
|
const parsed = new URL(url);
|
|
const allowed = new URL(allowedOrigin);
|
|
return parsed.origin === allowed.origin;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Rename archive to add extension because after downloading
|
|
// archive does not contain extension type and it leads to some issues
|
|
// on Windows runners without PowerShell Core.
|
|
//
|
|
// For default PowerShell Windows it should contain extension type to unpack it.
|
|
export function renameWinArchive(javaArchivePath: string): string {
|
|
const javaArchivePathRenamed = `${javaArchivePath}.zip`;
|
|
fs.renameSync(javaArchivePath, 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);
|
|
}
|