Merge branch 'actions:main' into main

This commit is contained in:
Sean Proctor 2026-06-17 22:51:14 +02:00 committed by GitHub
commit 7111600e00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 9028 additions and 6716 deletions

View File

@ -10,5 +10,9 @@ on:
jobs: jobs:
call-codeQL-analysis: call-codeQL-analysis:
permissions:
actions: read
contents: read
security-events: write
name: CodeQL analysis name: CodeQL analysis
uses: actions/reusable-workflows/.github/workflows/codeql-analysis.yml@main uses: actions/reusable-workflows/.github/workflows/codeql-analysis.yml@main

View File

@ -86,6 +86,32 @@ jobs:
run: bash __tests__/verify-java.sh "${{ matrix.version }}" "${{ steps.setup-java.outputs.path }}" run: bash __tests__/verify-java.sh "${{ matrix.version }}" "${{ steps.setup-java.outputs.path }}"
shell: bash shell: bash
setup-java-alpine-linux:
name: ${{ matrix.distribution }} ${{ matrix.version }} (jdk-x64) - alpine-linux - ${{ matrix.os }}
runs-on: ${{ matrix.os }}
container:
image: alpine:latest
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
distribution: ['temurin', 'sapmachine']
version: ['21', '17']
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install bash
run: apk add --no-cache bash
- name: setup-java
uses: ./
id: setup-java
with:
java-version: ${{ matrix.version }}
distribution: ${{ matrix.distribution }}
- name: Verify Java
run: bash __tests__/verify-java.sh "${{ matrix.version }}" "${{ steps.setup-java.outputs.path }}"
shell: bash
setup-java-major-minor-versions: setup-java-major-minor-versions:
name: ${{ matrix.distribution }} ${{ matrix.version }} (jdk-x64) - ${{ matrix.os }} name: ${{ matrix.distribution }} ${{ matrix.version }} (jdk-x64) - ${{ matrix.os }}
needs: setup-java-major-versions needs: setup-java-major-versions

BIN
.licenses/npm/@nodable/entities.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/anynum.dep.yml generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
.licenses/npm/ms.dep.yml generated

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
.licenses/npm/xml-naming.dep.yml generated Normal file

Binary file not shown.

View File

