Merge main into Alpine artifact selection · brunoborges/setup-java@1f0773d · GitHub
Skip to content

Commit 1f0773d

Browse files
committed
Merge main into Alpine artifact selection
PR actions#1210 landed the shared `isAlpineLinux()` helper, so this branch drops its own identical copy and imports main's. `platform-types.ts` and `java-platform-contract.test.ts` are now untouched by this branch; the duplicated `isAlpineLinux` test block was byte-identical to main's and was removed. Rebuilt dist/ to resolve the bundle conflict. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 74248bb0-72af-41d8-b85d-b0f5836e68db
2 parents d756d46 + d0e61fe commit 1f0773d

17 files changed

Lines changed: 333 additions & 92 deletions

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

Lines changed: 4 additions & 0 deletions

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

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,24 @@ const core = await import('@actions/core');
4747
const {JetBrainsDistribution} =
4848
await import('../../src/distributions/jetbrains/installer.js');
4949
const {RetryingHttpClient} = await import('../../src/retrying-http-client.js');
50+
const {MAX_PAGINATION_PAGES} = await import('../../src/util.js');
51+
52+
const JETBRAINS_RELEASES_URL =
53+
'https://api.github.com/repos/JetBrains/JetBrainsRuntime/releases?per_page=100';
54+
55+
function release(tagName: string, prerelease: boolean) {
56+
return {
57+
tag_name: tagName,
58+
name: tagName,
59+
prerelease
60+
};
61+
}
62+
63+
function nextPageHeader(page: number) {
64+
return {
65+
link: `<${JETBRAINS_RELEASES_URL}&page=${page}>; rel="next"`
66+
};
67+
}
5068

