Harden java-package validation (#1165)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 10b8fe1e-18f4-42cb-8672-11215715a713
This commit is contained in:
Bruno Borges 2026-07-29 00:50:01 -04:00 committed by GitHub
parent 6e26972896
commit 19c23b379e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 403 additions and 63 deletions

View File

@ -48,7 +48,7 @@ For more details, see the full release notes on the [releases page](https://git
- `distribution`: Java [distribution](#supported-distributions). Required unless `java-version-file` points to `.sdkmanrc` with a recognized distribution suffix (for example `java=21.0.5-tem`). - `distribution`: Java [distribution](#supported-distributions). Required unless `java-version-file` points to `.sdkmanrc` with a recognized distribution suffix (for example `java=21.0.5-tem`).
- `java-package`: The packaging variant of the chosen distribution. Possible values: `jdk`, `jre`, `jdk+fx`, `jre+fx`. For Azul Zulu, `jdk+crac` and `jre+crac` are also supported. For Eclipse Temurin 24 and later, `jdk+jmods` includes the separately packaged JMOD files. Default value: `jdk`. - `java-package`: The packaging variant of the chosen distribution. Possible values across all distributions are `jdk`, `jre`, `jdk+fx`, `jre+fx`, `jdk+crac`, `jre+crac`, `jdk+jmods`, `jdk+jcef`, `jre+jcef`, `jdk+ft`, and `jre+ft`. Supported values vary by distribution; see the [package compatibility table](docs/advanced-usage.md#package-compatibility). Default value: `jdk`.
- `architecture`: The target architecture of the package. Possible values: `x86`, `x64`, `armv7`, `aarch64`, `ppc64le`. Default value: Derived from the runner machine. - `architecture`: The target architecture of the package. Possible values: `x86`, `x64`, `armv7`, `aarch64`, `ppc64le`. Default value: Derived from the runner machine.

View File

@ -1,5 +1,16 @@
import {getJavaDistribution} from '../../src/distributions/distribution-factory.js'; import {getJavaDistribution} from '../../src/distributions/distribution-factory.js';
import {RetryingHttpClient} from '../../src/retrying-http-client.js'; import {RetryingHttpClient} from '../../src/retrying-http-client.js';
import {
JAVA_PACKAGE_CAPABILITIES,
JavaDistribution
} from '../../src/distributions/package-types.js';
const installerOptions = (packageType: string, version = '25') => ({
version,
architecture: 'x64',
packageType,
checkLatest: false
});
describe('getJavaDistribution', () => { describe('getJavaDistribution', () => {
it.each([ it.each([
@ -33,16 +44,71 @@ describe('getJavaDistribution', () => {
expect(distribution!['http']).toBeInstanceOf(RetryingHttpClient); expect(distribution!['http']).toBeInstanceOf(RetryingHttpClient);
}); });
it.each(
Object.entries(JAVA_PACKAGE_CAPABILITIES).flatMap(
([distributionName, packageTypes]) =>
packageTypes.map(packageType => [distributionName, packageType])
)
)('accepts %s with java-package %s', (distributionName, packageType) => {
expect(
getJavaDistribution(
distributionName,
installerOptions(packageType as string)
)
).not.toBeNull();
});
it.each(Object.entries(JAVA_PACKAGE_CAPABILITIES))(
'rejects unsupported java-package values for %s',
(distributionName, packageTypes) => {
expect(() =>
getJavaDistribution(distributionName, installerOptions('jdk+typo'))
).toThrow(
`Java package 'jdk+typo' is not supported for distribution '${distributionName}'. Supported package types: ${packageTypes.join(', ')}.`
);
}
);
it("rejects java-package 'jdk+jmods' for non-Temurin distributions", () => { it("rejects java-package 'jdk+jmods' for non-Temurin distributions", () => {
expect(() => expect(() =>
getJavaDistribution('zulu', { getJavaDistribution('zulu', installerOptions('jdk+jmods'))
version: '25',
architecture: 'x64',
packageType: 'jdk+jmods',
checkLatest: false
})
).toThrow( ).toThrow(
"java-package 'jdk+jmods' is only supported for distribution 'temurin'." "Java package 'jdk+jmods' is not supported for distribution 'zulu'. Supported package types: jdk, jre, jdk+fx, jre+fx, jdk+crac, jre+crac."
); );
}); });
it.each(['8', '23.x', '23.0.1.1', '<24'])(
"rejects Temurin java-package 'jdk+jmods' for version %s",
version => {
expect(() =>
getJavaDistribution(
JavaDistribution.Temurin,
installerOptions('jdk+jmods', version)
)
).toThrow(
`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}'.`
);
}
);
it.each(['24', '24.0.1.1', '25-ea', '>=21', 'latest'])(
"accepts Temurin java-package 'jdk+jmods' for version %s",
version => {
expect(
getJavaDistribution(
JavaDistribution.Temurin,
installerOptions('jdk+jmods', version)
)
).not.toBeNull();
}
);
it('preserves unsupported distribution handling', () => {
expect(
getJavaDistribution(
'not-a-distribution',
installerOptions('not-a-package')
)
).toBeNull();
});
}); });

View File

@ -0,0 +1,82 @@
import fs from 'fs';
import path from 'path';
import {
JAVA_PACKAGE_CAPABILITIES,
JavaDistribution
} from '../src/distributions/package-types.js';
const repositoryRoot = process.cwd();
const readRepositoryFile = (filePath: string) =>
fs.readFileSync(path.join(repositoryRoot, filePath), 'utf8');
const allPackageTypes = [
...new Set(Object.values(JAVA_PACKAGE_CAPABILITIES).flat())
];
describe('java-package published contract', () => {
it.each(['action.yml', 'README.md'])(
'documents every supported package type in %s',
filePath => {
const content = readRepositoryFile(filePath);
const contractLine =
filePath === 'action.yml'
? content.match(/ {2}java-package:\n(?: {4}.+\n)+/)?.[0]
: content
.split('\n')
.find(line => line.includes('- `java-package`:'));
expect(contractLine).toBeDefined();
for (const packageType of allPackageTypes) {
expect(contractLine).toContain(`\`${packageType}\``);
}
}
);
it.each(Object.entries(JAVA_PACKAGE_CAPABILITIES))(
'keeps the advanced compatibility table aligned for %s',
(distributionName, packageTypes) => {
const advancedUsage = readRepositoryFile('docs/advanced-usage.md');
const compatibilityTable = advancedUsage.slice(
advancedUsage.indexOf('### Package compatibility')
);
const compatibilityRow = compatibilityTable
.split('\n')
.find(
line =>
line.startsWith('|') && line.includes(`\`${distributionName}\``)
);
expect(compatibilityRow).toBeDefined();
for (const packageType of packageTypes) {
expect(compatibilityRow).toContain(`\`${packageType}\``);
}
}
);
it('only exercises supported distribution/package combinations in E2E', () => {
const workflow = readRepositoryFile('.github/workflows/e2e-versions.yml');
const defaultMatrix = workflow.match(
/distribution:\s*\n\s*\[([^\]]+)\]\s*\n\s*java-package:\s*\['([^']+)'\]/
);
expect(defaultMatrix).not.toBeNull();
const defaultDistributions = [
...defaultMatrix![1].matchAll(/'([^']+)'/g)
].map(match => match[1]);
const defaultPackage = defaultMatrix![2];
for (const distributionName of defaultDistributions) {
expect(supportedPackagesFor(distributionName)).toContain(defaultPackage);
}
const includedPackages = workflow.matchAll(
/- distribution: '([^']+)'\s*\n\s*java-package: ([^\s]+)/g
);
for (const match of includedPackages) {
const [, distributionName, packageType] = match;
expect(supportedPackagesFor(distributionName)).toContain(packageType);
}
});
});
function supportedPackagesFor(distributionName: string): readonly string[] {
return JAVA_PACKAGE_CAPABILITIES[distributionName as JavaDistribution] ?? [];
}

View File

@ -13,7 +13,7 @@ inputs:
description: 'Java distribution. See the list of supported distributions in README file. This input is required except when java-version-file points to .sdkmanrc with a recognized distribution suffix (e.g., java=21.0.5-tem).' description: 'Java distribution. See the list of supported distributions in README file. This input is required except when java-version-file points to .sdkmanrc with a recognized distribution suffix (e.g., java=21.0.5-tem).'
required: false required: false
java-package: java-package:
description: 'The package type (jdk, jre, jdk+fx, jre+fx, jdk+crac, jre+crac, jdk+jmods)' description: 'The package type (`jdk`, `jre`, `jdk+fx`, `jre+fx`, `jdk+crac`, `jre+crac`, `jdk+jmods`, `jdk+jcef`, `jre+jcef`, `jdk+ft`, or `jre+ft`). Supported values vary by distribution.'
required: false required: false
default: 'jdk' default: 'jdk'
architecture: architecture:

121
dist/setup/index.js vendored
View File

@ -132121,21 +132121,7 @@ class OpenJdkDistribution extends JavaBase {
} }
} }
;// CONCATENATED MODULE: ./src/distributions/distribution-factory.ts ;// CONCATENATED MODULE: ./src/distributions/package-types.ts
var JavaDistribution; var JavaDistribution;
@ -132160,11 +132146,108 @@ var JavaDistribution;
JavaDistribution["Kona"] = "kona"; JavaDistribution["Kona"] = "kona";
JavaDistribution["OracleOpenJdk"] = "oracle-openjdk"; JavaDistribution["OracleOpenJdk"] = "oracle-openjdk";
})(JavaDistribution || (JavaDistribution = {})); })(JavaDistribution || (JavaDistribution = {}));
function getJavaDistribution(distributionName, installerOptions, jdkFile) { const JAVA_PACKAGE_CAPABILITIES = {
if (installerOptions.packageType === 'jdk+jmods' && [JavaDistribution.Adopt]: ['jdk', 'jre'],
distributionName !== JavaDistribution.Temurin) { [JavaDistribution.AdoptHotspot]: ['jdk', 'jre'],
throw new Error("java-package 'jdk+jmods' is only supported for distribution 'temurin'."); [JavaDistribution.AdoptOpenJ9]: ['jdk', 'jre'],
[JavaDistribution.Temurin]: ['jdk', 'jre', 'jdk+jmods'],
[JavaDistribution.Zulu]: [
'jdk',
'jre',
'jdk+fx',
'jre+fx',
'jdk+crac',
'jre+crac'
],
[JavaDistribution.Liberica]: ['jdk', 'jre', 'jdk+fx', 'jre+fx'],
[JavaDistribution.LibericaNik]: ['jdk', 'jdk+fx'],
[JavaDistribution.JdkFile]: ['jdk'],
[JavaDistribution.Microsoft]: ['jdk'],
[JavaDistribution.Semeru]: ['jdk', 'jre'],
[JavaDistribution.Corretto]: ['jdk', 'jre'],
[JavaDistribution.Oracle]: ['jdk'],
[JavaDistribution.Dragonwell]: ['jdk'],
[JavaDistribution.SapMachine]: ['jdk', 'jre'],
[JavaDistribution.GraalVM]: ['jdk'],
[JavaDistribution.GraalVMCommunity]: ['jdk'],
[JavaDistribution.JetBrains]: [
'jdk',
'jre',
'jdk+jcef',
'jre+jcef',
'jdk+ft',
'jre+ft'
],
[JavaDistribution.Kona]: ['jdk'],
[JavaDistribution.OracleOpenJdk]: ['jdk']
};
function validateJavaPackage(distributionName, packageType, version) {
if (!isJavaDistribution(distributionName)) {
return;
} }
const supportedPackages = JAVA_PACKAGE_CAPABILITIES[distributionName];
if (!supportedPackages.includes(packageType)) {
throw createUnsupportedPackageError(distributionName, packageType, supportedPackages);
}
if (distributionName === JavaDistribution.Temurin &&
packageType === 'jdk+jmods' &&
!canResolveTemurinJmods(version)) {
throw createUnsupportedPackageError(distributionName, packageType, supportedPackages, `Package 'jdk+jmods' requires Java 24 or later; requested version '${version}'.`);
}
}
function isJavaDistribution(value) {
return Object.prototype.hasOwnProperty.call(JAVA_PACKAGE_CAPABILITIES, value);
}
function canResolveTemurinJmods(version) {
const normalizedVersion = version.trim().toLowerCase();
if (normalizedVersion === 'latest') {
return true;
}
let normalizedRange = normalizedVersion
.replace(/-ea$/, '')
.replace('-ea.', '+');
if (/^\d+(\.\d+){3,}$/.test(normalizedRange)) {
normalizedRange = convertVersionToSemver(normalizedRange);
}
if (!semver_default().validRange(normalizedRange)) {
// JavaBase owns general version validation and its targeted error messages.
return true;
}
return semver_default().intersects(normalizedRange, '>=24.0.0', {
includePrerelease: true
});
}
function createUnsupportedPackageError(distributionName, packageType, supportedPackages, detail) {
const message = [
`Java package '${packageType}' is not supported for distribution '${distributionName}'.`,
`Supported package types: ${supportedPackages.join(', ')}.`
];
if (detail) {
message.push(detail);
}
return new Error(message.join(' '));
}
;// CONCATENATED MODULE: ./src/distributions/distribution-factory.ts
function getJavaDistribution(distributionName, installerOptions, jdkFile) {
validateJavaPackage(distributionName, installerOptions.packageType, installerOptions.version);
switch (distributionName) { switch (distributionName) {
case JavaDistribution.JdkFile: case JavaDistribution.JdkFile:
return new LocalDistribution(installerOptions, jdkFile); return new LocalDistribution(installerOptions, jdkFile);

View File

@ -321,12 +321,10 @@ The package types have these meanings:
| `graalvm-community` | `jdk` | Stable GraalVM Community releases for JDK 17 and later only. | | `graalvm-community` | `jdk` | Stable GraalVM Community releases for JDK 17 and later only. |
| `jetbrains` | `jdk`, `jre`, `jdk+jcef`, `jre+jcef`, `jdk+ft`, `jre+ft` | JetBrains publishes selected LTS-based releases rather than every OpenJDK patch. JDK/JRE and JCEF bundles start with the Java 11 release family; FreeType bundles start with Java 17. Exact package, LTS family, patch, OS, and architecture availability is determined from release assets. | | `jetbrains` | `jdk`, `jre`, `jdk+jcef`, `jre+jcef`, `jdk+ft`, `jre+ft` | JetBrains publishes selected LTS-based releases rather than every OpenJDK patch. JDK/JRE and JCEF bundles start with the Java 11 release family; FreeType bundles start with Java 17. Exact package, LTS family, patch, OS, and architecture availability is determined from release assets. |
| `kona` | `jdk` | Stable Java 8, 11, 17, 21, and 25 releases only. | | `kona` | `jdk` | Stable Java 8, 11, 17, 21, and 25 releases only. |
| `jdkfile` | `jdk` (recommended) | The package contents and version are supplied by `jdk-file`; `setup-java` does not validate them. `java-package` only separates the local archive's tool-cache entry, so use `jdk` unless separate cache namespaces are required. | | `jdkfile` | `jdk` | The package contents and version are supplied by `jdk-file`; `setup-java` validates the package type but does not inspect the archive contents. |
Values outside this table are unsupported even when a distribution forwards the Values outside this table are unsupported. The action rejects them before
value to its vendor API instead of rejecting it immediately. In that case, the checking the tool cache or requesting a vendor catalog.
action normally fails with a version-not-found error because no matching
artifact exists.
```yaml ```yaml
steps: steps:

View File

@ -22,42 +22,18 @@ import {
import {JetBrainsDistribution} from './jetbrains/installer.js'; import {JetBrainsDistribution} from './jetbrains/installer.js';
import {KonaDistribution} from './kona/installer.js'; import {KonaDistribution} from './kona/installer.js';
import {OpenJdkDistribution} from './openjdk/installer.js'; import {OpenJdkDistribution} from './openjdk/installer.js';
import {JavaDistribution, validateJavaPackage} from './package-types.js';
enum JavaDistribution {
Adopt = 'adopt',
AdoptHotspot = 'adopt-hotspot',
AdoptOpenJ9 = 'adopt-openj9',
Temurin = 'temurin',
Zulu = 'zulu',
Liberica = 'liberica',
LibericaNik = 'liberica-nik',
JdkFile = 'jdkfile',
Microsoft = 'microsoft',
Semeru = 'semeru',
Corretto = 'corretto',
Oracle = 'oracle',
Dragonwell = 'dragonwell',
SapMachine = 'sapmachine',
GraalVM = 'graalvm',
GraalVMCommunity = 'graalvm-community',
JetBrains = 'jetbrains',
Kona = 'kona',
OracleOpenJdk = 'oracle-openjdk'
}
export function getJavaDistribution( export function getJavaDistribution(
distributionName: string, distributionName: string,
installerOptions: JavaInstallerOptions, installerOptions: JavaInstallerOptions,
jdkFile?: string jdkFile?: string
): JavaBase | null { ): JavaBase | null {
if ( validateJavaPackage(
installerOptions.packageType === 'jdk+jmods' && distributionName,
distributionName !== JavaDistribution.Temurin installerOptions.packageType,
) { installerOptions.version
throw new Error(
"java-package 'jdk+jmods' is only supported for distribution 'temurin'."
); );
}
switch (distributionName) { switch (distributionName) {
case JavaDistribution.JdkFile: case JavaDistribution.JdkFile:

View File

@ -0,0 +1,135 @@
import semver from 'semver';
import {convertVersionToSemver} from '../util.js';
export enum JavaDistribution {
Adopt = 'adopt',
AdoptHotspot = 'adopt-hotspot',
AdoptOpenJ9 = 'adopt-openj9',
Temurin = 'temurin',
Zulu = 'zulu',
Liberica = 'liberica',
LibericaNik = 'liberica-nik',
JdkFile = 'jdkfile',
Microsoft = 'microsoft',
Semeru = 'semeru',
Corretto = 'corretto',
Oracle = 'oracle',
Dragonwell = 'dragonwell',
SapMachine = 'sapmachine',
GraalVM = 'graalvm',
GraalVMCommunity = 'graalvm-community',
JetBrains = 'jetbrains',
Kona = 'kona',
OracleOpenJdk = 'oracle-openjdk'
}
export const JAVA_PACKAGE_CAPABILITIES = {
[JavaDistribution.Adopt]: ['jdk', 'jre'],
[JavaDistribution.AdoptHotspot]: ['jdk', 'jre'],
[JavaDistribution.AdoptOpenJ9]: ['jdk', 'jre'],
[JavaDistribution.Temurin]: ['jdk', 'jre', 'jdk+jmods'],
[JavaDistribution.Zulu]: [
'jdk',
'jre',
'jdk+fx',
'jre+fx',
'jdk+crac',
'jre+crac'
],
[JavaDistribution.Liberica]: ['jdk', 'jre', 'jdk+fx', 'jre+fx'],
[JavaDistribution.LibericaNik]: ['jdk', 'jdk+fx'],
[JavaDistribution.JdkFile]: ['jdk'],
[JavaDistribution.Microsoft]: ['jdk'],
[JavaDistribution.Semeru]: ['jdk', 'jre'],
[JavaDistribution.Corretto]: ['jdk', 'jre'],
[JavaDistribution.Oracle]: ['jdk'],
[JavaDistribution.Dragonwell]: ['jdk'],
[JavaDistribution.SapMachine]: ['jdk', 'jre'],
[JavaDistribution.GraalVM]: ['jdk'],
[JavaDistribution.GraalVMCommunity]: ['jdk'],
[JavaDistribution.JetBrains]: [
'jdk',
'jre',
'jdk+jcef',
'jre+jcef',
'jdk+ft',
'jre+ft'
],
[JavaDistribution.Kona]: ['jdk'],
[JavaDistribution.OracleOpenJdk]: ['jdk']
} as const satisfies Record<JavaDistribution, readonly string[]>;
export function validateJavaPackage(
distributionName: string,
packageType: string,
version: string
): void {
if (!isJavaDistribution(distributionName)) {
return;
}
const supportedPackages: readonly string[] =
JAVA_PACKAGE_CAPABILITIES[distributionName];
if (!supportedPackages.includes(packageType)) {
throw createUnsupportedPackageError(
distributionName,
packageType,
supportedPackages
);
}
if (
distributionName === JavaDistribution.Temurin &&
packageType === 'jdk+jmods' &&
!canResolveTemurinJmods(version)
) {
throw createUnsupportedPackageError(
distributionName,
packageType,
supportedPackages,
`Package 'jdk+jmods' requires Java 24 or later; requested version '${version}'.`
);
}
}
function isJavaDistribution(value: string): value is JavaDistribution {
return Object.prototype.hasOwnProperty.call(JAVA_PACKAGE_CAPABILITIES, value);
}
function canResolveTemurinJmods(version: string): boolean {
const normalizedVersion = version.trim().toLowerCase();
if (normalizedVersion === 'latest') {
return true;
}
let normalizedRange = normalizedVersion
.replace(/-ea$/, '')
.replace('-ea.', '+');
if (/^\d+(\.\d+){3,}$/.test(normalizedRange)) {
normalizedRange = convertVersionToSemver(normalizedRange);
}
if (!semver.validRange(normalizedRange)) {
// JavaBase owns general version validation and its targeted error messages.
return true;
}
return semver.intersects(normalizedRange, '>=24.0.0', {
includePrerelease: true
});
}
function createUnsupportedPackageError(
distributionName: JavaDistribution,
packageType: string,
supportedPackages: readonly string[],
detail?: string
): Error {
const message = [
`Java package '${packageType}' is not supported for distribution '${distributionName}'.`,
`Supported package types: ${supportedPackages.join(', ')}.`
];
if (detail) {
message.push(detail);
}
return new Error(message.join(' '));
}