Support Temurin JEP 322 patch versions like 26.0.2.1+1 (#1270) (#1279)

Accept 4-part versions with build metadata (X.Y.Z.P+B -> X.Y.Z+P.B) and
match exact Temurin requests against OpenJDK-derived version keys, since the
Adoptium API semver folds the patch into the build number (26.0.2+101) and
adds LTS metadata (25.0.4+7.0.LTS).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Bruno Borges
2026-10-02 00:09:56 -04:00
committed by GitHub
parent 74920aab4a
commit 47b36480ae
14 changed files with 243 additions and 39 deletions
+1 -1
View File
@@ -233,7 +233,7 @@ Additional distribution notes:
| --- | --- |
| Major version | `8`, `11`, `17`, `21`, `25` |
| Specific feature or patch version | `11.0`, `11.0.4`, `17.0`, `8.0.282+8` |
| JEP 322 multi-field versions | `11.0.9.1`, `18.0.1.1` |
| JEP 322 multi-field versions | `11.0.9.1`, `18.0.1.1`, `26.0.2.1+1` |
| Early access | `15-ea`, `15.0.0-ea`, `27-ea` |
| Latest stable GA release | `latest` |
@@ -1758,6 +1758,8 @@ describe('normalizeVersion', () => {
['11.0.9.1', {version: '11.0.9+1', stable: true, latest: false}],
['12.0.2.1.0', {version: '12.0.2+1.0', stable: true, latest: false}],
['18.0.1.1-ea', {version: '18.0.1+1', stable: false, latest: false}],
['26.0.2.1+1', {version: '26.0.2+1.1', stable: true, latest: false}],
['25.0.4.1+1', {version: '25.0.4+1.1', stable: true, latest: false}],
['latest', {version: 'x', stable: true, latest: true}],
['LATEST', {version: 'x', stable: true, latest: true}],
[' Latest ', {version: 'x', stable: true, latest: true}]
@@ -82,7 +82,7 @@ describe('getJavaDistribution', () => {
);
});
it.each(['8', '23.x', '23.0.1.1', '<24'])(
it.each(['8', '23.x', '23.0.1.1', '23.0.1.1+1', '<24'])(
"rejects Temurin java-package 'jdk+jmods' for version %s",
async version => {
await expect(
@@ -96,7 +96,7 @@ describe('getJavaDistribution', () => {
}
);
it.each(['24', '24.0.1.1', '25-ea', '>=21', 'latest'])(
it.each(['24', '24.0.1.1', '25.0.4.1+1', '25-ea', '>=21', 'latest'])(
"accepts Temurin java-package 'jdk+jmods' for version %s",
async version => {
expect(
@@ -378,6 +378,97 @@ describe('findPackageForDownload', () => {
expect(resolvedVersion.version).toBe('16.0.2+7');
});
describe('OpenJDK patch (respin) versions', () => {
const makeRelease = (
semverVersion: string,
openjdkVersion: string,
versionData: Record<string, number>
) => ({
binaries: [
{
package: {
link: `https://example.com/${openjdkVersion}.tar.gz`,
checksum: `checksum-${openjdkVersion}`,
checksum_link: `https://example.com/${openjdkVersion}.sha256.txt`
}
}
],
version_data: {
semver: semverVersion,
openjdk_version: openjdkVersion,
minor: 0,
...versionData
}
});
const respinManifest = [
makeRelease('26.0.2+101', '26.0.2.1+1', {
major: 26,
security: 2,
patch: 1,
build: 1
}),
makeRelease('26.0.2+10', '26.0.2+10', {
major: 26,
security: 2,
build: 10
}),
makeRelease('25.0.4+101.0.LTS', '25.0.4.1+1-LTS', {
major: 25,
security: 4,
patch: 1,
build: 1
}),
makeRelease('25.0.4+7.0.LTS', '25.0.4+7-LTS', {
major: 25,
security: 4,
build: 7
})
];
it.each([
['26.0.2.1+1', '26.0.2+101'],
['26.0.2+10', '26.0.2+10'],
['26', '26.0.2+101'],
['26.0.2', '26.0.2+101'],
['25.0.4.1+1', '25.0.4+101.0.LTS'],
['25.0.4+7', '25.0.4+7.0.LTS'],
['25.0.4', '25.0.4+101.0.LTS']
])('%s resolves to %s', async (input, expected) => {
const distribution = new TemurinDistribution(
{
version: input,
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
},
TemurinImplementation.Hotspot
);
distribution['getAvailableVersions'] = async () => respinManifest as any;
const resolvedVersion = await distribution['findPackageForDownload'](
distribution['version']
);
expect(resolvedVersion.version).toBe(expected);
expect(resolvedVersion).not.toHaveProperty('openjdkVersion');
});
it('does not match a non-existent respin', async () => {
const distribution = new TemurinDistribution(
{
version: '26.0.2.2+1',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
},
TemurinImplementation.Hotspot
);
distribution['getAvailableVersions'] = async () => respinManifest as any;
await expect(
distribution['findPackageForDownload'](distribution['version'])
).rejects.toThrow(/No matching version found for SemVer '26.0.2\+2.1'/);
});
});
it('version is found but binaries list is empty', async () => {
const distribution = new TemurinDistribution(
{
+17
View File
@@ -43,6 +43,7 @@ const core = await import('@actions/core');
const {
convertVersionToSemver,
normalizeJavaVersionToSemver,
getNextPageUrlFromLinkHeader,
getVersionFromFileContent,
isVersionSatisfies,
@@ -188,6 +189,22 @@ describe('convertVersionToSemver', () => {
});
});
describe('normalizeJavaVersionToSemver', () => {
it.each([
['17', '17'],
['17.0.8', '17.0.8'],
['17.0.8+7', '17.0.8+7'],
['11.0.9.1', '11.0.9+1'],
['12.0.2.1.0', '12.0.2+1.0'],
['26.0.2.1+1', '26.0.2+1.1'],
['17.0.8.1+1080.1', '17.0.8+1.1080.1'],
['>=11.0.9.1', '>=11.0.9.1'],
['17.x', '17.x']
])('%s -> %s', (input: string, expected: string) => {
expect(normalizeJavaVersionToSemver(input)).toBe(expected);
});
});
describe('getNextPageUrlFromLinkHeader', () => {
it.each([
[
+15 -1
View File
@@ -30848,7 +30848,7 @@ const DISTRIBUTIONS_ONLY_MAJOR_VERSION = (/* unused pure expression or super */
/* harmony export */ Vt: () => (/* binding */ getBooleanInput),
/* harmony export */ lN: () => (/* binding */ isJdkCacheEnabled)
/* harmony export */ });
/* unused harmony exports getVersionFromToolcachePath, extractJdkFile, cacheJdkDir, getJavaVersionFromReleaseFile, getDownloadArchiveExtension, isVersionSatisfies, getToolcachePath, isGhes, getVersionFromFileContent, convertVersionToSemver, getArtifactFingerprint, getGitHubToken, getGitHubHttpHeaders, MAX_PAGINATION_PAGES, getNextPageUrlFromLinkHeader, validatePaginationUrl, renameWinArchive, getLatestMajorVersion */
/* unused harmony exports getVersionFromToolcachePath, extractJdkFile, cacheJdkDir, getJavaVersionFromReleaseFile, getDownloadArchiveExtension, isVersionSatisfies, getToolcachePath, isGhes, getVersionFromFileContent, convertVersionToSemver, normalizeJavaVersionToSemver, getArtifactFingerprint, getGitHubToken, getGitHubHttpHeaders, MAX_PAGINATION_PAGES, getNextPageUrlFromLinkHeader, validatePaginationUrl, renameWinArchive, getLatestMajorVersion */
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_0__ = __nccwpck_require__(857);
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__nccwpck_require__.n(os__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __nccwpck_require__(6928);
@@ -31247,6 +31247,20 @@ function convertVersionToSemver(version) {
}
return mainVersion;
}
/**
* Java versions (JEP 322) can contain more numeric fields than SemVer allows,
* e.g. '11.0.9.1' or Temurin respins such as '26.0.2.1+1'. Move the extra
* fields into SemVer build metadata ('11.0.9+1', '26.0.2+1.1'). Any other
* input (ranges, regular SemVer versions) is returned unchanged.
*/
function normalizeJavaVersionToSemver(version) {
const match = /^(\d+(?:\.\d+){3,})(?:\+([0-9A-Za-z.-]+))?$/.exec(version);
if (!match) {
return version;
}
const converted = convertVersionToSemver(match[1]);
return match[2] ? `${converted}.${match[2]}` : converted;
}
/**
* Builds a validator for the bytes currently served by a URL from the response
* headers of a HEAD request. A vendor's `/latest/` URL never changes, so this
+3 -6
View File
@@ -730,12 +730,9 @@ class JavaBase {
}
// Java uses a versioning scheme (JEP 322) that can contain more numeric
// fields than SemVer allows, e.g. '18.0.1.1' or '11.0.9.1'. Convert such
// exact versions to SemVer build notation ('18.0.1+1') so they are
// accepted. Ranges and versions that already carry build metadata are
// left untouched.
if (/^\d+(\.\d+){3,}$/.test(version)) {
version = (0,util/* convertVersionToSemver */.ZY)(version);
}
// exact versions to SemVer build notation ('18.0.1+1', or '26.0.2+1.1'
// for '26.0.2.1+1') so they are accepted. Ranges are left untouched.
version = (0,util/* normalizeJavaVersionToSemver */.zZ)(version);
if (!semver_default().validRange(version)) {
throw new Error(`The string '${version}' is not valid SemVer notation for a Java version. Please check README file for code snippets and more detailed information`);
}
+31 -3
View File
@@ -109,7 +109,7 @@ class TemurinDistribution extends base_installer/* JavaBase */.O {
const formattedVersion = this.stable
? item.version_data.semver
: item.version_data.semver.replace('-beta+', '+');
return {
const release = {
version: formattedVersion,
url: item.binaries[0].package.link,
signatureUrl: item.binaries[0].package.signature_link,
@@ -119,15 +119,29 @@ class TemurinDistribution extends base_installer/* JavaBase */.O {
source: item.binaries[0].package.checksum_link
}
};
return {
release,
openjdkVersion: getOpenJdkSemverVersion(item.version_data)
};
});
// The Adoptium API `semver` folds the JEP 322 patch field into the build
// number ('26.0.2.1+1' -> '26.0.2+101') and appends extra metadata for LTS
// releases ('25.0.4+7' -> '25.0.4+7.0.LTS'). Exact versions requested by
// users follow the OpenJDK notation instead, so also match them against a
// key derived from the OpenJDK version fields ('26.0.2+1.1', '25.0.4+7').
const isExactBuildRequest = (semver_default().parse(version)?.build.length ?? 0) > 0;
const satisfiedVersions = availableVersionsWithBinaries
.filter(item => (0,util/* isVersionSatisfies */.y)(version, item.version))
.filter(({ release, openjdkVersion }) => (0,util/* isVersionSatisfies */.y)(version, release.version) ||
(isExactBuildRequest &&
openjdkVersion !== null &&
semver_default().compareBuild(version, openjdkVersion) === 0))
.map(({ release }) => release)
.sort((a, b) => {
return -semver_default().compareBuild(a.version, b.version);
});
const resolvedFullVersion = satisfiedVersions.length > 0 ? satisfiedVersions[0] : null;
if (!resolvedFullVersion) {
const availableVersionStrings = availableVersionsWithBinaries.map(item => item.version);
const availableVersionStrings = availableVersionsWithBinaries.map(({ release }) => release.version);
throw this.createVersionNotFoundError(version, availableVersionStrings);
}
return resolvedFullVersion;
@@ -279,6 +293,20 @@ class TemurinDistribution extends base_installer/* JavaBase */.O {
return architecture === 'armv7' ? 'arm' : architecture;
}
}
/**
* Builds a SemVer version from the OpenJDK version fields reported by the
* Adoptium API, e.g. '26.0.2.1+1' -> '26.0.2+1.1' and '25.0.4+7-LTS' ->
* '25.0.4+7'. Returns null if the fields cannot form a valid SemVer version.
*/
function getOpenJdkSemverVersion(versionData) {
const { major, minor, security, patch, build } = versionData;
if (build === undefined || build === null) {
return null;
}
const buildMetadata = patch ? `${patch}.${build}` : `${build}`;
const version = `${major}.${minor}.${security}+${buildMetadata}`;
return semver_default().valid(version) ? version : null;
}
/***/ }),
+17 -7
View File
@@ -30975,12 +30975,7 @@ function canResolveTemurinJmods(version) {
if (normalizedVersion === 'latest') {
return true;
}
let normalizedRange = normalizedVersion
.replace(/-ea$/, '')
.replace('-ea.', '+');
if (/^\d+(\.\d+){3,}$/.test(normalizedRange)) {
normalizedRange = (0,_util_js__WEBPACK_IMPORTED_MODULE_1__/* .convertVersionToSemver */ .ZY)(normalizedRange);
}
const normalizedRange = (0,_util_js__WEBPACK_IMPORTED_MODULE_1__/* .normalizeJavaVersionToSemver */ .zZ)(normalizedVersion.replace(/-ea$/, '').replace('-ea.', '+'));
if (!semver__WEBPACK_IMPORTED_MODULE_0___default().validRange(normalizedRange)) {
// JavaBase owns general version validation and its targeted error messages.
return true;
@@ -31311,7 +31306,8 @@ function validateToolchainIds(versions, versionFile, toolchainIds) {
/* harmony export */ rC: () => (/* binding */ getNextPageUrlFromLinkHeader),
/* harmony export */ ri: () => (/* binding */ getLatestMajorVersion),
/* harmony export */ y: () => (/* binding */ isVersionSatisfies),
/* harmony export */ yH: () => (/* binding */ getToolcachePath)
/* harmony export */ yH: () => (/* binding */ getToolcachePath),
/* harmony export */ zZ: () => (/* binding */ normalizeJavaVersionToSemver)
/* harmony export */ });
/* unused harmony exports getVersionFromToolcachePath, isJobStatusSuccess */
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_0__ = __nccwpck_require__(857);
@@ -31712,6 +31708,20 @@ function convertVersionToSemver(version) {
}
return mainVersion;
}
/**
* Java versions (JEP 322) can contain more numeric fields than SemVer allows,
* e.g. '11.0.9.1' or Temurin respins such as '26.0.2.1+1'. Move the extra
* fields into SemVer build metadata ('11.0.9+1', '26.0.2+1.1'). Any other
* input (ranges, regular SemVer versions) is returned unchanged.
*/
function normalizeJavaVersionToSemver(version) {
const match = /^(\d+(?:\.\d+){3,})(?:\+([0-9A-Za-z.-]+))?$/.exec(version);
if (!match) {
return version;
}
const converted = convertVersionToSemver(match[1]);
return match[2] ? `${converted}.${match[2]}` : converted;
}
/**
* Builds a validator for the bytes currently served by a URL from the response
* headers of a HEAD request. A vendor's `/latest/` URL never changes, so this
+5 -8
View File
@@ -5,9 +5,9 @@ import semver from 'semver';
import path from 'path';
import * as httpm from '@actions/http-client';
import {
convertVersionToSemver,
getToolcachePath,
isVersionSatisfies
isVersionSatisfies,
normalizeJavaVersionToSemver
} from '../util.js';
import type {
ChecksumAlgorithm,
@@ -675,12 +675,9 @@ export abstract class JavaBase {
// Java uses a versioning scheme (JEP 322) that can contain more numeric
// fields than SemVer allows, e.g. '18.0.1.1' or '11.0.9.1'. Convert such
// exact versions to SemVer build notation ('18.0.1+1') so they are
// accepted. Ranges and versions that already carry build metadata are
// left untouched.
if (/^\d+(\.\d+){3,}$/.test(version)) {
version = convertVersionToSemver(version);
}
// exact versions to SemVer build notation ('18.0.1+1', or '26.0.2+1.1'
// for '26.0.2.1+1') so they are accepted. Ranges are left untouched.
version = normalizeJavaVersionToSemver(version);
if (!semver.validRange(version)) {
throw new Error(
+4 -7
View File
@@ -1,5 +1,5 @@
import semver from 'semver';
import {convertVersionToSemver} from '../util.js';
import {normalizeJavaVersionToSemver} from '../util.js';
export enum JavaDistribution {
Temurin = 'temurin',
@@ -98,12 +98,9 @@ function canResolveTemurinJmods(version: string): boolean {
return true;
}
let normalizedRange = normalizedVersion
.replace(/-ea$/, '')
.replace('-ea.', '+');
if (/^\d+(\.\d+){3,}$/.test(normalizedRange)) {
normalizedRange = convertVersionToSemver(normalizedRange);
}
const normalizedRange = normalizeJavaVersionToSemver(
normalizedVersion.replace(/-ea$/, '').replace('-ea.', '+')
);
if (!semver.validRange(normalizedRange)) {
// JavaBase owns general version validation and its targeted error messages.
return true;
+39 -4
View File
@@ -70,7 +70,7 @@ export class TemurinDistribution extends JavaBase {
const formattedVersion = this.stable
? item.version_data.semver
: item.version_data.semver.replace('-beta+', '+');
return {
const release: JavaDownloadRelease = {
version: formattedVersion,
url: item.binaries[0].package.link,
signatureUrl: item.binaries[0].package.signature_link,
@@ -79,11 +79,29 @@ export class TemurinDistribution extends JavaBase {
value: item.binaries[0].package.checksum,
source: item.binaries[0].package.checksum_link
}
} as JavaDownloadRelease;
};
return {
release,
openjdkVersion: getOpenJdkSemverVersion(item.version_data)
};
});
// The Adoptium API `semver` folds the JEP 322 patch field into the build
// number ('26.0.2.1+1' -> '26.0.2+101') and appends extra metadata for LTS
// releases ('25.0.4+7' -> '25.0.4+7.0.LTS'). Exact versions requested by
// users follow the OpenJDK notation instead, so also match them against a
// key derived from the OpenJDK version fields ('26.0.2+1.1', '25.0.4+7').
const isExactBuildRequest = (semver.parse(version)?.build.length ?? 0) > 0;
const satisfiedVersions = availableVersionsWithBinaries
.filter(item => isVersionSatisfies(version, item.version))
.filter(
({release, openjdkVersion}) =>
isVersionSatisfies(version, release.version) ||
(isExactBuildRequest &&
openjdkVersion !== null &&
semver.compareBuild(version, openjdkVersion) === 0)
)
.map(({release}) => release)
.sort((a, b) => {
return -semver.compareBuild(a.version, b.version);
});
@@ -92,7 +110,7 @@ export class TemurinDistribution extends JavaBase {
satisfiedVersions.length > 0 ? satisfiedVersions[0] : null;
if (!resolvedFullVersion) {
const availableVersionStrings = availableVersionsWithBinaries.map(
item => item.version
({release}) => release.version
);
throw this.createVersionNotFoundError(version, availableVersionStrings);
}
@@ -309,3 +327,20 @@ export class TemurinDistribution extends JavaBase {
return architecture === 'armv7' ? 'arm' : architecture;
}
}
/**
* Builds a SemVer version from the OpenJDK version fields reported by the
* Adoptium API, e.g. '26.0.2.1+1' -> '26.0.2+1.1' and '25.0.4+7-LTS' ->
* '25.0.4+7'. Returns null if the fields cannot form a valid SemVer version.
*/
function getOpenJdkSemverVersion(
versionData: ITemurinAvailableVersions['version_data']
): string | null {
const {major, minor, security, patch, build} = versionData;
if (build === undefined || build === null) {
return null;
}
const buildMetadata = patch ? `${patch}.${build}` : `${build}`;
const version = `${major}.${minor}.${security}+${buildMetadata}`;
return semver.valid(version) ? version : null;
}
+1
View File
@@ -34,6 +34,7 @@ export interface ITemurinAvailableVersions {
minor: number;
openjdk_version: string;
security: string;
patch?: number;
semver: string;
};
}
+15
View File
@@ -510,6 +510,21 @@ export function convertVersionToSemver(version: number[] | string) {
return mainVersion;
}
/**
* Java versions (JEP 322) can contain more numeric fields than SemVer allows,
* e.g. '11.0.9.1' or Temurin respins such as '26.0.2.1+1'. Move the extra
* fields into SemVer build metadata ('11.0.9+1', '26.0.2+1.1'). Any other
* input (ranges, regular SemVer versions) is returned unchanged.
*/
export function normalizeJavaVersionToSemver(version: string): string {
const match = /^(\d+(?:\.\d+){3,})(?:\+([0-9A-Za-z.-]+))?$/.exec(version);
if (!match) {
return version;
}
const converted = convertVersionToSemver(match[1]);
return match[2] ? `${converted}.${match[2]}` : converted;
}
/**
* Builds a validator for the bytes currently served by a URL from the response
* headers of a HEAD request. A vendor's `/latest/` URL never changes, so this