mirror of
https://gitea.com/actions/setup-java.git
synced 2026-07-11 19:41:57 +08:00
* 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> * Clarify latest note for oracle/graalvm version resolution vs download Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * graalvm-community: float latest to its own newest GA release GraalVM Community publishes its releases on GitHub, so the `latest` alias now matches against that real release list using the SemVer wildcard instead of asking the Adoptium API for the newest GA major. This prevents `latest` from hard-failing when GraalVM lags behind a freshly released Java major (e.g. Adoptium reports 26 before GraalVM ships it). Oracle GraalVM has no listing endpoint, so it keeps deriving the newest major from Adoptium and errors clearly if that major is not yet published. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Give latest+qualifier inputs (e.g. latest-ea) a targeted error Inputs like 'latest-ea' had their '-ea' suffix stripped and fell through to the generic SemVer validation, failing with a confusing "'latest' is not valid SemVer" message even though 'latest' is supported. Add an explicit guard so any 'latest*' value other than exactly 'latest' throws a targeted error explaining that 'latest' resolves GA releases only and cannot be combined with '-ea' or other qualifiers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
487 lines
14 KiB
TypeScript
487 lines
14 KiB
TypeScript
import {
|
|
jest,
|
|
describe,
|
|
it,
|
|
expect,
|
|
beforeEach,
|
|
afterEach,
|
|
beforeAll,
|
|
afterAll
|
|
} from '@jest/globals';
|
|
import type {JavaInstallerOptions} from '../../src/distributions/base-models.js';
|
|
import type {TemurinImplementation as TemurinImplementationType} from '../../src/distributions/temurin/installer.js';
|
|
import {HttpClient} from '@actions/http-client';
|
|
import fs from 'fs';
|
|
import os from 'os';
|
|
|
|
import manifestData from '../data/temurin.json' with {type: 'json'};
|
|
|
|
// Mock @actions/core before importing source modules that depend on it
|
|
jest.unstable_mockModule('@actions/core', () => ({
|
|
info: jest.fn(),
|
|
warning: jest.fn(),
|
|
debug: jest.fn(),
|
|
error: jest.fn(),
|
|
notice: jest.fn(),
|
|
setFailed: jest.fn(),
|
|
setOutput: jest.fn(),
|
|
getInput: jest.fn(),
|
|
getBooleanInput: jest.fn(),
|
|
getMultilineInput: jest.fn(),
|
|
addPath: jest.fn(),
|
|
exportVariable: jest.fn(),
|
|
saveState: jest.fn(),
|
|
getState: jest.fn(),
|
|
setSecret: jest.fn(),
|
|
isDebug: jest.fn(() => false),
|
|
startGroup: jest.fn(),
|
|
endGroup: jest.fn(),
|
|
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
|
|
toPlatformPath: jest.fn((p: string) => p),
|
|
toWin32Path: jest.fn((p: string) => p),
|
|
toPosixPath: jest.fn((p: string) => p)
|
|
}));
|
|
|
|
jest.unstable_mockModule('@actions/tool-cache', () => ({
|
|
find: jest.fn(),
|
|
findAllVersions: jest.fn(),
|
|
downloadTool: jest.fn(),
|
|
extractZip: jest.fn(),
|
|
extractTar: jest.fn(),
|
|
extract7z: jest.fn(),
|
|
extractXar: jest.fn(),
|
|
cacheDir: jest.fn(),
|
|
cacheFile: jest.fn(),
|
|
getManifestFromRepo: jest.fn(),
|
|
findFromManifest: jest.fn(),
|
|
evaluateVersions: jest.fn()
|
|
}));
|
|
|
|
const real_util_module = await import('../../src/util.js');
|
|
jest.unstable_mockModule('../../src/util.js', () => ({
|
|
...real_util_module,
|
|
extractJdkFile: jest.fn(),
|
|
getDownloadArchiveExtension: jest.fn(),
|
|
getToolcachePath: jest.fn(),
|
|
isJobStatusSuccess: jest.fn(),
|
|
renameWinArchive: jest.fn(),
|
|
isVersionSatisfies: real_util_module.isVersionSatisfies,
|
|
getTempDir: real_util_module.getTempDir
|
|
}));
|
|
|
|
jest.unstable_mockModule('../../src/gpg.js', () => ({
|
|
importKey: jest.fn(),
|
|
deleteKey: jest.fn(),
|
|
verifyPackageSignature: jest.fn()
|
|
}));
|
|
|
|
// Dynamic imports after mocking
|
|
const core = await import('@actions/core');
|
|
const gpg = await import('../../src/gpg.js');
|
|
const tc = await import('@actions/tool-cache');
|
|
const {TemurinDistribution, TemurinImplementation, ADOPTIUM_PUBLIC_KEY} =
|
|
await import('../../src/distributions/temurin/installer.js');
|
|
const util = await import('../../src/util.js');
|
|
|
|
describe('getAvailableVersions', () => {
|
|
let spyHttpClient: any;
|
|
let spyCoreError: any;
|
|
let spyCoreWarning: any;
|
|
|
|
beforeEach(() => {
|
|
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
|
|
spyHttpClient.mockReturnValue({
|
|
statusCode: 200,
|
|
headers: {},
|
|
result: []
|
|
});
|
|
// Mock core.error to suppress error logs
|
|
spyCoreError = core.error as jest.Mock;
|
|
spyCoreError.mockImplementation(() => {});
|
|
spyCoreWarning = core.warning as jest.Mock;
|
|
spyCoreWarning.mockImplementation(() => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.resetAllMocks();
|
|
jest.clearAllMocks();
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
{
|
|
version: '16',
|
|
architecture: 'x64',
|
|
packageType: 'jdk',
|
|
checkLatest: false
|
|
},
|
|
TemurinImplementation.Hotspot,
|
|
'os=mac&architecture=x64&image_type=jdk&release_type=ga&jvm_impl=hotspot&page_size=20&page=0'
|
|
],
|
|
[
|
|
{
|
|
version: '16',
|
|
architecture: 'x86',
|
|
packageType: 'jdk',
|
|
checkLatest: false
|
|
},
|
|
TemurinImplementation.Hotspot,
|
|
'os=mac&architecture=x86&image_type=jdk&release_type=ga&jvm_impl=hotspot&page_size=20&page=0'
|
|
],
|
|
[
|
|
{
|
|
version: '16',
|
|
architecture: 'x64',
|
|
packageType: 'jre',
|
|
checkLatest: false
|
|
},
|
|
TemurinImplementation.Hotspot,
|
|
'os=mac&architecture=x64&image_type=jre&release_type=ga&jvm_impl=hotspot&page_size=20&page=0'
|
|
],
|
|
[
|
|
{
|
|
version: '16-ea',
|
|
architecture: 'x64',
|
|
packageType: 'jdk',
|
|
checkLatest: false
|
|
},
|
|
TemurinImplementation.Hotspot,
|
|
'os=mac&architecture=x64&image_type=jdk&release_type=ea&jvm_impl=hotspot&page_size=20&page=0'
|
|
]
|
|
])(
|
|
'build correct url for %s',
|
|
async (
|
|
installerOptions: JavaInstallerOptions,
|
|
impl: TemurinImplementationType,
|
|
expectedParameters
|
|
) => {
|
|
const distribution = new TemurinDistribution(installerOptions, impl);
|
|
const baseUrl =
|
|
'https://api.adoptium.net/v3/assets/version/%5B1.0,100.0%5D';
|
|
const expectedUrl = `${baseUrl}?project=jdk&vendor=adoptium&heap_size=normal&sort_method=DEFAULT&sort_order=DESC&${expectedParameters}`;
|
|
distribution['getPlatformOption'] = () => 'mac';
|
|
|
|
await distribution['getAvailableVersions']();
|
|
|
|
expect(spyHttpClient.mock.calls).toHaveLength(1);
|
|
expect(spyHttpClient.mock.calls[0][0]).toBe(expectedUrl);
|
|
}
|
|
);
|
|
|
|
it('load available versions', async () => {
|
|
const nextPageUrl =
|
|
'https://api.adoptium.net/v3/assets/version/%5B1.0,100.0%5D?page=1&page_size=20';
|
|
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
|
|
spyHttpClient
|
|
.mockReturnValueOnce({
|
|
statusCode: 200,
|
|
headers: {link: `<${nextPageUrl}>; rel="next"`},
|
|
result: manifestData as any
|
|
})
|
|
.mockReturnValueOnce({
|
|
statusCode: 200,
|
|
headers: {},
|
|
result: manifestData as any
|
|
});
|
|
|
|
const distribution = new TemurinDistribution(
|
|
{
|
|
version: '8',
|
|
architecture: 'x64',
|
|
packageType: 'jdk',
|
|
checkLatest: false
|
|
},
|
|
TemurinImplementation.Hotspot
|
|
);
|
|
const availableVersions = await distribution['getAvailableVersions']();
|
|
expect(availableVersions).not.toBeNull();
|
|
expect(availableVersions.length).toBe(manifestData.length * 2);
|
|
expect(spyHttpClient).toHaveBeenNthCalledWith(2, nextPageUrl);
|
|
});
|
|
|
|
it('stops pagination after 1000 pages as a safeguard', async () => {
|
|
const nextPageUrl =
|
|
'https://api.adoptium.net/v3/assets/version/%5B1.0,100.0%5D?page=2&page_size=20';
|
|
spyHttpClient.mockReturnValue({
|
|
statusCode: 200,
|
|
headers: {link: `<${nextPageUrl}>; rel="next"`},
|
|
result: [{version_data: {semver: '17.0.1'}, binaries: []}] as any
|
|
});
|
|
|
|
const distribution = new TemurinDistribution(
|
|
{
|
|
version: '8',
|
|
architecture: 'x64',
|
|
packageType: 'jdk',
|
|
checkLatest: false
|
|
},
|
|
TemurinImplementation.Hotspot
|
|
);
|
|
|
|
await distribution['getAvailableVersions']();
|
|
|
|
expect(spyHttpClient).toHaveBeenCalledTimes(1000);
|
|
expect(spyCoreWarning).toHaveBeenCalledWith(
|
|
expect.stringContaining('Reached pagination safeguard limit (1000 pages)')
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
[TemurinImplementation.Hotspot, 'jdk', 'Java_Temurin-Hotspot_jdk'],
|
|
[TemurinImplementation.Hotspot, 'jre', 'Java_Temurin-Hotspot_jre']
|
|
])(
|
|
'find right toolchain folder',
|
|
(
|
|
impl: TemurinImplementationType,
|
|
packageType: string,
|
|
expected: string
|
|
) => {
|
|
const distribution = new TemurinDistribution(
|
|
{
|
|
version: '8',
|
|
architecture: 'x64',
|
|
packageType: packageType,
|
|
checkLatest: false
|
|
},
|
|
impl
|
|
);
|
|
|
|
// @ts-ignore - because it is protected
|
|
expect(distribution.toolcacheFolderName).toBe(expected);
|
|
}
|
|
);
|
|
|
|
it.each([
|
|
['amd64', 'x64'],
|
|
['arm64', 'aarch64']
|
|
])(
|
|
'defaults to os.arch(): %s mapped to distro arch: %s',
|
|
async (osArch: string, distroArch: string) => {
|
|
jest
|
|
.spyOn(os, 'arch')
|
|
.mockReturnValue(osArch as ReturnType<typeof os.arch>);
|
|
|
|
const installerOptions: JavaInstallerOptions = {
|
|
version: '17',
|
|
architecture: '',
|
|
packageType: 'jdk',
|
|
checkLatest: false
|
|
};
|
|
|
|
const expectedParameters = `os=mac&architecture=${distroArch}&image_type=jdk&release_type=ga&jvm_impl=hotspot&page_size=20&page=0`;
|
|
|
|
const distribution = new TemurinDistribution(
|
|
installerOptions,
|
|
TemurinImplementation.Hotspot
|
|
);
|
|
const baseUrl =
|
|
'https://api.adoptium.net/v3/assets/version/%5B1.0,100.0%5D';
|
|
const expectedUrl = `${baseUrl}?project=jdk&vendor=adoptium&heap_size=normal&sort_method=DEFAULT&sort_order=DESC&${expectedParameters}`;
|
|
distribution['getPlatformOption'] = () => 'mac';
|
|
|
|
await distribution['getAvailableVersions']();
|
|
|
|
expect(spyHttpClient.mock.calls).toHaveLength(1);
|
|
expect(spyHttpClient.mock.calls[0][0]).toBe(expectedUrl);
|
|
}
|
|
);
|
|
});
|
|
|
|
describe('findPackageForDownload', () => {
|
|
it.each([
|
|
['8', '8.0.302+8'],
|
|
['16', '16.0.2+7'],
|
|
['16.0', '16.0.2+7'],
|
|
['16.0.2', '16.0.2+7'],
|
|
['8.x', '8.0.302+8'],
|
|
['x', '16.0.2+7']
|
|
])('version is resolved correctly %s -> %s', async (input, expected) => {
|
|
const distribution = new TemurinDistribution(
|
|
{
|
|
version: '8',
|
|
architecture: 'x64',
|
|
packageType: 'jdk',
|
|
checkLatest: false
|
|
},
|
|
TemurinImplementation.Hotspot
|
|
);
|
|
distribution['getAvailableVersions'] = async () => manifestData as any;
|
|
const resolvedVersion = await distribution['findPackageForDownload'](input);
|
|
expect(resolvedVersion.version).toBe(expected);
|
|
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(
|
|
{
|
|
version: '9.0.8',
|
|
architecture: 'x64',
|
|
packageType: 'jdk',
|
|
checkLatest: false
|
|
},
|
|
TemurinImplementation.Hotspot
|
|
);
|
|
distribution['getAvailableVersions'] = async () => manifestData as any;
|
|
await expect(
|
|
distribution['findPackageForDownload']('9.0.8')
|
|
).rejects.toThrow(/No matching version found for SemVer */);
|
|
});
|
|
|
|
it('version is not found', async () => {
|
|
const distribution = new TemurinDistribution(
|
|
{
|
|
version: '7.x',
|
|
architecture: 'x64',
|
|
packageType: 'jdk',
|
|
checkLatest: false
|
|
},
|
|
TemurinImplementation.Hotspot
|
|
);
|
|
distribution['getAvailableVersions'] = async () => manifestData as any;
|
|
await expect(distribution['findPackageForDownload']('7.x')).rejects.toThrow(
|
|
/No matching version found for SemVer */
|
|
);
|
|
});
|
|
|
|
it('version list is empty', async () => {
|
|
const distribution = new TemurinDistribution(
|
|
{
|
|
version: '8',
|
|
architecture: 'x64',
|
|
packageType: 'jdk',
|
|
checkLatest: false
|
|
},
|
|
TemurinImplementation.Hotspot
|
|
);
|
|
distribution['getAvailableVersions'] = async () => [];
|
|
await expect(distribution['findPackageForDownload']('8')).rejects.toThrow(
|
|
/No matching version found for SemVer */
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('downloadTool', () => {
|
|
let spyDownloadTool: any;
|
|
let spyVerifySignature: any;
|
|
let spyExtractJdkFile: any;
|
|
let spyCacheDir: any;
|
|
let spyReadDirSync: any;
|
|
let spyRenameWinArchive: any;
|
|
|
|
beforeEach(() => {
|
|
spyDownloadTool = tc.downloadTool as jest.Mock;
|
|
spyDownloadTool.mockResolvedValue('/tmp/jdk.tar.gz');
|
|
spyVerifySignature = gpg.verifyPackageSignature as jest.Mock;
|
|
spyVerifySignature.mockResolvedValue(undefined);
|
|
spyExtractJdkFile = util.extractJdkFile as jest.Mock;
|
|
spyExtractJdkFile.mockResolvedValue('/tmp/extracted');
|
|
spyCacheDir = tc.cacheDir as jest.Mock;
|
|
spyCacheDir.mockResolvedValue('/tmp/toolcache');
|
|
spyReadDirSync = jest.spyOn(fs, 'readdirSync');
|
|
spyReadDirSync.mockReturnValue(['jdk-17'] as any);
|
|
spyRenameWinArchive = util.renameWinArchive as jest.Mock;
|
|
spyRenameWinArchive.mockReturnValue('/tmp/jdk.tar.gz.zip');
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.resetAllMocks();
|
|
jest.clearAllMocks();
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
it('verifies signature when enabled', async () => {
|
|
const distribution = new TemurinDistribution(
|
|
{
|
|
version: '17',
|
|
architecture: 'x64',
|
|
packageType: 'jdk',
|
|
checkLatest: false,
|
|
verifySignature: true
|
|
},
|
|
TemurinImplementation.Hotspot
|
|
);
|
|
|
|
await distribution['downloadTool']({
|
|
version: '17.0.14+7',
|
|
url: 'https://example.com/jdk.tar.gz',
|
|
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
|
|
});
|
|
|
|
expect(spyVerifySignature).toHaveBeenCalledWith(
|
|
'/tmp/jdk.tar.gz',
|
|
'https://example.com/jdk.tar.gz.sig',
|
|
ADOPTIUM_PUBLIC_KEY
|
|
);
|
|
});
|
|
|
|
it('fails when signature is missing and verification is enabled', async () => {
|
|
const distribution = new TemurinDistribution(
|
|
{
|
|
version: '17',
|
|
architecture: 'x64',
|
|
packageType: 'jdk',
|
|
checkLatest: false,
|
|
verifySignature: true
|
|
},
|
|
TemurinImplementation.Hotspot
|
|
);
|
|
|
|
await expect(
|
|
distribution['downloadTool']({
|
|
version: '17.0.14+7',
|
|
url: 'https://example.com/jdk.tar.gz'
|
|
})
|
|
).rejects.toThrow(
|
|
"Input 'verify-signature' is enabled, but no signature URL was found"
|
|
);
|
|
expect(spyVerifySignature).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('uses custom public key when verifySignaturePublicKey is provided', async () => {
|
|
const customKey =
|
|
'-----BEGIN PGP PUBLIC KEY BLOCK-----\ncustom\n-----END PGP PUBLIC KEY BLOCK-----';
|
|
const distribution = new TemurinDistribution(
|
|
{
|
|
version: '17',
|
|
architecture: 'x64',
|
|
packageType: 'jdk',
|
|
checkLatest: false,
|
|
verifySignature: true,
|
|
verifySignaturePublicKey: customKey
|
|
},
|
|
TemurinImplementation.Hotspot
|
|
);
|
|
|
|
await distribution['downloadTool']({
|
|
version: '17.0.14+7',
|
|
url: 'https://example.com/jdk.tar.gz',
|
|
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
|
|
});
|
|
|
|
expect(spyVerifySignature).toHaveBeenCalledWith(
|
|
'/tmp/jdk.tar.gz',
|
|
'https://example.com/jdk.tar.gz.sig',
|
|
customKey
|
|
);
|
|
});
|
|
});
|