5
0
mirror of https://gitea.com/actions/setup-java.git synced 2026-08-05 02:31:18 +00:00

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>
This commit is contained in:
Bruno Borges
2026-08-04 19:02:19 -04:00
parent 60b1ab8234
commit ee3e6d82d3
21 changed files with 1695 additions and 804 deletions
+27
View File
@@ -8,6 +8,7 @@ import {
beforeAll,
afterAll
} from '@jest/globals';
import fs from 'fs';
// Mock @actions/cache before importing source modules
const real_cache_module = await import('@actions/cache');
@@ -163,6 +164,32 @@ describe('cleanup', () => {
expect(spyCacheSave).toHaveBeenCalled();
});
it('saves the JDK cache without dependency caching', async () => {
const key = 'setup-java-jdk-v1-Linux-x64-key';
(core.getInput as jest.Mock).mockReturnValue('');
(core.getState as jest.Mock<any>).mockImplementation((name: string) =>
name === 'jdk-caches'
? JSON.stringify([{key, path: '/toolcache/java'}])
: ''
);
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
spyCacheSave.mockResolvedValue(1);
await cleanup();
expect(spyCacheSave).toHaveBeenCalledWith(['/toolcache/java'], 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();
});
});
function resetState() {
@@ -70,6 +70,10 @@ jest.unstable_mockModule('@actions/tool-cache', () => ({
}
}));
jest.unstable_mockModule('../../src/jdk-cache.js', () => ({
restoreJdk: jest.fn()
}));
const real_util_module = await import('../../src/util.js');
jest.unstable_mockModule('../../src/util.js', () => ({
...real_util_module,
@@ -86,6 +90,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 {
@@ -465,6 +470,7 @@ describe('setupJava', () => {
checkLatest: false,
forceDownload: true
});
const findInToolcache = jest.fn(() => ({
version: actualJavaVersion,
path: javaPathInstalled
@@ -486,6 +492,37 @@ describe('setupJava', () => {
);
});
it('restores the exact resolved JDK before downloading', async () => {
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}`,
path: path.join('Java_Empty_jdk', actualJavaVersion)
});
expect(downloadTool).not.toHaveBeenCalled();
expect(spyCoreInfo).not.toHaveBeenCalledWith('Trying to download...');
});
it.each([
[
{
+114
View File
@@ -0,0 +1,114 @@
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import fs from 'fs';
import path from 'path';
jest.unstable_mockModule('@actions/cache', () => ({
restoreCache: jest.fn(),
saveCache: jest.fn(),
ReserveCacheError: class ReserveCacheError extends Error {}
}));
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, restoreJdk, saveJdkCaches} =
await import('../src/jdk-cache.js');
const jdk = {
distribution: 'temurin',
packageType: 'jdk',
architecture: 'x64',
version: '21.0.8+9',
source: 'sha256:abc123',
path: '/toolcache/Java_temurin_jdk/21.0.8-9'
};
describe('JDK cache', () => {
beforeEach(() => {
jest.resetAllMocks();
(cacheFeature.isCacheFeatureAvailable as jest.Mock).mockReturnValue(true);
process.env['RUNNER_OS'] = 'Linux';
});
afterEach(() => {
jest.restoreAllMocks();
delete process.env['RUNNER_OS'];
});
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('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 recorded during restore', async () => {
const key = buildJdkCacheKey(jdk);
(core.getState as jest.Mock).mockReturnValue(
JSON.stringify([{key, path: jdk.path}])
);
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
(cache.saveCache as jest.Mock).mockResolvedValue(1);
await saveJdkCaches();
expect(cache.saveCache).toHaveBeenCalledWith([jdk.path], key);
});
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, matchedKey: key}])
);
await saveJdkCaches();
expect(cache.saveCache).not.toHaveBeenCalled();
});
});
+7
View File
@@ -217,6 +217,7 @@ describe('setup action orchestration', () => {
packageType: 'jdk',
checkLatest: true,
forceDownload: true,
cacheJdk: true,
setDefault: false,
verifySignature: true,
verifySignaturePublicKey: 'public-key'
@@ -457,6 +458,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,6 +470,11 @@ describe('setup action orchestration', () => {
expect(cacheFeature.isCacheFeatureAvailable).not.toHaveBeenCalled();
expect(cache.restore).not.toHaveBeenCalled();
expect(factory.getJavaDistribution).toHaveBeenCalledWith(
'temurin',
expect.objectContaining({cacheJdk: false}),
''
);
});
it('reports unsupported distributions through core.setFailed', async () => {