5169
function response(
5270
statusCode: number,
@@ -110,6 +128,138 @@ describe('getAvailableVersions', () => {
110128
expect(availableVersions.length).toBe(length);
111129
}, 10_000);
112130

131+
it('continues a stable request after an all-prerelease page', async () => {
132+
jest.spyOn(HttpClient.prototype, 'head').mockResolvedValue({
133+
message: {statusCode: 200}
134+
} as any);
135+
spyHttpClient
136+
.mockResolvedValueOnce({
137+
statusCode: 200,
138+
headers: nextPageHeader(2),
139+
result: [release('jbr-release-26.0.0b1.1', true)]
140+
})
141+
.mockResolvedValueOnce({
142+
statusCode: 200,
143+
headers: {},
144+
result: [release('jbr-release-21.0.11b1163.116', false)]
145+
});
146+
const distribution = new JetBrainsDistribution({
147+
version: '21',
148+
architecture: 'x64',
149+
packageType: 'jdk',
150+
checkLatest: false
151+
});
152+
153+
const availableVersions = await distribution['getAvailableVersions']();
154+
155+
expect(availableVersions.map(version => version.tag_name)).toContain(
156+
'jbr-release-21.0.11b1163.116'
157+
);
158+
expect(availableVersions.map(version => version.tag_name)).not.toContain(
159+
'jbr-release-26.0.0b1.1'
160+
);
161+
expect(spyHttpClient).toHaveBeenCalledTimes(2);
162+
});
163+
164+
it('continues an EA request after an all-stable page', async () => {
165+
jest.spyOn(HttpClient.prototype, 'head').mockResolvedValue({
166+
message: {statusCode: 200}
167+
} as any);
168+
spyHttpClient
169+
.mockResolvedValueOnce({
170+
statusCode: 200,
171+
headers: nextPageHeader(2),
172+
result: [release('jbr-release-21.0.11b1163.116', false)]
173+
})
174+
.mockResolvedValueOnce({
175+
statusCode: 200,
176+
headers: {},
177+
result: [release('jbr-release-26.0.0b1.1', true)]
178+
});
179+
const distribution = new JetBrainsDistribution({
180+
version: '26-ea',
181+
architecture: 'x64',
182+
packageType: 'jdk',
183+
checkLatest: false
184+
});
185+
186+
const availableVersions = await distribution['getAvailableVersions']();
187+
188+
expect(availableVersions.map(version => version.tag_name)).toEqual([
189+
'jbr-release-26.0.0b1.1'
190+
]);
191+
expect(spyHttpClient).toHaveBeenCalledTimes(2);
192+
});
193+
194+
it('stops pagination when a raw GitHub page is empty', async () => {
195+
spyHttpClient
196+
.mockResolvedValueOnce({
197+
statusCode: 200,
198+
headers: nextPageHeader(2),
199+
result: [release('jbr-release-21.0.11b1163.116', false)]
200+
})
201+
.mockResolvedValueOnce({
202+
statusCode: 200,
203+
headers: nextPageHeader(3),
204+
result: []
205+
});
206+
const distribution = new JetBrainsDistribution({
207+
version: '26-ea',
208+
architecture: 'x64',
209+
packageType: 'jdk',
210+
checkLatest: false
211+
});
212+
213+
await distribution['getAvailableVersions']();
214+
215+
expect(spyHttpClient).toHaveBeenCalledTimes(2);
216+
});
217+
218+
it('stops at the pagination safeguard', async () => {
219+
spyHttpClient.mockResolvedValue({
220+
statusCode: 200,
221+
headers: nextPageHeader(2),
222+
result: [release('jbr-release-21.0.11b1163.116', false)]
223+
});
224+
const distribution = new JetBrainsDistribution({
225+
version: '26-ea',
226+
architecture: 'x64',
227+
packageType: 'jdk',
228+
checkLatest: false
229+
});
230+
231+
const availableVersions = await distribution['getAvailableVersions']();
232+
233+
expect(availableVersions).toEqual([]);
234+
expect(spyHttpClient).toHaveBeenCalledTimes(MAX_PAGINATION_PAGES);
235+
expect(core.warning).toHaveBeenCalledWith(
236+
`Reached pagination safeguard limit (${MAX_PAGINATION_PAGES} pages) while listing JetBrains Runtime releases.`
237+
);
238+
});
239+
240+
it('ignores pagination links with an unexpected origin', async () => {
241+
spyHttpClient.mockResolvedValueOnce({
242+
statusCode: 200,
243+
headers: {
244+
link: '<https://example.com/releases?page=2>; rel="next"'
245+
},
246+
result: [release('jbr-release-21.0.11b1163.116', false)]
247+
});
248+
const distribution = new JetBrainsDistribution({
249+
version: '26-ea',
250+
architecture: 'x64',
251+
packageType: 'jdk',
252+
checkLatest: false
253+
});
254+
255+
await distribution['getAvailableVersions']();
256+
257+
expect(spyHttpClient).toHaveBeenCalledTimes(1);
258+
expect(core.warning).toHaveBeenCalledWith(
259+
'Ignoring pagination link with unexpected origin: https://example.com/releases?page=2'
260+
);
261+
});
262+
113263
it('retries a GitHub rate limit using Retry-After', async () => {
114264
spyHttpClient.mockRestore();
115265
const sleep = jest.fn(async () => undefined);

‎__tests__/java-platform-contract.test.ts‎

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from 'fs';
22
import path from 'path';
33
import {
4+
getJavaPlatformIdentity,
45
isAlpineLinux,
56
JAVA_PLATFORM_CAPABILITIES,
67
normalizeArchitecture,
@@ -29,6 +30,38 @@ describe('Java platform capabilities', () => {
2930
expect(normalizeArchitecture(input)).toBe(expected);
3031
});
3132

33+
it.each([
34+
['linux', false, 'linux-glibc'],
35+
['linux', true, 'linux-musl'],
36+
['darwin', false, 'macos'],
37+
['win32', false, 'windows'],
38+
// Exercises the normalizePlatform alias path and the `?? platform`
39+
// fallback for a platform that has no Java alias.
40+
['sunos', false, 'solaris'],
41+
['aix', false, 'aix']
42+
] as const)(
43+
'identifies %s with Alpine release %s as %s',
44+
(platform, alpineReleaseExists, expected) => {
45+
expect(getJavaPlatformIdentity(platform, alpineReleaseExists)).toBe(
46+
expected
47+
);
48+
}
49+
);
50+
51+
// The platform check has to short-circuit before the filesystem probe, so a
52+
// stray /etc/alpine-release can never make a non-Linux runner look like musl.
53+
it.each([
54+
['linux', true, true],
55+
['linux', false, false],
56+
['darwin', true, false],
57+
['win32', true, false]
58+
] as const)(
59+
'treats %s with Alpine release %s as Alpine: %s',
60+
(platform, alpineReleaseExists, expected) => {
61+
expect(isAlpineLinux(platform, alpineReleaseExists)).toBe(expected);
62+
}
63+
);
64+
3265
it('uses the normalized architecture for validation', () => {
3366
expect(validateJavaPlatform('microsoft', 'linux', 'arm64', '25')).toBe(
3467
'aarch64'
@@ -123,19 +156,3 @@ describe('Java platform capabilities', () => {
123156
}
124157
);
125158
});
126-
127-
describe('isAlpineLinux', () => {
128-
// The platform check has to short-circuit before the filesystem probe, so a
129-
// stray /etc/alpine-release can never make a non-Linux runner look like musl.
130-
it.each([
131-
['linux', true, true],
132-
['linux', false, false],
133-
['darwin', true, false],
134-
['win32', true, false]
135-
] as const)(
136-
'treats %s with Alpine release %s as Alpine: %s',
137-
(platform, alpineReleaseExists, expected) => {
138-
expect(isAlpineLinux(platform, alpineReleaseExists)).toBe(expected);
139-
}
140-
);
141-
});

‎__tests__/jdk-resolution-cache.test.ts‎

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const {restoreJdkResolution, registerJdkResolution, saveJdkResolutionCaches} =
2525
const request = {
2626
distribution: 'Temurin-Hotspot',
2727
packageType: 'jdk',
28+
platform: 'linux-glibc',
2829
architecture: 'x64',
2930
versionSpec: '21',
3031
stable: true
@@ -102,10 +103,24 @@ describe('JDK resolution cache', () => {
102103
expect(paths[0]).not.toContain(bucket());
103104
expect(primaryKey).toBe(`${restoreKeys[0]}${bucket()}`);
104105
expect(restoreKeys[0]).toMatch(
105-
/^setup-java-jdkres-v1-Linux-x64-[0-9a-f]{64}-$/
106+
/^setup-java-jdkres-v2-Linux-x64-[0-9a-f]{64}-$/
106107
);
107108
});
108109

110+
it('separates glibc and musl Linux resolutions', async () => {
111+
createRunnerTemp();
112+
await restoreJdkResolution(request);
113+
const [glibcPaths, glibcKey] = jest.mocked(cache.restoreCache).mock
114+
.calls[0] as [string[], string];
115+
116+
await restoreJdkResolution({...request, platform: 'linux-musl'});
117+
const [muslPaths, muslKey] = jest.mocked(cache.restoreCache).mock
118+
.calls[1] as [string[], string];
119+
120+
expect(muslKey).not.toBe(glibcKey);
121+
expect(muslPaths).not.toEqual(glibcPaths);
122+
});
123+
109124
it('holds the key steady for a week and then rolls it', async () => {
110125
createRunnerTemp();
111126
const nowSpy = jest.spyOn(Date, 'now');
@@ -129,7 +144,7 @@ describe('JDK resolution cache', () => {
129144

130145
it('reports a hit on the current bucket as fresh', async () => {
131146
createRunnerTemp();
132-
const key = `setup-java-jdkres-v1-Linux-x64-${'0'.repeat(64)}-${bucket()}`;
147+
const key = `setup-java-jdkres-v2-Linux-x64-${'0'.repeat(64)}-${bucket()}`;
133148
restoreWith(JSON.stringify(release), key);
134149

135150
// The key the module computes is the one it passes to restoreCache, so
@@ -152,7 +167,7 @@ describe('JDK resolution cache', () => {
152167

153168
it('reports a hit on an older bucket as stale', async () => {
154169
createRunnerTemp();
155-
restoreWith(JSON.stringify(release), 'setup-java-jdkres-v1-old');
170+
restoreWith(JSON.stringify(release), 'setup-java-jdkres-v2-old');
156171

157172
const restored = await restoreJdkResolution(request);
158173
expect(restored?.fresh).toBe(false);
@@ -233,7 +248,7 @@ describe('JDK resolution cache', () => {
233248
]
234249
])('rejects an entry with %s', async (_name, contents) => {
235250
createRunnerTemp();
236-
restoreWith(contents, 'setup-java-jdkres-v1-old');
251+
restoreWith(contents, 'setup-java-jdkres-v2-old');
237252

238253
await expect(restoreJdkResolution(request)).resolves.toBeUndefined();
239254
});
@@ -251,7 +266,7 @@ describe('JDK resolution cache', () => {
251266
},
252267
floating: true
253268
};
254-
restoreWith(JSON.stringify(full), 'setup-java-jdkres-v1-old');
269+
restoreWith(JSON.stringify(full), 'setup-java-jdkres-v2-old');
255270

256271
const restored = await restoreJdkResolution(request);
257272
expect(restored?.release).toEqual(full);
@@ -261,7 +276,7 @@ describe('JDK resolution cache', () => {
261276
createRunnerTemp();
262277
restoreWith(
263278
JSON.stringify({...release, evil: 'payload'}),
264-
'setup-java-jdkres-v1-old'
279+
'setup-java-jdkres-v2-old'
265280
);
266281

267282
const restored = await restoreJdkResolution(request);
@@ -332,7 +347,7 @@ describe('JDK resolution cache', () => {
332347
const stateFor = (cachePath: string) =>
333348
JSON.stringify([
334349
{
335-
key: 'setup-java-jdkres-v1-key',
350+
key: 'setup-java-jdkres-v2-key',
336351
path: cachePath,
337352
release: JSON.stringify(release)
338353
}
@@ -350,7 +365,7 @@ describe('JDK resolution cache', () => {
350365
await saveJdkResolutionCaches();
351366
expect(cache.saveCache).toHaveBeenCalledWith(
352367
[root],
353-
'setup-java-jdkres-v1-key'
368+
'setup-java-jdkres-v2-key'
354369
);
355370
});
356371

‎dist/cleanup/348.index.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const modules = {
2323

2424

2525
const STATE_JDK_RESOLUTIONS = 'jdk-resolutions';
26-
const JDK_RESOLUTION_KEY_VERSION = 1;
26+
const JDK_RESOLUTION_KEY_VERSION = 2;
2727
const RESOLUTION_DIRECTORY = 'setup-java-jdk-resolution';
2828
const RESOLUTION_FILE_NAME = 'release.json';
2929
const pendingResolutions = (/* unused pure expression or super */ null && ([]));
@@ -146,6 +146,7 @@ function getResolutionIdentity(request) {
146146
runnerOs: getRunnerOs(),
147147
distribution: request.distribution.toLowerCase(),
148148
packageType: request.packageType.toLowerCase(),
149+
platform: request.platform.toLowerCase(),
149150
architecture: request.architecture.toLowerCase(),
150151
versionSpec: request.versionSpec,
151152
stable: request.stable,

‎dist/setup/242.index.js‎

Lines changed: 2 additions & 0 deletions

0 commit comments

Comments
 (0)