Skip to content

Commit 3cc3643

Browse files
brunoborgesCopilot
andauthored
Optimize Temurin tool-cache fast path with lazy loading (actions#1179)
* 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
1 parent 6937f5e commit 3cc3643

39 files changed

Lines changed: 191897 additions & 189838 deletions

‎__tests__/cache-feature.test.ts‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {jest, describe, it, expect, afterEach} from '@jest/globals';
2+
3+
jest.unstable_mockModule('@actions/cache', () => ({
4+
isFeatureAvailable: jest.fn()
5+
}));
6+
7+
jest.unstable_mockModule('@actions/core', () => ({
8+
warning: jest.fn(),
9+
debug: jest.fn(),
10+
getInput: jest.fn(),
11+
getBooleanInput: jest.fn(),
12+
getMultilineInput: jest.fn(),
13+
info: jest.fn(),
14+
error: jest.fn(),
15+
notice: jest.fn(),
16+
setFailed: jest.fn(),
17+
setOutput: jest.fn(),
18+
addPath: jest.fn(),
19+
exportVariable: jest.fn(),
20+
saveState: jest.fn(),
21+
getState: jest.fn(),
22+
setSecret: jest.fn(),
23+
isDebug: jest.fn(() => false),
24+
startGroup: jest.fn(),
25+
endGroup: jest.fn(),
26+
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
27+
toPlatformPath: jest.fn((p: string) => p),
28+
toWin32Path: jest.fn((p: string) => p),
29+
toPosixPath: jest.fn((p: string) => p)
30+
}));
31+
32+
const cache = await import('@actions/cache');
33+
const core = await import('@actions/core');
34+
const {isCacheFeatureAvailable} = await import('../src/cache-feature.js');
35+
36+
describe('isCacheFeatureAvailable', () => {
37+
it('is disabled on GHES when cache feature is unavailable', () => {
38+
(cache.isFeatureAvailable as jest.Mock<any>).mockImplementation(
39+
() => false
40+
);
41+
const warningMock = core.warning as jest.Mock;
42+
const message =
43+
'Caching is only supported on GHES version >= 3.5. If you are on a version >= 3.5, please check with your GHES admin if the Actions cache service is enabled or not.';
44+
45+
try {
46+
process.env['GITHUB_SERVER_URL'] = 'http://example.com';
47+
expect(isCacheFeatureAvailable()).toBe(false);
48+
expect(warningMock).toHaveBeenCalledWith(message);
49+
} finally {
50+
delete process.env['GITHUB_SERVER_URL'];
51+
}
52+
});
53+
54+
it('is disabled on dotcom when cache feature is unavailable', () => {
55+
(cache.isFeatureAvailable as jest.Mock<any>).mockImplementation(
56+
() => false
57+
);
58+
const warningMock = core.warning as jest.Mock;
59+
const message =
60+
'The runner was not able to contact the cache service. Caching will be skipped';
61+
62+
try {
63+
process.env['GITHUB_SERVER_URL'] = 'http://github.com';
64+
expect(isCacheFeatureAvailable()).toBe(false);
65+
expect(warningMock).toHaveBeenCalledWith(message);
66+
} finally {
67+
delete process.env['GITHUB_SERVER_URL'];
68+
}
69+
});
70+
71+
it('is enabled when cache feature is available', () => {
72+
(cache.isFeatureAvailable as jest.Mock<any>).mockImplementation(() => true);
73+
expect(isCacheFeatureAvailable()).toBe(true);
74+
});
75+
76+
afterEach(() => {
77+
jest.resetAllMocks();
78+
jest.clearAllMocks();
79+
});
80+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {jest, describe, it, expect} from '@jest/globals';
2+
3+
jest.unstable_mockModule('../../src/distributions/zulu/installer.js', () => {
4+
throw new Error(
5+
'Zulu installer module must not be imported on the Temurin fast path'
6+
);
7+
});
8+
9+
const {getJavaDistribution} =
10+
await import('../../src/distributions/distribution-factory.js');
11+
12+
describe('distribution factory lazy loading', () => {
13+
it('does not load non-selected distribution installers for Temurin', async () => {
14+
const distribution = await getJavaDistribution('temurin', {
15+
version: '21',
16+
architecture: 'x64',
17+
packageType: 'jdk',
18+
checkLatest: false
19+
});
20+
21+
expect(distribution).not.toBeNull();
22+
});
23+
});

‎__tests__/distributors/distribution-factory.test.ts‎

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ const installerOptions = (packageType: string, version = '25') => ({
2929
describe('getJavaDistribution', () => {
3030
it.each(supportedDistributionsOnCurrentPlatform)(
3131
'uses the shared retrying HTTP client for %s',
32-
distributionName => {
33-
const distribution = getJavaDistribution(distributionName, {
32+
async distributionName => {
33+
const distribution = await getJavaDistribution(distributionName, {
3434
version: '25',
3535
architecture: 'x64',
3636
packageType: 'jdk',
@@ -51,63 +51,66 @@ describe('getJavaDistribution', () => {
5151
? packageTypes.map(packageType => [distributionName, packageType])
5252
: []
5353
)
54-
)('accepts %s with java-package %s', (distributionName, packageType) => {
55-
expect(
56-
getJavaDistribution(
57-
distributionName,
58-
installerOptions(packageType as string)
59-
)
60-
).not.toBeNull();
61-
});
54+
)(
55+
'accepts %s with java-package %s',
56+
async (distributionName, packageType) => {
57+
expect(
58+
await getJavaDistribution(
59+
distributionName,
60+
installerOptions(packageType as string)
61+
)
62+
).not.toBeNull();
63+
}
64+
);
6265

6366
it.each(Object.entries(JAVA_PACKAGE_CAPABILITIES))(
6467
'rejects unsupported java-package values for %s',
65-
(distributionName, packageTypes) => {
66-
expect(() =>
68+
async (distributionName, packageTypes) => {
69+
await expect(
6770
getJavaDistribution(distributionName, installerOptions('jdk+typo'))
68-
).toThrow(
71+
).rejects.toThrow(
6972
`Java package 'jdk+typo' is not supported for distribution '${distributionName}'. Supported package types: ${packageTypes.join(', ')}.`
7073
);
7174
}
7275
);
7376

74-
it("rejects java-package 'jdk+jmods' for non-Temurin distributions", () => {
75-
expect(() =>
77+
it("rejects java-package 'jdk+jmods' for non-Temurin distributions", async () => {
78+
await expect(
7679
getJavaDistribution('zulu', installerOptions('jdk+jmods'))
77-
).toThrow(
80+
).rejects.toThrow(
7881
"Java package 'jdk+jmods' is not supported for distribution 'zulu'. Supported package types: jdk, jre, jdk+fx, jre+fx, jdk+crac, jre+crac."
7982
);
8083
});
8184

8285
it.each(['8', '23.x', '23.0.1.1', '<24'])(
8386
"rejects Temurin java-package 'jdk+jmods' for version %s",
84-
version => {
85-
expect(() =>
87+
async version => {
88+
await expect(
8689
getJavaDistribution(
8790
JavaDistribution.Temurin,
8891
installerOptions('jdk+jmods', version)
8992
)
90-
).toThrow(
93+
).rejects.toThrow(
9194
`Java package 'jdk+jmods' is not supported for distribution 'temurin'. Supported package types: jdk, jre, jdk+jmods. Package 'jdk+jmods' requires Java 24 or later; requested version '${version}'.`
9295
);
9396
}
9497
);
9598

9699
it.each(['24', '24.0.1.1', '25-ea', '>=21', 'latest'])(
97100
"accepts Temurin java-package 'jdk+jmods' for version %s",
98-
version => {
101+
async version => {
99102
expect(
100-
getJavaDistribution(
103+
await getJavaDistribution(
101104
JavaDistribution.Temurin,
102105
installerOptions('jdk+jmods', version)
103106
)
104107
).not.toBeNull();
105108
}
106109
);
107110

108-
it('preserves unsupported distribution handling', () => {
111+
it('preserves unsupported distribution handling', async () => {
109112
expect(
110-
getJavaDistribution(
113+
await getJavaDistribution(
111114
'not-a-distribution',
112115
installerOptions('not-a-package')
113116
)
@@ -118,17 +121,17 @@ describe('getJavaDistribution', () => {
118121
['amd64', 'x64'],
119122
['ia32', 'x86'],
120123
['arm64', 'aarch64']
121-
])('passes normalized architecture %s as %s', (input, expected) => {
122-
const normalized = getJavaDistribution(JavaDistribution.JdkFile, {
124+
])('passes normalized architecture %s as %s', async (input, expected) => {
125+
const normalized = await getJavaDistribution(JavaDistribution.JdkFile, {
123126
...installerOptions('jdk'),
124127
architecture: input
125128
});
126129

127130
expect(normalized!['architecture']).toBe(expected);
128131
});
129132

130-
it('uses the runner architecture when the input is empty', () => {
131-
const distribution = getJavaDistribution(JavaDistribution.Temurin, {
133+
it('uses the runner architecture when the input is empty', async () => {
134+
const distribution = await getJavaDistribution(JavaDistribution.Temurin, {
132135
...installerOptions('jdk'),
133136
architecture: ''
134137
});
@@ -137,12 +140,12 @@ describe('getJavaDistribution', () => {
137140
expect(distribution!['architecture']).toBe(expected);
138141
});
139142

140-
it('rejects an unsupported combination before creating an HTTP client', () => {
141-
expect(() =>
143+
it('rejects an unsupported combination before creating an HTTP client', async () => {
144+
await expect(
142145
getJavaDistribution(JavaDistribution.Oracle, {
143146
...installerOptions('jdk'),
144147
architecture: 'x86'
145148
})
146-
).toThrow(/does not support operating system/);
149+
).rejects.toThrow(/does not support operating system/);
147150
});
148151
});

‎__tests__/distributors/graalvm-installer.test.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1403,8 +1403,11 @@ describe('distribution factory', () => {
14031403
checkLatest: false
14041404
};
14051405

1406-
it('should map graalvm-community to the community installer', () => {
1407-
const community = getJavaDistribution('graalvm-community', defaultOptions);
1406+
it('should map graalvm-community to the community installer', async () => {
1407+
const community = await getJavaDistribution(
1408+
'graalvm-community',
1409+
defaultOptions
1410+
);
14081411

14091412
expect(community).toBeInstanceOf(GraalVMCommunityDistribution);
14101413
});

‎__tests__/distributors/openjdk-installer.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,8 @@ describe('OpenJdkDistribution', () => {
233233
expect(windowsRelease[0].url.endsWith('.tar.gz')).toBe(true);
234234
});
235235

236-
it('is registered in the distribution factory', () => {
237-
const distribution = getJavaDistribution('oracle-openjdk', {
236+
it('is registered in the distribution factory', async () => {
237+
const distribution = await getJavaDistribution('oracle-openjdk', {
238238
version: '26',
239239
architecture: 'x64',
240240
packageType: 'jdk',
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import {jest, describe, it, expect, beforeEach} from '@jest/globals';
2+
3+
jest.unstable_mockModule('@actions/core', () => ({
4+
info: jest.fn(),
5+
warning: jest.fn(),
6+
debug: jest.fn(),
7+
error: jest.fn(),
8+
notice: jest.fn(),
9+
setFailed: jest.fn(),
10+
setOutput: jest.fn(),
11+
getInput: jest.fn(),
12+
getBooleanInput: jest.fn(),
13+
getMultilineInput: jest.fn(),
14+
addPath: jest.fn(),
15+
exportVariable: jest.fn(),
16+
saveState: jest.fn(),
17+
getState: jest.fn(),
18+
setSecret: jest.fn(),
19+
isDebug: jest.fn(() => false),
20+
startGroup: jest.fn(),
21+
endGroup: jest.fn(),
22+
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
23+
toPlatformPath: jest.fn((value: string) => value),
24+
toWin32Path: jest.fn((value: string) => value),
25+
toPosixPath: jest.fn((value: string) => value)
26+
}));
27+
28+
jest.unstable_mockModule('fs', () => ({
29+
default: {
30+
readFileSync: jest.fn()
31+
}
32+
}));
33+
34+
jest.unstable_mockModule('../src/util.js', () => ({
35+
getBooleanInput: jest.fn(),
36+
getVersionFromFileContent: jest.fn()
37+
}));
38+
39+
jest.unstable_mockModule('../src/toolchains.js', () => ({
40+
validateToolchainIds: jest.fn(),
41+
configureToolchains: jest.fn()
42+
}));
43+
44+
jest.unstable_mockModule(
45+
'../src/distributions/distribution-factory.js',
46+
() => ({
47+
getJavaDistribution: jest.fn()
48+
})
49+
);
50+
51+
jest.unstable_mockModule('../src/auth.js', () => ({
52+
configureAuthentication: jest.fn()
53+
}));
54+
55+
jest.unstable_mockModule('../src/maven-args.js', () => ({
56+
configureMavenArgs: jest.fn()
57+
}));
58+
59+
jest.unstable_mockModule('../src/problem-matcher.js', () => ({
60+
configureProblemMatcher: jest.fn()
61+
}));
62+
63+
// These modules should never be imported when `cache` input is empty.
64+
jest.unstable_mockModule('../src/cache-feature.js', () => {
65+
throw new Error('cache-feature module should not be loaded');
66+
});
67+
jest.unstable_mockModule('../src/cache.js', () => {
68+
throw new Error('cache module should not be loaded');
69+
});
70+
71+
const core = await import('@actions/core');
72+
const util = await import('../src/util.js');
73+
const toolchains = await import('../src/toolchains.js');
74+
const factory = await import('../src/distributions/distribution-factory.js');
75+
const {run} = await import('../src/setup-java.js');
76+
77+
describe('setup-java conditional module loading', () => {
78+
const inputs = new Map<string, string>();
79+
const multilineInputs = new Map<string, string[]>();
80+
const booleanInputs = new Map<string, boolean>();
81+
82+
beforeEach(() => {
83+
jest.resetAllMocks();
84+
inputs.clear();
85+
multilineInputs.clear();
86+
booleanInputs.clear();
87+
88+
(core.getInput as jest.Mock).mockImplementation((name: unknown) => {
89+
return inputs.get(name as string) ?? '';
90+
});
91+
(core.getMultilineInput as jest.Mock).mockImplementation(
92+
(name: unknown) => {
93+
return multilineInputs.get(name as string) ?? [];
94+
}
95+
);
96+
(util.getBooleanInput as jest.Mock).mockImplementation(
97+
(name: unknown, defaultValue: unknown) => {
98+
return booleanInputs.get(name as string) ?? defaultValue;
99+
}
100+
);
101+
(toolchains.configureToolchains as jest.Mock).mockResolvedValue(undefined);
102+
});
103+
104+
it('does not import cache modules when cache input is not provided', async () => {
105+
inputs.set('distribution', 'temurin');
106+
multilineInputs.set('java-version', ['21']);
107+
(factory.getJavaDistribution as jest.Mock).mockResolvedValue({
108+
setupJava: jest.fn(async () => ({
109+
version: '21.0.4+7',
110+
path: '/opt/java/21'
111+
}))
112+
});
113+
114+
await run();
115+
116+
expect(core.setFailed).not.toHaveBeenCalled();
117+
});
118+
});

0 commit comments

Comments
 (0)