mirror of
https://gitea.com/actions/setup-java.git
synced 2026-08-07 02:31:20 +00:00
Add conditional JDK caching (#1201)
* Add JDK caching Cache resolved JDK tool-cache entries by exact platform and release identity, with a default-on cache-jdk input and explicit opt-out. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Apply batched suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Fix JDK cache CI validation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Update brace-expansion security fix Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Refresh brace-expansion license metadata Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Refine JDK cache semantics Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Refine JDK cache documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Simplify JDK cache identity Use one normalized runner OS dimension, reset the internal cache key schema for the unreleased feature, and align documentation, tests, and bundles. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Align JDK cache OS identity Use the established RUNNER_OS value directly and retain process.platform only as a non-Actions fallback. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Harden JDK cache saves and document tool-cache reuse Bind each JDK cache key to the installation identity it was computed for, keep post-job saves best-effort per entry, and state the real reuse and verification guarantee in the documentation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * docs: restructure README caching section Rename '## Caching dependencies' to '## Caching' and add a what-gets-cached overview table covering the dependency, wrapper, and JDK caches. Lead with the common 'cache: maven' example and the dependency-cache material, and demote JDK caching into its own subsection. Also corrects the IMPORTANT callout, which implied JDK caching required an explicit opt-in; it is enabled implicitly whenever 'cache' is set. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * docs: fix caching documentation defects - Remove pull-request framing that compared behavior to `main`; state the tool-cache and `jdkfile` behavior directly and unconditionally. - Clarify that the JDK cache is a separate cache *entry* from the dependency and wrapper caches, while its *enablement* is coupled to `cache`, so the opening paragraph agrees with the enablement matrix. - Cite the actions/setup-java-benchmarks repository instead of an open PR and a self-referential PR comment, keeping the measured figures and caveats. - Keep the `cache`/`cache-jdk` matrix only in docs/advanced-usage.md and summarize the rules in prose in README.md to avoid divergence. - Describe the guarantee that a cache key is only saved with the installation it was computed for, instead of documenting inode/size/timestamp internals. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * docs: add V6 what's new entry for JDK caching Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e2755464-4e83-47b6-ba71-731bb481b418 --------- 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> Copilot-Session: e2755464-4e83-47b6-ba71-731bb481b418
This commit is contained in:
@@ -8,6 +8,9 @@ import {
|
||||
beforeAll,
|
||||
afterAll
|
||||
} from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
// Mock @actions/cache before importing source modules
|
||||
const real_cache_module = await import('@actions/cache');
|
||||
@@ -60,6 +63,9 @@ const core = await import('@actions/core');
|
||||
const cache = await import('@actions/cache');
|
||||
const {run: cleanup} = await import('../src/cleanup-java.js');
|
||||
const util = await import('../src/util.js');
|
||||
const {registerJdk, buildJdkCacheKey} = await import('../src/jdk-cache.js');
|
||||
|
||||
const jdkTempRoots: string[] = [];
|
||||
|
||||
describe('cleanup', () => {
|
||||
let spyWarning: any;
|
||||
@@ -88,6 +94,9 @@ describe('cleanup', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (jdkTempRoots.length) {
|
||||
fs.rmSync(jdkTempRoots.pop()!, {recursive: true, force: true});
|
||||
}
|
||||
resetState();
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
@@ -163,6 +172,103 @@ describe('cleanup', () => {
|
||||
|
||||
expect(spyCacheSave).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('saves the JDK cache without dependency caching', async () => {
|
||||
const {key, path: jdkPath, state} = createRegisteredJdk();
|
||||
(core.getInput as jest.Mock<any>).mockImplementation((name: string) =>
|
||||
name === 'cache-jdk' ? 'true' : ''
|
||||
);
|
||||
(core.getState as jest.Mock<any>).mockImplementation((name: string) =>
|
||||
name === 'jdk-caches' ? state : ''
|
||||
);
|
||||
spyCacheSave.mockResolvedValue(1);
|
||||
|
||||
await cleanup();
|
||||
|
||||
expect(spyCacheSave).toHaveBeenCalledWith([jdkPath], key);
|
||||
});
|
||||
|
||||
it('does not save a JDK cache when cache-jdk is disabled', async () => {
|
||||
(core.getInput as jest.Mock<any>).mockImplementation((name: string) =>
|
||||
name === 'cache-jdk' ? 'false' : ''
|
||||
);
|
||||
|
||||
await cleanup();
|
||||
|
||||
expect(spyCacheSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['', '', false],
|
||||
['', 'true', true],
|
||||
['', 'false', false],
|
||||
['maven', '', true],
|
||||
['maven', 'true', true],
|
||||
['maven', 'false', false]
|
||||
])(
|
||||
'uses effective JDK caching for cache=%j and cache-jdk=%j',
|
||||
async (cacheInput, cacheJdkInput, expectedJdkSave) => {
|
||||
const {key: jdkKey, path: jdkPath, state} = createRegisteredJdk();
|
||||
(core.getInput as jest.Mock<any>).mockImplementation((name: string) => {
|
||||
if (name === 'cache') return cacheInput;
|
||||
if (name === 'cache-jdk') return cacheJdkInput;
|
||||
return '';
|
||||
});
|
||||
(core.getState as jest.Mock<any>).mockImplementation((name: string) =>
|
||||
name === 'jdk-caches' ? state : ''
|
||||
);
|
||||
spyCacheSave.mockResolvedValue(1);
|
||||
|
||||
await cleanup();
|
||||
|
||||
const jdkSaveCalls = spyCacheSave.mock.calls.filter(
|
||||
([, key]) => key === jdkKey
|
||||
);
|
||||
expect(jdkSaveCalls).toHaveLength(expectedJdkSave ? 1 : 0);
|
||||
if (expectedJdkSave) {
|
||||
expect(spyCacheSave).toHaveBeenCalledWith([jdkPath], jdkKey);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
it('keeps saving the remaining JDK caches when one save fails', async () => {
|
||||
const first = createRegisteredJdk();
|
||||
const second = createRegisteredJdk('17.0.19+9');
|
||||
(core.getInput as jest.Mock<any>).mockImplementation((name: string) =>
|
||||
name === 'cache-jdk' ? 'true' : ''
|
||||
);
|
||||
(core.getState as jest.Mock<any>).mockImplementation((name: string) =>
|
||||
name === 'jdk-caches' ? second.state : ''
|
||||
);
|
||||
spyCacheSave.mockImplementation(async (paths: string[]) => {
|
||||
if (paths[0] === first.path) {
|
||||
throw new Error('Unexpected save failure');
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
|
||||
await cleanup();
|
||||
|
||||
expect(spyCacheSave).toHaveBeenCalledWith([first.path], first.key);
|
||||
expect(spyCacheSave).toHaveBeenCalledWith([second.path], second.key);
|
||||
expect(spyCoreError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not save a JDK installation that was replaced after registration', async () => {
|
||||
const {key, path: jdkPath, state, replace} = createRegisteredJdk();
|
||||
(core.getInput as jest.Mock<any>).mockImplementation((name: string) =>
|
||||
name === 'cache-jdk' ? 'true' : ''
|
||||
);
|
||||
(core.getState as jest.Mock<any>).mockImplementation((name: string) =>
|
||||
name === 'jdk-caches' ? state : ''
|
||||
);
|
||||
spyCacheSave.mockResolvedValue(1);
|
||||
replace();
|
||||
|
||||
await cleanup();
|
||||
|
||||
expect(spyCacheSave).not.toHaveBeenCalledWith([jdkPath], key);
|
||||
});
|
||||
});
|
||||
|
||||
function resetState() {
|
||||
@@ -199,3 +305,49 @@ function createStateForSuccessfulRestoreWithWrapper(packageManager: string) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a real JDK installation in a temporary tool cache so the post-job
|
||||
* save sees the same installation identity that setup recorded.
|
||||
*/
|
||||
function createRegisteredJdk(version = '21.0.8+9') {
|
||||
const root = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'setup-java-cleanup-jdk-')
|
||||
);
|
||||
jdkTempRoots.push(root);
|
||||
const jdkPath = path.join(
|
||||
root,
|
||||
'Java_temurin_jdk',
|
||||
version.replace('+', '-')
|
||||
);
|
||||
const write = (marker: string) => {
|
||||
const architecturePath = path.join(jdkPath, 'x64');
|
||||
fs.rmSync(architecturePath, {recursive: true, force: true});
|
||||
fs.rmSync(`${architecturePath}.complete`, {force: true});
|
||||
fs.mkdirSync(architecturePath, {recursive: true});
|
||||
fs.writeFileSync(path.join(architecturePath, 'release'), marker);
|
||||
fs.writeFileSync(`${architecturePath}.complete`, marker);
|
||||
};
|
||||
write('installed');
|
||||
|
||||
const jdk = {
|
||||
distribution: 'temurin',
|
||||
packageType: 'jdk',
|
||||
architecture: 'x64',
|
||||
version,
|
||||
source: `sha256:${path.basename(root)}`,
|
||||
verification: 'unverified',
|
||||
path: jdkPath
|
||||
};
|
||||
registerJdk(jdk);
|
||||
const state = (
|
||||
(core.saveState as jest.Mock).mock.calls.at(-1) as string[]
|
||||
)[1];
|
||||
|
||||
return {
|
||||
key: buildJdkCacheKey(jdk),
|
||||
path: jdkPath,
|
||||
state,
|
||||
replace: () => write('replaced-by-a-later-step')
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,6 +70,14 @@ jest.unstable_mockModule('@actions/tool-cache', () => ({
|
||||
}
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../src/jdk-cache.js', () => ({
|
||||
getJdkVerificationIdentity: jest.fn((verified: boolean, key?: string) =>
|
||||
verified ? (key ? 'verified:custom' : 'verified:bundled') : 'unverified'
|
||||
),
|
||||
registerJdk: jest.fn(),
|
||||
restoreJdk: jest.fn()
|
||||
}));
|
||||
|
||||
const real_util_module = await import('../../src/util.js');
|
||||
jest.unstable_mockModule('../../src/util.js', () => ({
|
||||
...real_util_module,
|
||||
@@ -86,6 +94,7 @@ jest.unstable_mockModule('../../src/util.js', () => ({
|
||||
const core = await import('@actions/core');
|
||||
const tc = await import('@actions/tool-cache');
|
||||
const util = await import('../../src/util.js');
|
||||
const jdkCache = await import('../../src/jdk-cache.js');
|
||||
const {JavaBase} = await import('../../src/distributions/base-installer.js');
|
||||
|
||||
class EmptyJavaBase extends JavaBase {
|
||||
@@ -336,6 +345,10 @@ describe('setupJava', () => {
|
||||
let spyCoreError: any;
|
||||
|
||||
beforeEach(() => {
|
||||
(jdkCache.getJdkVerificationIdentity as jest.Mock).mockImplementation(
|
||||
(verified: boolean, key?: string) =>
|
||||
verified ? (key ? 'verified:custom' : 'verified:bundled') : 'unverified'
|
||||
);
|
||||
spyGetToolcachePath = util.getToolcachePath as jest.Mock;
|
||||
spyGetToolcachePath.mockImplementation(
|
||||
(toolname: string, javaVersion: string, architecture: string) => {
|
||||
@@ -463,8 +476,10 @@ describe('setupJava', () => {
|
||||
architecture: 'x86',
|
||||
packageType: 'jdk',
|
||||
checkLatest: false,
|
||||
forceDownload: true
|
||||
forceDownload: true,
|
||||
cacheJdk: true
|
||||
});
|
||||
|
||||
const findInToolcache = jest.fn(() => ({
|
||||
version: actualJavaVersion,
|
||||
path: javaPathInstalled
|
||||
@@ -484,6 +499,111 @@ describe('setupJava', () => {
|
||||
expect(spyCoreInfo).not.toHaveBeenCalledWith(
|
||||
`Resolved Java ${actualJavaVersion} from tool-cache`
|
||||
);
|
||||
expect(jdkCache.restoreJdk).not.toHaveBeenCalled();
|
||||
expect(jdkCache.registerJdk).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
version: actualJavaVersion,
|
||||
verification: 'unverified'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[false, false, false, false],
|
||||
[false, true, true, true],
|
||||
[true, false, false, false],
|
||||
[true, true, false, true]
|
||||
])(
|
||||
'handles force-download=%s and cache-jdk=%s',
|
||||
async (forceDownload, cacheJdkEnabled, restores, registers) => {
|
||||
mockJavaBase = new EmptyJavaBase({
|
||||
version: actualJavaVersion,
|
||||
architecture: 'x86',
|
||||
packageType: 'jdk',
|
||||
checkLatest: true,
|
||||
forceDownload,
|
||||
cacheJdk: cacheJdkEnabled
|
||||
});
|
||||
(jdkCache.restoreJdk as jest.Mock).mockResolvedValue(false);
|
||||
|
||||
await mockJavaBase.setupJava();
|
||||
|
||||
expect(jdkCache.restoreJdk).toHaveBeenCalledTimes(restores ? 1 : 0);
|
||||
expect(jdkCache.registerJdk).toHaveBeenCalledTimes(registers ? 1 : 0);
|
||||
}
|
||||
);
|
||||
|
||||
it('restores the exact resolved JDK before downloading', async () => {
|
||||
const toolCachePath = path.join('toolcache');
|
||||
jest.replaceProperty(process, 'env', {
|
||||
...process.env,
|
||||
RUNNER_TOOL_CACHE: toolCachePath
|
||||
});
|
||||
mockJavaBase = new EmptyJavaBase({
|
||||
version: '11',
|
||||
architecture: 'x86',
|
||||
packageType: 'jdk',
|
||||
checkLatest: true,
|
||||
cacheJdk: true
|
||||
});
|
||||
const downloadTool = jest.spyOn(mockJavaBase as any, 'downloadTool');
|
||||
(jdkCache.restoreJdk as jest.Mock).mockResolvedValue(true);
|
||||
jest
|
||||
.spyOn(mockJavaBase as any, 'getRestoredJdkPath')
|
||||
.mockReturnValue(javaPathInstalled);
|
||||
|
||||
await expect(mockJavaBase.setupJava()).resolves.toEqual({
|
||||
version: actualJavaVersion,
|
||||
path: javaPathInstalled
|
||||
});
|
||||
|
||||
expect(jdkCache.restoreJdk).toHaveBeenCalledWith({
|
||||
distribution: 'Empty',
|
||||
packageType: 'jdk',
|
||||
architecture: 'x86',
|
||||
version: actualJavaVersion,
|
||||
source: `some/random_url/java/${actualJavaVersion}`,
|
||||
verification: 'unverified',
|
||||
path: path.join(toolCachePath, 'Java_Empty_jdk', actualJavaVersion)
|
||||
});
|
||||
expect(downloadTool).not.toHaveBeenCalled();
|
||||
expect(spyCoreInfo).not.toHaveBeenCalledWith('Trying to download...');
|
||||
// A restored entry is already stored under its key; it must not be
|
||||
// re-registered for a post-job save.
|
||||
expect(jdkCache.registerJdk).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('registers the downloaded JDK identity after a JDK cache miss', async () => {
|
||||
const toolCachePath = path.join('toolcache');
|
||||
jest.replaceProperty(process, 'env', {
|
||||
...process.env,
|
||||
RUNNER_TOOL_CACHE: toolCachePath
|
||||
});
|
||||
mockJavaBase = new EmptyJavaBase({
|
||||
version: '11',
|
||||
architecture: 'x86',
|
||||
packageType: 'jdk',
|
||||
checkLatest: true,
|
||||
cacheJdk: true
|
||||
});
|
||||
(jdkCache.restoreJdk as jest.Mock).mockResolvedValue(false);
|
||||
|
||||
await mockJavaBase.setupJava();
|
||||
|
||||
const expectedIdentity = {
|
||||
distribution: 'Empty',
|
||||
packageType: 'jdk',
|
||||
architecture: 'x86',
|
||||
version: actualJavaVersion,
|
||||
source: `some/random_url/java/${actualJavaVersion}`,
|
||||
verification: 'unverified',
|
||||
path: path.join(toolCachePath, 'Java_Empty_jdk', actualJavaVersion)
|
||||
};
|
||||
expect(jdkCache.restoreJdk).toHaveBeenCalledWith(expectedIdentity);
|
||||
// Registration happens after the installation exists, so the post-job save
|
||||
// can detect a later step replacing it.
|
||||
expect(jdkCache.registerJdk).toHaveBeenCalledWith(expectedIdentity);
|
||||
expect(spyCoreInfo).toHaveBeenCalledWith('Trying to download...');
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -12,6 +12,9 @@ import fs from 'fs';
|
||||
|
||||
import path from 'path';
|
||||
import * as semver from 'semver';
|
||||
import os from 'os';
|
||||
|
||||
const realStatSync = fs.statSync;
|
||||
|
||||
// Mock @actions/core before importing source modules that depend on it
|
||||
jest.unstable_mockModule('@actions/core', () => ({
|
||||
@@ -54,6 +57,12 @@ jest.unstable_mockModule('@actions/tool-cache', () => ({
|
||||
evaluateVersions: jest.fn()
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../src/jdk-cache.js', () => ({
|
||||
getJdkVerificationIdentity: jest.fn(() => 'unverified'),
|
||||
registerJdk: jest.fn(),
|
||||
restoreJdk: jest.fn()
|
||||
}));
|
||||
|
||||
const real_util_module = await import('../../src/util.js');
|
||||
jest.unstable_mockModule('../../src/util.js', () => ({
|
||||
...real_util_module,
|
||||
@@ -70,6 +79,7 @@ jest.unstable_mockModule('../../src/util.js', () => ({
|
||||
const core = await import('@actions/core');
|
||||
const tc = await import('@actions/tool-cache');
|
||||
const util = await import('../../src/util.js');
|
||||
const jdkCache = await import('../../src/jdk-cache.js');
|
||||
const {LocalDistribution} =
|
||||
await import('../../src/distributions/local/installer.js');
|
||||
|
||||
@@ -95,6 +105,9 @@ describe('setupJava', () => {
|
||||
const expectedJdkFile = 'JavaLocalJdkFile';
|
||||
|
||||
beforeEach(() => {
|
||||
(jdkCache.getJdkVerificationIdentity as jest.Mock).mockReturnValue(
|
||||
'unverified'
|
||||
);
|
||||
spyGetToolcachePath = util.getToolcachePath as jest.Mock;
|
||||
spyGetToolcachePath.mockImplementation(
|
||||
(toolname: string, javaVersion: string, architecture: string) => {
|
||||
@@ -231,6 +244,72 @@ describe('setupJava', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[false, true, true],
|
||||
[true, false, true]
|
||||
])(
|
||||
'handles jdkfile caching with force-download=%s',
|
||||
async (forceDownload, restores, registers) => {
|
||||
const temporaryDirectory = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'setup-java-local-cache-')
|
||||
);
|
||||
const jdkFile = path.join(temporaryDirectory, 'java.tar.gz');
|
||||
fs.writeFileSync(jdkFile, 'jdk archive');
|
||||
spyGetToolcachePath.mockReturnValue('');
|
||||
spyFsStat.mockImplementation((file: string) => realStatSync(file));
|
||||
(jdkCache.restoreJdk as jest.Mock).mockResolvedValue(false);
|
||||
|
||||
try {
|
||||
mockJavaBase = new LocalDistribution(
|
||||
{
|
||||
version: actualJavaVersion,
|
||||
architecture: 'x86',
|
||||
packageType: 'jdk',
|
||||
checkLatest: false,
|
||||
forceDownload,
|
||||
cacheJdk: true
|
||||
},
|
||||
jdkFile
|
||||
);
|
||||
|
||||
await mockJavaBase.setupJava();
|
||||
|
||||
expect(jdkCache.restoreJdk).toHaveBeenCalledTimes(restores ? 1 : 0);
|
||||
expect(jdkCache.registerJdk).toHaveBeenCalledTimes(registers ? 1 : 0);
|
||||
expect(
|
||||
(jdkCache.restoreJdk as jest.Mock).mock.calls[0]?.[0] ??
|
||||
(jdkCache.registerJdk as jest.Mock).mock.calls[0]?.[0]
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
distribution: 'jdkfile',
|
||||
version: actualJavaVersion,
|
||||
verification: 'unverified'
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(temporaryDirectory, {recursive: true});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
it('rejects signature verification for jdkfile archives', async () => {
|
||||
mockJavaBase = new LocalDistribution(
|
||||
{
|
||||
version: actualJavaVersion,
|
||||
architecture: 'x86',
|
||||
packageType: 'jdk',
|
||||
checkLatest: false,
|
||||
verifySignature: true
|
||||
},
|
||||
expectedJdkFile
|
||||
);
|
||||
|
||||
await expect(mockJavaBase.setupJava()).rejects.toThrow(
|
||||
"Input 'verify-signature' is not supported for distribution 'jdkfile'."
|
||||
);
|
||||
expect(spyGetToolcachePath).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("java is resolved from toolcache, jdkfile doesn't exist", async () => {
|
||||
const inputs = {
|
||||
version: actualJavaVersion,
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
jest.unstable_mockModule('@actions/cache', () => ({
|
||||
restoreCache: jest.fn(),
|
||||
saveCache: jest.fn(),
|
||||
ReserveCacheError: class ReserveCacheError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ReserveCacheError';
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('@actions/core', () => ({
|
||||
info: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
saveState: jest.fn(),
|
||||
getState: jest.fn()
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../src/cache-feature.js', () => ({
|
||||
isCacheFeatureAvailable: jest.fn()
|
||||
}));
|
||||
|
||||
const cache = await import('@actions/cache');
|
||||
const core = await import('@actions/core');
|
||||
const cacheFeature = await import('../src/cache-feature.js');
|
||||
const {
|
||||
buildJdkCacheKey,
|
||||
getJdkVerificationIdentity,
|
||||
registerJdk,
|
||||
restoreJdk,
|
||||
saveJdkCaches
|
||||
} = await import('../src/jdk-cache.js');
|
||||
|
||||
const jdk = {
|
||||
distribution: 'temurin',
|
||||
packageType: 'jdk',
|
||||
architecture: 'x64',
|
||||
version: '21.0.8+9',
|
||||
source: 'sha256:abc123',
|
||||
verification: 'unverified',
|
||||
path: '/toolcache/Java_temurin_jdk/21.0.8-9'
|
||||
};
|
||||
|
||||
describe('JDK cache', () => {
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
const createInstallation = (marker = 'a'): string => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'setup-java-jdk-'));
|
||||
tempRoots.push(root);
|
||||
const jdkPath = path.join(root, 'Java_temurin_jdk', '21.0.8-9');
|
||||
writeInstallation(jdkPath, marker);
|
||||
return jdkPath;
|
||||
};
|
||||
|
||||
const writeInstallation = (jdkPath: string, marker: string): void => {
|
||||
const architecturePath = path.join(jdkPath, 'x64');
|
||||
fs.rmSync(architecturePath, {recursive: true, force: true});
|
||||
fs.rmSync(`${architecturePath}.complete`, {force: true});
|
||||
fs.mkdirSync(path.join(architecturePath, 'bin'), {recursive: true});
|
||||
fs.writeFileSync(path.join(architecturePath, 'bin', 'java'), marker);
|
||||
fs.writeFileSync(`${architecturePath}.complete`, marker);
|
||||
};
|
||||
|
||||
const lastState = (): string =>
|
||||
((core.saveState as jest.Mock).mock.calls.at(-1) as string[])[1];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
(cacheFeature.isCacheFeatureAvailable as jest.Mock).mockReturnValue(true);
|
||||
process.env['RUNNER_OS'] = 'Linux';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
delete process.env['RUNNER_OS'];
|
||||
while (tempRoots.length) {
|
||||
fs.rmSync(tempRoots.pop()!, {recursive: true, force: true});
|
||||
}
|
||||
});
|
||||
|
||||
it('builds distinct keys for incompatible JDK identities', () => {
|
||||
const key = buildJdkCacheKey(jdk);
|
||||
|
||||
expect(key).toMatch(/^setup-java-jdk-v1-Linux-x64-[a-f0-9]{64}$/);
|
||||
expect(buildJdkCacheKey({...jdk, architecture: 'aarch64'})).not.toBe(key);
|
||||
expect(buildJdkCacheKey({...jdk, distribution: 'zulu'})).not.toBe(key);
|
||||
expect(buildJdkCacheKey({...jdk, packageType: 'jre'})).not.toBe(key);
|
||||
expect(buildJdkCacheKey({...jdk, version: '21.0.7+6'})).not.toBe(key);
|
||||
expect(buildJdkCacheKey({...jdk, source: 'sha256:def456'})).not.toBe(key);
|
||||
});
|
||||
|
||||
it('preserves canonical runner OS values and separates operating systems', () => {
|
||||
process.env['RUNNER_OS'] = 'Linux';
|
||||
const linux = buildJdkCacheKey(jdk);
|
||||
process.env['RUNNER_OS'] = 'Windows';
|
||||
const windows = buildJdkCacheKey(jdk);
|
||||
process.env['RUNNER_OS'] = 'macOS';
|
||||
const macos = buildJdkCacheKey(jdk);
|
||||
|
||||
expect(new Set([linux, windows, macos])).toHaveProperty('size', 3);
|
||||
expect(linux).toMatch(/^setup-java-jdk-v1-Linux-x64-/);
|
||||
expect(windows).toMatch(/^setup-java-jdk-v1-Windows-x64-/);
|
||||
expect(macos).toMatch(/^setup-java-jdk-v1-macOS-x64-/);
|
||||
});
|
||||
|
||||
it('falls back to process.platform without RUNNER_OS', () => {
|
||||
delete process.env['RUNNER_OS'];
|
||||
|
||||
expect(buildJdkCacheKey(jdk)).toMatch(
|
||||
new RegExp(`^setup-java-jdk-v1-${process.platform}-x64-`)
|
||||
);
|
||||
});
|
||||
|
||||
it('separates unverified, bundled-key, and custom-key caches', () => {
|
||||
const unverified = getJdkVerificationIdentity(false);
|
||||
const bundled = getJdkVerificationIdentity(true);
|
||||
const customA = getJdkVerificationIdentity(
|
||||
true,
|
||||
'-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nkey-a\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n'
|
||||
);
|
||||
const customANormalized = getJdkVerificationIdentity(
|
||||
true,
|
||||
'-----BEGIN PGP PUBLIC KEY BLOCK-----\nkey-a\n-----END PGP PUBLIC KEY BLOCK-----'
|
||||
);
|
||||
const customB = getJdkVerificationIdentity(true, 'different-key');
|
||||
|
||||
expect(new Set([unverified, bundled, customA, customB])).toHaveProperty(
|
||||
'size',
|
||||
4
|
||||
);
|
||||
expect(customA).toBe(customANormalized);
|
||||
expect(customA).not.toContain('key-a');
|
||||
expect(
|
||||
new Set(
|
||||
[unverified, bundled, customA, customB].map(verification =>
|
||||
buildJdkCacheKey({...jdk, verification})
|
||||
)
|
||||
)
|
||||
).toHaveProperty('size', 4);
|
||||
});
|
||||
|
||||
it('restores and records an exact JDK cache hit', async () => {
|
||||
(cache.restoreCache as jest.Mock).mockResolvedValue(buildJdkCacheKey(jdk));
|
||||
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
|
||||
|
||||
await expect(restoreJdk(jdk)).resolves.toBe(true);
|
||||
|
||||
expect(cache.restoreCache).toHaveBeenCalledWith(
|
||||
[jdk.path],
|
||||
buildJdkCacheKey(jdk)
|
||||
);
|
||||
const architecturePath = path.join(jdk.path, 'x64');
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(architecturePath);
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(`${architecturePath}.complete`);
|
||||
expect(core.saveState).toHaveBeenCalledWith(
|
||||
'jdk-caches',
|
||||
expect.stringContaining(buildJdkCacheKey(jdk))
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to download when restoration fails', async () => {
|
||||
(cache.restoreCache as jest.Mock).mockRejectedValue(
|
||||
new Error('cache unavailable')
|
||||
);
|
||||
|
||||
await expect(restoreJdk(jdk)).resolves.toBe(false);
|
||||
expect(core.warning).toHaveBeenCalledWith(
|
||||
'Failed to restore JDK cache: cache unavailable'
|
||||
);
|
||||
});
|
||||
|
||||
it('saves a downloaded JDK registered after installation', async () => {
|
||||
const jdkPath = createInstallation();
|
||||
const installed = {...jdk, path: jdkPath};
|
||||
const key = buildJdkCacheKey(installed);
|
||||
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
await restoreJdk(installed);
|
||||
registerJdk(installed);
|
||||
(core.getState as jest.Mock).mockReturnValue(lastState());
|
||||
(cache.saveCache as jest.Mock).mockResolvedValue(1);
|
||||
|
||||
await saveJdkCaches();
|
||||
|
||||
expect(cache.saveCache).toHaveBeenCalledWith([jdkPath], key);
|
||||
});
|
||||
|
||||
it('does not save an installation that was replaced after registration', async () => {
|
||||
const jdkPath = createInstallation();
|
||||
const installed = {...jdk, path: jdkPath};
|
||||
const key = buildJdkCacheKey(installed);
|
||||
|
||||
registerJdk(installed);
|
||||
(core.getState as jest.Mock).mockReturnValue(lastState());
|
||||
writeInstallation(jdkPath, 'replaced-by-a-later-step');
|
||||
|
||||
await saveJdkCaches();
|
||||
|
||||
expect(cache.saveCache).not.toHaveBeenCalledWith([jdkPath], key);
|
||||
expect(core.warning).toHaveBeenCalledWith(
|
||||
expect.stringContaining('was replaced after it was registered')
|
||||
);
|
||||
});
|
||||
|
||||
it('saves only the key matching the installation that occupies the path', async () => {
|
||||
const jdkPath = createInstallation();
|
||||
const verified = {...jdk, path: jdkPath, verification: 'verified:bundled'};
|
||||
const unverified = {...jdk, path: jdkPath};
|
||||
|
||||
registerJdk(verified);
|
||||
writeInstallation(jdkPath, 'force-downloaded-without-verification');
|
||||
registerJdk(unverified);
|
||||
(core.getState as jest.Mock).mockReturnValue(lastState());
|
||||
(cache.saveCache as jest.Mock).mockResolvedValue(1);
|
||||
|
||||
await saveJdkCaches();
|
||||
|
||||
expect(cache.saveCache).not.toHaveBeenCalledWith(
|
||||
[jdkPath],
|
||||
buildJdkCacheKey(verified)
|
||||
);
|
||||
expect(cache.saveCache).toHaveBeenCalledWith(
|
||||
[jdkPath],
|
||||
buildJdkCacheKey(unverified)
|
||||
);
|
||||
});
|
||||
|
||||
it('does not save a path that was never registered as installed', async () => {
|
||||
const jdkPath = createInstallation();
|
||||
const installed = {...jdk, path: jdkPath};
|
||||
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
await restoreJdk(installed);
|
||||
(core.getState as jest.Mock).mockReturnValue(lastState());
|
||||
|
||||
await saveJdkCaches();
|
||||
|
||||
expect(cache.saveCache).not.toHaveBeenCalledWith(
|
||||
[jdkPath],
|
||||
buildJdkCacheKey(installed)
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps saving the remaining JDK caches when one save fails', async () => {
|
||||
const failingPath = createInstallation();
|
||||
const succeedingPath = createInstallation();
|
||||
const failing = {...jdk, path: failingPath};
|
||||
const succeeding = {...jdk, path: succeedingPath, version: '17.0.19+9'};
|
||||
|
||||
registerJdk(failing);
|
||||
registerJdk(succeeding);
|
||||
(core.getState as jest.Mock).mockReturnValue(lastState());
|
||||
(cache.saveCache as jest.Mock).mockImplementation(
|
||||
async (paths: unknown) => {
|
||||
if ((paths as string[])[0] === failingPath) {
|
||||
throw new Error('cache service unavailable');
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
);
|
||||
|
||||
await expect(saveJdkCaches()).resolves.toBeUndefined();
|
||||
|
||||
expect(cache.saveCache).toHaveBeenCalledWith(
|
||||
[succeedingPath],
|
||||
buildJdkCacheKey(succeeding)
|
||||
);
|
||||
expect(core.warning).toHaveBeenCalledWith(
|
||||
expect.stringContaining('cache service unavailable')
|
||||
);
|
||||
expect(core.info).toHaveBeenCalledWith(
|
||||
`JDK cache saved with the key: ${buildJdkCacheKey(succeeding)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('reports a reserved cache key without failing the remaining saves', async () => {
|
||||
const reservedPath = createInstallation();
|
||||
const reserved = {...jdk, path: reservedPath};
|
||||
|
||||
registerJdk(reserved);
|
||||
(core.getState as jest.Mock).mockReturnValue(lastState());
|
||||
(cache.saveCache as jest.Mock).mockRejectedValue(
|
||||
new cache.ReserveCacheError('Unable to reserve cache')
|
||||
);
|
||||
|
||||
await expect(saveJdkCaches()).resolves.toBeUndefined();
|
||||
|
||||
expect(core.info).toHaveBeenCalledWith('Unable to reserve cache');
|
||||
});
|
||||
|
||||
it('registers a force-downloaded JDK without restoring it', () => {
|
||||
const jdkPath = createInstallation();
|
||||
registerJdk({...jdk, path: jdkPath});
|
||||
|
||||
expect(cache.restoreCache).not.toHaveBeenCalled();
|
||||
expect(core.saveState).toHaveBeenCalledWith(
|
||||
'jdk-caches',
|
||||
expect.stringContaining(buildJdkCacheKey({...jdk, path: jdkPath}))
|
||||
);
|
||||
});
|
||||
|
||||
it('does not save an exact JDK cache hit again', async () => {
|
||||
const key = buildJdkCacheKey(jdk);
|
||||
(core.getState as jest.Mock).mockReturnValue(
|
||||
JSON.stringify([
|
||||
{
|
||||
key,
|
||||
path: jdk.path,
|
||||
architecture: jdk.architecture,
|
||||
matchedKey: key
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
await saveJdkCaches();
|
||||
|
||||
expect(cache.saveCache).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -33,7 +33,8 @@ jest.unstable_mockModule('fs', () => ({
|
||||
|
||||
jest.unstable_mockModule('../src/util.js', () => ({
|
||||
getBooleanInput: jest.fn(),
|
||||
getVersionFromFileContent: jest.fn()
|
||||
getVersionFromFileContent: jest.fn(),
|
||||
isJdkCacheEnabled: jest.fn()
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../src/toolchains.js', () => ({
|
||||
@@ -98,6 +99,7 @@ describe('setup-java conditional module loading', () => {
|
||||
return booleanInputs.get(name as string) ?? defaultValue;
|
||||
}
|
||||
);
|
||||
(util.isJdkCacheEnabled as jest.Mock).mockReturnValue(false);
|
||||
(toolchains.configureToolchains as jest.Mock).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ jest.unstable_mockModule('fs', () => ({
|
||||
|
||||
jest.unstable_mockModule('../src/util.js', () => ({
|
||||
getBooleanInput: jest.fn(),
|
||||
getVersionFromFileContent: jest.fn()
|
||||
getVersionFromFileContent: jest.fn(),
|
||||
isJdkCacheEnabled: jest.fn()
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../src/toolchains.js', () => ({
|
||||
@@ -113,6 +114,14 @@ describe('setup action orchestration', () => {
|
||||
return booleanInputs.get(name as string) ?? defaultValue;
|
||||
}
|
||||
);
|
||||
(util.isJdkCacheEnabled as jest.Mock).mockImplementation(
|
||||
(cache: string) => {
|
||||
const explicit = inputs.get('cache-jdk');
|
||||
return explicit
|
||||
? (booleanInputs.get('cache-jdk') ?? explicit === 'true')
|
||||
: Boolean(cache);
|
||||
}
|
||||
);
|
||||
(cacheFeature.isCacheFeatureAvailable as jest.Mock).mockReturnValue(true);
|
||||
(toolchainIds.validateToolchainIds as jest.Mock).mockImplementation(
|
||||
() => undefined
|
||||
@@ -217,6 +226,7 @@ describe('setup action orchestration', () => {
|
||||
packageType: 'jdk',
|
||||
checkLatest: true,
|
||||
forceDownload: true,
|
||||
cacheJdk: false,
|
||||
setDefault: false,
|
||||
verifySignature: true,
|
||||
verifySignaturePublicKey: 'public-key'
|
||||
@@ -457,6 +467,7 @@ describe('setup action orchestration', () => {
|
||||
it('does not initialize cache modules when cache input is absent', async () => {
|
||||
inputs.set('distribution', 'temurin');
|
||||
multilineInputs.set('java-version', ['21']);
|
||||
booleanInputs.set('cache-jdk', false);
|
||||
(factory.getJavaDistribution as jest.Mock).mockReturnValue({
|
||||
setupJava: jest.fn(async () => ({
|
||||
version: '21.0.4+7',
|
||||
@@ -468,8 +479,47 @@ describe('setup action orchestration', () => {
|
||||
|
||||
expect(cacheFeature.isCacheFeatureAvailable).not.toHaveBeenCalled();
|
||||
expect(cache.restore).not.toHaveBeenCalled();
|
||||
expect(factory.getJavaDistribution).toHaveBeenCalledWith(
|
||||
'temurin',
|
||||
expect.objectContaining({cacheJdk: false}),
|
||||
''
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['', '', false],
|
||||
['', 'true', true],
|
||||
['', 'false', false],
|
||||
['maven', '', true],
|
||||
['maven', 'true', true],
|
||||
['maven', 'false', false]
|
||||
])(
|
||||
'passes effective JDK caching for cache=%j and cache-jdk=%j',
|
||||
async (cacheInput, cacheJdkInput, expected) => {
|
||||
inputs.set('distribution', 'temurin');
|
||||
inputs.set('cache', cacheInput);
|
||||
inputs.set('cache-jdk', cacheJdkInput);
|
||||
multilineInputs.set('java-version', ['21']);
|
||||
if (cacheJdkInput) {
|
||||
booleanInputs.set('cache-jdk', cacheJdkInput === 'true');
|
||||
}
|
||||
(factory.getJavaDistribution as jest.Mock).mockReturnValue({
|
||||
setupJava: jest.fn(async () => ({
|
||||
version: '21.0.4+7',
|
||||
path: '/opt/java/21'
|
||||
}))
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(factory.getJavaDistribution).toHaveBeenCalledWith(
|
||||
'temurin',
|
||||
expect.objectContaining({cacheJdk: expected}),
|
||||
''
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it('reports unsupported distributions through core.setFailed', async () => {
|
||||
inputs.set('distribution', 'unsupported');
|
||||
multilineInputs.set('java-version', ['21']);
|
||||
|
||||
+33
-1
@@ -49,7 +49,8 @@ const {
|
||||
isGhes,
|
||||
validatePaginationUrl,
|
||||
getLatestMajorVersion,
|
||||
getBooleanInput
|
||||
getBooleanInput,
|
||||
isJdkCacheEnabled
|
||||
} = await import('../src/util.js');
|
||||
|
||||
describe('getBooleanInput', () => {
|
||||
@@ -115,6 +116,37 @@ describe('getBooleanInput', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isJdkCacheEnabled', () => {
|
||||
let inputs: Record<string, string>;
|
||||
|
||||
beforeEach(() => {
|
||||
inputs = {};
|
||||
(core.getInput as jest.Mock).mockImplementation(
|
||||
(name: string) => inputs[name] ?? ''
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['', '', false],
|
||||
['', 'true', true],
|
||||
['', 'false', false],
|
||||
['maven', '', true],
|
||||
['maven', 'true', true],
|
||||
['maven', 'false', false]
|
||||
])(
|
||||
'resolves cache=%j and cache-jdk=%j to %s',
|
||||
(cache, cacheJdk, expected) => {
|
||||
inputs['cache-jdk'] = cacheJdk;
|
||||
|
||||
expect(isJdkCacheEnabled(cache)).toBe(expected);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('isVersionSatisfies', () => {
|
||||
it.each([
|
||||
['x', '11.0.0', true],
|
||||
|
||||
Reference in New Issue
Block a user