@ -131,6 +131,8 @@ Currently, the following distributions are supported:
**NOTE:** Oracle JDK 17 licensing varies by patch level. As shown on the [JDK 17 Archive](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) (versions up to 17.0.12 are under the [NFTC](https://www.oracle.com/downloads/licenses/no-fee-license.html) license) and the [JDK 17.0.13+ Archive](https://www.oracle.com/java/technologies/javase/jdk17-0-13-later-archive-downloads.html) (versions 17.0.13 and later are under the [OTN](https://www.oracle.com/downloads/licenses/javase-license1.html) license). To stay on the free NFTC license, use `distribution: 'oracle'` with `java-version: '17.0.12'` (or earlier) instead of the floating `'17'`. Alternatively, upgrade to Oracle JDK 21+, which remains under the NFTC license. **NOTE:** Oracle JDK 17 licensing varies by patch level. As shown on the [JDK 17 Archive](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) (versions up to 17.0.12 are under the [NFTC](https://www.oracle.com/downloads/licenses/no-fee-license.html) license) and the [JDK 17.0.13+ Archive](https://www.oracle.com/java/technologies/javase/jdk17-0-13-later-archive-downloads.html) (versions 17.0.13 and later are under the [OTN](https://www.oracle.com/downloads/licenses/javase-license1.html) license). To stay on the free NFTC license, use `distribution: 'oracle'` with `java-version: '17.0.12'` (or earlier) instead of the floating `'17'`. Alternatively, upgrade to Oracle JDK 21+, which remains under the NFTC license.
**NOTE:** On Ubuntu runners, commands executed via `sudo` do not inherit the `JAVA_HOME` and `PATH` set by `setup-java` and will fall back to the runner image's system-default JDK.
### Caching packages dependencies ### Caching packages dependencies
The action has a built-in functionality for caching and restoring dependencies. It uses [toolkit/cache](https://github.com/actions/toolkit/tree/main/packages/cache) under hood for caching dependencies but requires less configuration settings. Supported package managers are gradle, maven and sbt. The format of the used cache key is `setup-java-${{ platform }}-${{ packageManager }}-${{ fileHash }}`, where the hash is based on the following files: The action has a built-in functionality for caching and restoring dependencies. It uses [toolkit/cache](https://github.com/actions/toolkit/tree/main/packages/cache) under hood for caching dependencies but requires less configuration settings. Supported package managers are gradle, maven and sbt. The format of the used cache key is `setup-java-${{ platform }}-${{ packageManager }}-${{ fileHash }}`, where the hash is based on the following files:

View File

@ -4,6 +4,7 @@ import {
AdoptDistribution, AdoptDistribution,
AdoptImplementation AdoptImplementation
} from '../../src/distributions/adopt/installer'; } from '../../src/distributions/adopt/installer';
import {TemurinDistribution} from '../../src/distributions/temurin/installer';
import {JavaInstallerOptions} from '../../src/distributions/base-models'; import {JavaInstallerOptions} from '../../src/distributions/base-models';
import os from 'os'; import os from 'os';
@ -14,6 +15,7 @@ import * as core from '@actions/core';
describe('getAvailableVersions', () => { describe('getAvailableVersions', () => {
let spyHttpClient: jest.SpyInstance; let spyHttpClient: jest.SpyInstance;
let spyCoreError: jest.SpyInstance; let spyCoreError: jest.SpyInstance;
let spyCoreWarning: jest.SpyInstance;
beforeEach(() => { beforeEach(() => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
@ -26,6 +28,8 @@ describe('getAvailableVersions', () => {
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = jest.spyOn(core, 'error');
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
spyCoreWarning = jest.spyOn(core, 'warning');
spyCoreWarning.mockImplementation(() => {});
}); });
afterEach(() => { afterEach(() => {
@ -136,22 +140,19 @@ describe('getAvailableVersions', () => {
); );
it('load available versions', async () => { it('load available versions', async () => {
const nextPageUrl =
'https://api.adoptopenjdk.net/v3/assets/version/%5B1.0,100.0%5D?page=1&page_size=20';
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
spyHttpClient spyHttpClient
.mockReturnValueOnce({ .mockReturnValueOnce({
statusCode: 200, statusCode: 200,
headers: {}, headers: {link: `<${nextPageUrl}>; rel="next"`},
result: manifestData as any result: manifestData as any
}) })
.mockReturnValueOnce({ .mockReturnValueOnce({
statusCode: 200, statusCode: 200,
headers: {}, headers: {},
result: manifestData as any result: manifestData as any
})
.mockReturnValueOnce({
statusCode: 200,
headers: {},
result: []
}); });
const distribution = new AdoptDistribution( const distribution = new AdoptDistribution(
@ -166,6 +167,34 @@ describe('getAvailableVersions', () => {
const availableVersions = await distribution['getAvailableVersions'](); const availableVersions = await distribution['getAvailableVersions']();
expect(availableVersions).not.toBeNull(); expect(availableVersions).not.toBeNull();
expect(availableVersions.length).toBe(manifestData.length * 2); expect(availableVersions.length).toBe(manifestData.length * 2);
expect(spyHttpClient).toHaveBeenNthCalledWith(2, nextPageUrl);
});
it('stops pagination after 1000 pages as a safeguard', async () => {
const nextPageUrl =
'https://api.adoptopenjdk.net/v3/assets/version/%5B1.0,100.0%5D?page=2&page_size=20';
spyHttpClient.mockReturnValue({
statusCode: 200,
headers: {link: `<${nextPageUrl}>; rel="next"`},
result: [{version_data: {semver: '17.0.1'}, binaries: []}] as any
});
const distribution = new AdoptDistribution(
{
version: '11',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
},
AdoptImplementation.Hotspot
);
await distribution['getAvailableVersions']();
expect(spyHttpClient).toHaveBeenCalledTimes(1000);
expect(spyCoreWarning).toHaveBeenCalledWith(
expect.stringContaining('Reached pagination safeguard limit (1000 pages)')
);
}); });
it.each([ it.each([
@ -228,6 +257,38 @@ describe('getAvailableVersions', () => {
}); });
describe('findPackageForDownload', () => { describe('findPackageForDownload', () => {
it('returns Temurin result and does not query Adopt API when Temurin succeeds', async () => {
const temurinRelease = {
version: '11.0.31+11',
url: 'https://example.test/temurin-11.tar.gz'
};
const temurinFindPackageForDownload = jest
.fn()
.mockResolvedValue(temurinRelease);
const temurinDistribution = {
findPackageForDownload: temurinFindPackageForDownload
} as unknown as TemurinDistribution;
const distribution = new AdoptDistribution(
{
version: '11',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
},
AdoptImplementation.Hotspot,
temurinDistribution
);
const adoptLookupSpy = jest.fn();
distribution['getAvailableVersions'] = adoptLookupSpy;
const resolvedVersion = await distribution['findPackageForDownload']('11');
expect(resolvedVersion).toEqual(temurinRelease);
expect(temurinFindPackageForDownload).toHaveBeenCalledWith('11');
expect(adoptLookupSpy).not.toHaveBeenCalled();
});
it.each([ it.each([
['9', '9.0.7+10'], ['9', '9.0.7+10'],
['15', '15.0.2+7'], ['15', '15.0.2+7'],
@ -250,6 +311,11 @@ describe('findPackageForDownload', () => {
}, },
AdoptImplementation.Hotspot AdoptImplementation.Hotspot
); );
// Mock Temurin to fail so fallback to AdoptOpenJDK is tested
distribution['temurinDistribution']!['findPackageForDownload'] =
async () => {
throw new Error('No matching version found for SemVer');
};
distribution['getAvailableVersions'] = async () => manifestData as any; distribution['getAvailableVersions'] = async () => manifestData as any;
const resolvedVersion = await distribution['findPackageForDownload'](input); const resolvedVersion = await distribution['findPackageForDownload'](input);
expect(resolvedVersion.version).toBe(expected); expect(resolvedVersion.version).toBe(expected);
@ -265,6 +331,11 @@ describe('findPackageForDownload', () => {
}, },
AdoptImplementation.Hotspot AdoptImplementation.Hotspot
); );
// Mock Temurin to fail so fallback to AdoptOpenJDK is tested
distribution['temurinDistribution']!['findPackageForDownload'] =
async () => {
throw new Error('No matching version found for SemVer');
};
distribution['getAvailableVersions'] = async () => manifestData as any; distribution['getAvailableVersions'] = async () => manifestData as any;
await expect( await expect(
distribution['findPackageForDownload']('9.0.8') distribution['findPackageForDownload']('9.0.8')
@ -281,6 +352,11 @@ describe('findPackageForDownload', () => {
}, },
AdoptImplementation.Hotspot AdoptImplementation.Hotspot
); );
// Mock Temurin to fail so fallback to AdoptOpenJDK is tested
distribution['temurinDistribution']!['findPackageForDownload'] =
async () => {
throw new Error('No matching version found for SemVer');
};
distribution['getAvailableVersions'] = async () => manifestData as any; distribution['getAvailableVersions'] = async () => manifestData as any;
await expect(distribution['findPackageForDownload']('7.x')).rejects.toThrow( await expect(distribution['findPackageForDownload']('7.x')).rejects.toThrow(
/No matching version found for SemVer */ /No matching version found for SemVer */
@ -297,6 +373,11 @@ describe('findPackageForDownload', () => {
}, },
AdoptImplementation.Hotspot AdoptImplementation.Hotspot
); );
// Mock Temurin to fail so fallback to AdoptOpenJDK is tested
distribution['temurinDistribution']!['findPackageForDownload'] =
async () => {
throw new Error('No matching version found for SemVer');
};
distribution['getAvailableVersions'] = async () => []; distribution['getAvailableVersions'] = async () => [];
await expect(distribution['findPackageForDownload']('11')).rejects.toThrow( await expect(distribution['findPackageForDownload']('11')).rejects.toThrow(
/No matching version found for SemVer */ /No matching version found for SemVer */

View File

@ -9,6 +9,7 @@ import * as core from '@actions/core';
describe('getAvailableVersions', () => { describe('getAvailableVersions', () => {
let spyHttpClient: jest.SpyInstance; let spyHttpClient: jest.SpyInstance;
let spyCoreError: jest.SpyInstance; let spyCoreError: jest.SpyInstance;
let spyCoreWarning: jest.SpyInstance;
beforeEach(() => { beforeEach(() => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
@ -20,6 +21,8 @@ describe('getAvailableVersions', () => {
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = jest.spyOn(core, 'error');
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
spyCoreWarning = jest.spyOn(core, 'warning');
spyCoreWarning.mockImplementation(() => {});
}); });
afterEach(() => { afterEach(() => {
@ -82,22 +85,19 @@ describe('getAvailableVersions', () => {
); );
it('load available versions', async () => { it('load available versions', async () => {
const nextPageUrl =
'https://api.adoptopenjdk.net/v3/assets/version/%5B1.0,100.0%5D?page=1&page_size=20';
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
spyHttpClient spyHttpClient
.mockReturnValueOnce({ .mockReturnValueOnce({
statusCode: 200, statusCode: 200,
headers: {}, headers: {link: `<${nextPageUrl}>; rel="next"`},
result: manifestData as any result: manifestData as any
}) })
.mockReturnValueOnce({ .mockReturnValueOnce({
statusCode: 200, statusCode: 200,
headers: {}, headers: {},
result: manifestData as any result: manifestData as any
})
.mockReturnValueOnce({
statusCode: 200,
headers: {},
result: []
}); });
const distribution = new SemeruDistribution({ const distribution = new SemeruDistribution({
@ -109,6 +109,31 @@ describe('getAvailableVersions', () => {
const availableVersions = await distribution['getAvailableVersions'](); const availableVersions = await distribution['getAvailableVersions']();
expect(availableVersions).not.toBeNull(); expect(availableVersions).not.toBeNull();
expect(availableVersions.length).toBe(manifestData.length * 2); expect(availableVersions.length).toBe(manifestData.length * 2);
expect(spyHttpClient).toHaveBeenNthCalledWith(2, nextPageUrl);
});
it('stops pagination after 1000 pages as a safeguard', async () => {
const nextPageUrl =
'https://api.adoptopenjdk.net/v3/assets/version/%5B1.0,100.0%5D?page=2&page_size=20';
spyHttpClient.mockReturnValue({
statusCode: 200,
headers: {link: `<${nextPageUrl}>; rel="next"`},
result: [{version_data: {semver: '17.0.1'}, binaries: []}] as any
});
const distribution = new SemeruDistribution({
version: '8',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
});
await distribution['getAvailableVersions']();
expect(spyHttpClient).toHaveBeenCalledTimes(1000);
expect(spyCoreWarning).toHaveBeenCalledWith(
expect.stringContaining('Reached pagination safeguard limit (1000 pages)')
);
}); });
it.each([ it.each([

View File

@ -12,6 +12,7 @@ import * as core from '@actions/core';
describe('getAvailableVersions', () => { describe('getAvailableVersions', () => {
let spyHttpClient: jest.SpyInstance; let spyHttpClient: jest.SpyInstance;
let spyCoreError: jest.SpyInstance; let spyCoreError: jest.SpyInstance;
let spyCoreWarning: jest.SpyInstance;
beforeEach(() => { beforeEach(() => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
@ -23,6 +24,8 @@ describe('getAvailableVersions', () => {
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = jest.spyOn(core, 'error');
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
spyCoreWarning = jest.spyOn(core, 'warning');
spyCoreWarning.mockImplementation(() => {});
}); });
afterEach(() => { afterEach(() => {
@ -93,22 +96,19 @@ describe('getAvailableVersions', () => {
); );
it('load available versions', async () => { it('load available versions', async () => {
const nextPageUrl =
'https://api.adoptium.net/v3/assets/version/%5B1.0,100.0%5D?page=1&page_size=20';
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
spyHttpClient spyHttpClient
.mockReturnValueOnce({ .mockReturnValueOnce({
statusCode: 200, statusCode: 200,
headers: {}, headers: {link: `<${nextPageUrl}>; rel="next"`},
result: manifestData as any result: manifestData as any
}) })
.mockReturnValueOnce({ .mockReturnValueOnce({
statusCode: 200, statusCode: 200,
headers: {}, headers: {},
result: manifestData as any result: manifestData as any
})
.mockReturnValueOnce({
statusCode: 200,
headers: {},
result: []
}); });
const distribution = new TemurinDistribution( const distribution = new TemurinDistribution(
@ -123,6 +123,34 @@ describe('getAvailableVersions', () => {
const availableVersions = await distribution['getAvailableVersions'](); const availableVersions = await distribution['getAvailableVersions']();
expect(availableVersions).not.toBeNull(); expect(availableVersions).not.toBeNull();
expect(availableVersions.length).toBe(manifestData.length * 2); expect(availableVersions.length).toBe(manifestData.length * 2);
expect(spyHttpClient).toHaveBeenNthCalledWith(2, nextPageUrl);
});
it('stops pagination after 1000 pages as a safeguard', async () => {
const nextPageUrl =
'https://api.adoptium.net/v3/assets/version/%5B1.0,100.0%5D?page=2&page_size=20';
spyHttpClient.mockReturnValue({
statusCode: 200,
headers: {link: `<${nextPageUrl}>; rel="next"`},
result: [{version_data: {semver: '17.0.1'}, binaries: []}] as any
});
const distribution = new TemurinDistribution(
{
version: '8',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
},
TemurinImplementation.Hotspot
);
await distribution['getAvailableVersions']();
expect(spyHttpClient).toHaveBeenCalledTimes(1000);
expect(spyCoreWarning).toHaveBeenCalledWith(
expect.stringContaining('Reached pagination safeguard limit (1000 pages)')
);
}); });
it.each([ it.each([

View File

@ -4,10 +4,12 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { import {
convertVersionToSemver, convertVersionToSemver,
getNextPageUrlFromLinkHeader,
getVersionFromFileContent, getVersionFromFileContent,
isVersionSatisfies, isVersionSatisfies,
isCacheFeatureAvailable, isCacheFeatureAvailable,
isGhes isGhes,
validatePaginationUrl
} from '../src/util'; } from '../src/util';
jest.mock('@actions/cache'); jest.mock('@actions/cache');
@ -89,6 +91,78 @@ describe('convertVersionToSemver', () => {
}); });
}); });
describe('getNextPageUrlFromLinkHeader', () => {
it.each([
[
{
link: '<https://api.adoptium.net/v3/info/release_versions?page=1&page_size=10>; rel="next"'
},
'https://api.adoptium.net/v3/info/release_versions?page=1&page_size=10'
],
[
{
Link: '<https://example.com/last?page=5>; rel="last", <https://example.com/next?page=2>; rel="next"'
},
'https://example.com/next?page=2'
],
[
{
link: '<https://api.adoptium.net/v3/versions?page=3>; type="application/json"; rel="next"'
},
'https://api.adoptium.net/v3/versions?page=3'
],
[{link: '<https://example.com/last?page=5>; rel="last"'}, null],
[{link: '<https://example.com/page?p=2>; rel="nextsomething"'}, null],
[undefined, null]
])('returns %s -> %s', (headers, expected) => {
expect(getNextPageUrlFromLinkHeader(headers)).toBe(expected);
});
});
describe('validatePaginationUrl', () => {
it('accepts URL with matching origin', () => {
expect(
validatePaginationUrl(
'https://api.adoptium.net/v3/assets?page=2',
'https://api.adoptium.net'
)
).toBe(true);
});
it('rejects URL with different host', () => {
expect(
validatePaginationUrl(
'https://evil.example.com/steal?data=1',
'https://api.adoptium.net'
)
).toBe(false);
});
it('rejects URL with different protocol', () => {
expect(
validatePaginationUrl(
'http://api.adoptium.net/v3/assets?page=2',
'https://api.adoptium.net'
)
).toBe(false);
});
it('returns false for invalid URL', () => {
expect(validatePaginationUrl('not-a-url', 'https://api.adoptium.net')).toBe(
false
);
});
it('accepts URL with explicit default port', () => {
expect(
validatePaginationUrl(
'https://api.adoptium.net:443/v3/assets?page=2',
'https://api.adoptium.net'
)
).toBe(true);
});
});
describe('getVersionFromFileContent', () => { describe('getVersionFromFileContent', () => {
describe('.sdkmanrc', () => { describe('.sdkmanrc', () => {
it.each([ it.each([

4758
dist/cleanup/index.js vendored

File diff suppressed because one or more lines are too long

6585
dist/setup/index.js vendored

File diff suppressed because one or more lines are too long

3820
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -40,20 +40,20 @@
"xmlbuilder2": "^4.0.3" "xmlbuilder2": "^4.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.14", "@types/jest": "^30.0.0",
"@types/node": "^24.1.0", "@types/node": "^25.9.3",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"@typescript-eslint/eslint-plugin": "^8.35.1", "@typescript-eslint/eslint-plugin": "^8.48.0",
"@typescript-eslint/parser": "^8.35.1", "@typescript-eslint/parser": "^8.61.1",
"@vercel/ncc": "^0.38.1", "@vercel/ncc": "^0.44.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^8.6.0", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-jest": "^29.0.1", "eslint-plugin-jest": "^29.0.1",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"jest": "^29.7.0", "jest": "^30.4.2",
"jest-circus": "^29.7.0", "jest-circus": "^30.4.2",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"ts-jest": "^29.3.0", "ts-jest": "^29.4.11",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"bugs": { "bugs": {

View File

@ -14,10 +14,14 @@ import {
} from '../base-models'; } from '../base-models';
import { import {
extractJdkFile, extractJdkFile,
getNextPageUrlFromLinkHeader,
getDownloadArchiveExtension, getDownloadArchiveExtension,
isVersionSatisfies, isVersionSatisfies,
renameWinArchive renameWinArchive,
MAX_PAGINATION_PAGES,
validatePaginationUrl
} from '../../util'; } from '../../util';
import {TemurinDistribution, TemurinImplementation} from '../temurin/installer';
export enum AdoptImplementation { export enum AdoptImplementation {
Hotspot = 'Hotspot', Hotspot = 'Hotspot',
@ -25,15 +29,72 @@ export enum AdoptImplementation {
} }
export class AdoptDistribution extends JavaBase { export class AdoptDistribution extends JavaBase {
private readonly temurinDistribution: TemurinDistribution | null;
constructor( constructor(
installerOptions: JavaInstallerOptions, installerOptions: JavaInstallerOptions,
private readonly jvmImpl: AdoptImplementation private readonly jvmImpl: AdoptImplementation,
temurinDistribution: TemurinDistribution | null = null
) { ) {
super(`Adopt-${jvmImpl}`, installerOptions); super(`Adopt-${jvmImpl}`, installerOptions);
if (
temurinDistribution !== null &&
jvmImpl !== AdoptImplementation.Hotspot
) {
throw new Error('Only Hotspot JVM is supported by Temurin.');
}
// Only use the temurin repo for Hotspot JVMs
this.temurinDistribution =
temurinDistribution ??
(jvmImpl === AdoptImplementation.Hotspot
? new TemurinDistribution(
installerOptions,
TemurinImplementation.Hotspot
)
: null);
} }
protected async findPackageForDownload( protected async findPackageForDownload(
version: string version: string
): Promise<JavaDownloadRelease> {
if (this.jvmImpl === AdoptImplementation.Hotspot) {
core.notice(
"AdoptOpenJDK has moved to Eclipse Temurin https://github.com/actions/setup-java#supported-distributions please consider changing to the 'temurin' distribution type in your setup-java configuration."
);
}
if (
this.jvmImpl === AdoptImplementation.Hotspot &&
this.temurinDistribution !== null
) {
try {
return await this.temurinDistribution.findPackageForDownload(version);
} catch (error) {
// Log the failure but always fall back to legacy AdoptOpenJDK for resilience
const errorMessage =
error instanceof Error ? error.message : String(error);
if (error instanceof Error && error.name === 'VersionNotFoundError') {
core.notice(
'The JVM you are looking for could not be found in the Temurin repository, this likely indicates ' +
'that you are using an out of date version of Java, consider updating and moving to using the Temurin distribution type in setup-java.'
);
} else {
// Log other errors for debugging but gracefully fall back
core.debug(
`Temurin lookup failed: ${errorMessage}. Falling back to AdoptOpenJDK API.`
);
}
}
}
// failed to find a Temurin version, so fall back to AdoptOpenJDK
return this.findPackageForDownloadOldAdoptOpenJdk(version);
}
private async findPackageForDownloadOldAdoptOpenJdk(
version: string
): Promise<JavaDownloadRelease> { ): Promise<JavaDownloadRelease> {
const availableVersionsRaw = await this.getAvailableVersions(); const availableVersionsRaw = await this.getAvailableVersions();
const availableVersionsWithBinaries = availableVersionsRaw const availableVersionsWithBinaries = availableVersionsRaw
@ -125,30 +186,46 @@ export class AdoptDistribution extends JavaBase {
`jvm_impl=${this.jvmImpl.toLowerCase()}` `jvm_impl=${this.jvmImpl.toLowerCase()}`
].join('&'); ].join('&');
// need to iterate through all pages to retrieve the list of all versions const requestArguments = `${baseRequestArguments}&page_size=20&page=0`;
// Adopt API doesn't provide way to retrieve the count of pages to iterate so infinity loop let availableVersionsUrl: string | null =
let page_index = 0; `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`;
const availableVersions: IAdoptAvailableVersions[] = []; const availableVersions: IAdoptAvailableVersions[] = [];
while (true) { let pageCount = 0;
const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`; if (core.isDebug()) {
const availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`; core.debug(`Gathering available versions from '${availableVersionsUrl}'`);
if (core.isDebug() && page_index === 0) { }
// url is identical except page_index so print it once for debug
core.debug(
`Gathering available versions from '${availableVersionsUrl}'`
);
}
const paginationPage = ( while (availableVersionsUrl) {
await this.http.getJson<IAdoptAvailableVersions[]>(availableVersionsUrl) pageCount++;
).result; const response =
await this.http.getJson<IAdoptAvailableVersions[]>(
availableVersionsUrl
);
const paginationPage = response.result;
const nextUrl = getNextPageUrlFromLinkHeader(response.headers);
if (
nextUrl &&
!validatePaginationUrl(nextUrl, 'https://api.adoptopenjdk.net')
) {
core.warning(
`Ignoring pagination link with unexpected origin: ${nextUrl}`
);
availableVersionsUrl = null;
} else {
availableVersionsUrl = nextUrl;
}
if (paginationPage === null || paginationPage.length === 0) { if (paginationPage === null || paginationPage.length === 0) {
// break infinity loop because we have reached end of pagination
break; break;
} }
availableVersions.push(...paginationPage); availableVersions.push(...paginationPage);
page_index++;
if (pageCount >= MAX_PAGINATION_PAGES) {
core.warning(
`Reached pagination safeguard limit (${MAX_PAGINATION_PAGES} pages) while listing Adopt releases.`
);
break;
}
} }
if (core.isDebug()) { if (core.isDebug()) {

View File

@ -292,7 +292,9 @@ export abstract class JavaBase {
} }
} }
return new Error(parts.join('\n')); const error = new Error(parts.join('\n'));
error.name = 'VersionNotFoundError';
return error;
} }
protected setJavaDefault(version: string, toolPath: string) { protected setJavaDefault(version: string, toolPath: string) {

View File

@ -7,9 +7,12 @@ import {
import semver from 'semver'; import semver from 'semver';
import { import {
extractJdkFile, extractJdkFile,
getNextPageUrlFromLinkHeader,
getDownloadArchiveExtension, getDownloadArchiveExtension,
isVersionSatisfies, isVersionSatisfies,
renameWinArchive renameWinArchive,
MAX_PAGINATION_PAGES,
validatePaginationUrl
} from '../../util'; } from '../../util';
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as tc from '@actions/tool-cache'; import * as tc from '@actions/tool-cache';
@ -155,32 +158,46 @@ export class SemeruDistribution extends JavaBase {
`jvm_impl=openj9` `jvm_impl=openj9`
].join('&'); ].join('&');
// need to iterate through all pages to retrieve the list of all versions const requestArguments = `${baseRequestArguments}&page_size=20&page=0`;
// Adoptium API doesn't provide way to retrieve the count of pages to iterate so infinity loop let availableVersionsUrl: string | null =
let page_index = 0; `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`;
const availableVersions: ISemeruAvailableVersions[] = []; const availableVersions: ISemeruAvailableVersions[] = [];
while (true) { let pageCount = 0;
const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`; if (core.isDebug()) {
const availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`; core.debug(`Gathering available versions from '${availableVersionsUrl}'`);
if (core.isDebug() && page_index === 0) { }
// url is identical except page_index so print it once for debug
core.debug(
`Gathering available versions from '${availableVersionsUrl}'`
);
}
const paginationPage = ( while (availableVersionsUrl) {
pageCount++;
const response =
await this.http.getJson<ISemeruAvailableVersions[]>( await this.http.getJson<ISemeruAvailableVersions[]>(
availableVersionsUrl availableVersionsUrl
) );
).result; const paginationPage = response.result;
const nextUrl = getNextPageUrlFromLinkHeader(response.headers);
if (
nextUrl &&
!validatePaginationUrl(nextUrl, 'https://api.adoptopenjdk.net')
) {
core.warning(
`Ignoring pagination link with unexpected origin: ${nextUrl}`
);
availableVersionsUrl = null;
} else {
availableVersionsUrl = nextUrl;
}
if (paginationPage === null || paginationPage.length === 0) { if (paginationPage === null || paginationPage.length === 0) {
// break infinity loop because we have reached end of pagination
break; break;
} }
availableVersions.push(...paginationPage); availableVersions.push(...paginationPage);
page_index++;
if (pageCount >= MAX_PAGINATION_PAGES) {
core.warning(
`Reached pagination safeguard limit (${MAX_PAGINATION_PAGES} pages) while listing Semeru releases.`
);
break;
}
} }
if (core.isDebug()) { if (core.isDebug()) {

View File

@ -14,9 +14,12 @@ import {
} from '../base-models'; } from '../base-models';
import { import {
extractJdkFile, extractJdkFile,
getNextPageUrlFromLinkHeader,
getDownloadArchiveExtension, getDownloadArchiveExtension,
isVersionSatisfies, isVersionSatisfies,
renameWinArchive renameWinArchive,
MAX_PAGINATION_PAGES,
validatePaginationUrl
} from '../../util'; } from '../../util';
export enum TemurinImplementation { export enum TemurinImplementation {
@ -31,7 +34,10 @@ export class TemurinDistribution extends JavaBase {
super(`Temurin-${jvmImpl}`, installerOptions); super(`Temurin-${jvmImpl}`, installerOptions);
} }
protected async findPackageForDownload( /**
* @internal For cross-distribution reuse only. Not intended as a public API.
*/
public async findPackageForDownload(
version: string version: string
): Promise<JavaDownloadRelease> { ): Promise<JavaDownloadRelease> {
const availableVersionsRaw = await this.getAvailableVersions(); const availableVersionsRaw = await this.getAvailableVersions();
@ -123,32 +129,47 @@ export class TemurinDistribution extends JavaBase {
`jvm_impl=${this.jvmImpl.toLowerCase()}` `jvm_impl=${this.jvmImpl.toLowerCase()}`
].join('&'); ].join('&');
// need to iterate through all pages to retrieve the list of all versions const requestArguments = `${baseRequestArguments}&page_size=20&page=0`;
// Adoptium API doesn't provide way to retrieve the count of pages to iterate so infinity loop let availableVersionsUrl: string | null =
let page_index = 0; `https://api.adoptium.net/v3/assets/version/${versionRange}?${requestArguments}`;
const availableVersions: ITemurinAvailableVersions[] = []; const availableVersions: ITemurinAvailableVersions[] = [];
while (true) { let pageCount = 0;
const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`; if (core.isDebug()) {
const availableVersionsUrl = `https://api.adoptium.net/v3/assets/version/${versionRange}?${requestArguments}`; core.debug(`Gathering available versions from '${availableVersionsUrl}'`);
if (core.isDebug() && page_index === 0) { }
// url is identical except page_index so print it once for debug
core.debug(
`Gathering available versions from '${availableVersionsUrl}'`
);
}
const paginationPage = ( while (availableVersionsUrl) {
pageCount++;
const response =
await this.http.getJson<ITemurinAvailableVersions[]>( await this.http.getJson<ITemurinAvailableVersions[]>(
availableVersionsUrl availableVersionsUrl
) );
).result; const paginationPage = response.result;
const nextUrl = getNextPageUrlFromLinkHeader(response.headers);
if (
nextUrl &&
!validatePaginationUrl(nextUrl, 'https://api.adoptium.net')
) {
core.warning(
`Ignoring pagination link with unexpected origin: ${nextUrl}`
);
availableVersionsUrl = null;
} else {
availableVersionsUrl = nextUrl;
}
if (paginationPage === null || paginationPage.length === 0) { if (paginationPage === null || paginationPage.length === 0) {
// break infinity loop because we have reached end of pagination
break; break;
} }
availableVersions.push(...paginationPage); availableVersions.push(...paginationPage);
page_index++;
if (pageCount >= MAX_PAGINATION_PAGES) {
core.warning(
`Reached pagination safeguard limit (${MAX_PAGINATION_PAGES} pages) while listing Temurin releases.`
);
break;
}
} }
if (core.isDebug()) { if (core.isDebug()) {
@ -171,6 +192,11 @@ export class TemurinDistribution extends JavaBase {
return 'mac'; return 'mac';
case 'win32': case 'win32':
return 'windows'; return 'windows';
case 'linux':
if (fs.existsSync('/etc/alpine-release')) {
return 'alpine-linux';
}
return 'linux';
default: default:
return process.platform; return process.platform;
} }

View File

@ -209,6 +209,55 @@ export function getGitHubHttpHeaders(): OutgoingHttpHeaders {
return headers; return headers;
} }
export const MAX_PAGINATION_PAGES = 1000;
export function getNextPageUrlFromLinkHeader(
headers?: Record<string, string | string[] | undefined>
): string | null {
if (!headers) {
return null;
}
const linkHeader = headers.link ?? headers.Link;
if (!linkHeader) {
return null;
}
const normalizedLinkHeader = Array.isArray(linkHeader)
? linkHeader.join(',')
: linkHeader;
// Split into individual link-values and find the one with rel="next"
// RFC 8288 allows rel to appear anywhere among the parameters
const linkValues = normalizedLinkHeader.split(/,(?=\s*<)/);
for (const linkValue of linkValues) {
const urlMatch = linkValue.match(/<([^>]+)>/);
if (!urlMatch) continue;
const params = linkValue.slice(urlMatch[0].length);
// Use word boundary to match "next" as a standalone relation type
// RFC 8288 allows space-separated relation types like rel="next prev"
if (/;\s*rel="?[^"]*\bnext\b/i.test(params)) {
return urlMatch[1];
}
}
return null;
}
export function validatePaginationUrl(
url: string,
allowedOrigin: string
): boolean {
try {
const parsed = new URL(url);
const allowed = new URL(allowedOrigin);
return parsed.origin === allowed.origin;
} catch {
return false;
}
}
// Rename archive to add extension because after downloading // Rename archive to add extension because after downloading
// archive does not contain extension type and it leads to some issues // archive does not contain extension type and it leads to some issues
// on Windows runners without PowerShell Core. // on Windows runners without PowerShell Core.