Reuse the tool cache for floating versions the resolution cache identified (#1219)

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
This commit is contained in:
Bruno Borges
2026-08-05 12:08:32 -04:00
committed by GitHub
parent 634b0f0d18
commit 2b61aea53d
3 changed files with 163 additions and 8 deletions
@@ -518,6 +518,106 @@ describe('setupJava', () => {
);
});
describe('floating tool-cache reuse', () => {
let toolCacheRoot: string;
const installVersion = (toolcacheVersion: string): string => {
const architecturePath = path.join(
toolCacheRoot,
'Java_Floating_jdk',
toolcacheVersion,
'x64'
);
fs.mkdirSync(architecturePath, {recursive: true});
fs.writeFileSync(`${architecturePath}.complete`, '');
return architecturePath;
};
beforeEach(() => {
toolCacheRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'setup-java-tc-'));
process.env['RUNNER_TOOL_CACHE'] = toolCacheRoot;
FloatingJavaBase.actualVersion = '21.0.8+9';
FloatingJavaBase.checksum = 'artifact-one';
(jdkCache.restoreJdk as jest.Mock).mockResolvedValue(false);
spyTcFindAllVersions.mockReturnValue([]);
});
afterEach(() => {
fs.rmSync(toolCacheRoot, {recursive: true, force: true});
delete process.env['RUNNER_TOOL_CACHE'];
});
const createDistribution = (forceDownload = false) =>
new FloatingJavaBase({
version: '21',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false,
cacheJdk: true,
forceDownload
});
it('reuses a tool-cache installation once the resolution cache identifies the floating version', async () => {
const installedPath = installVersion('21.0.8-9');
(jdkResolutionCache.restoreJdkResolution as jest.Mock).mockResolvedValue({
release: {version: '21.0.8+9'}
});
const distribution = createDistribution();
const downloadTool = jest.spyOn(distribution as any, 'downloadTool');
await expect(distribution.setupJava()).resolves.toEqual({
version: '21.0.8+9',
path: installedPath
});
// The artifact behind the mutable URL is already installed, so neither a
// download nor a cache round-trip is needed.
expect(downloadTool).not.toHaveBeenCalled();
expect(jdkCache.restoreJdk).not.toHaveBeenCalled();
});
it('ignores a tool-cache installation of a version the resolution cache did not vouch for', async () => {
installVersion('21.0.7-6');
(jdkResolutionCache.restoreJdkResolution as jest.Mock).mockResolvedValue({
release: {version: '21.0.8+9'}
});
const distribution = createDistribution();
const downloadTool = jest.spyOn(distribution as any, 'downloadTool');
await distribution.setupJava();
expect(downloadTool).toHaveBeenCalled();
});
it('never reuses the tool-cache for a floating artifact the resolution cache cannot identify', async () => {
installVersion('21.0.8-9');
spyTcFindAllVersions.mockReturnValue(['21.0.8-9']);
(jdkResolutionCache.restoreJdkResolution as jest.Mock).mockResolvedValue(
undefined
);
const distribution = createDistribution();
const downloadTool = jest.spyOn(distribution as any, 'downloadTool');
await distribution.setupJava();
// Nothing ties the installed bytes to what the URL serves right now.
expect(downloadTool).toHaveBeenCalled();
});
it('still downloads a resolution-cache-identified version when force-download is set', async () => {
installVersion('21.0.8-9');
(jdkResolutionCache.restoreJdkResolution as jest.Mock).mockResolvedValue({
release: {version: '21.0.8+9'}
});
const distribution = createDistribution(true);
const downloadTool = jest.spyOn(distribution as any, 'downloadTool');
await distribution.setupJava();
expect(downloadTool).toHaveBeenCalled();
});
});
it('uses the concrete versions of two different floating artifacts under the same major', async () => {
spyTcFindAllVersions.mockReturnValue(['21.0.8-9']);
spyGetToolcachePath.mockImplementation(