From 47b36480ae3ac5671badb6bde2dec0c1898a1cc9 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Fri, 2 Oct 2026 00:09:56 -0400 Subject: [PATCH] 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> --- README.md | 2 +- __tests__/distributors/base-installer.test.ts | 2 + .../distributors/distribution-factory.test.ts | 4 +- .../distributors/temurin-installer.test.ts | 91 +++++++++++++++++++ __tests__/util.test.ts | 17 ++++ dist/cleanup/index.js | 16 +++- dist/setup/242.index.js | 9 +- dist/setup/463.index.js | 34 ++++++- dist/setup/index.js | 24 +++-- src/distributions/base-installer.ts | 13 +-- src/distributions/package-types.ts | 11 +-- src/distributions/temurin/installer.ts | 43 ++++++++- src/distributions/temurin/models.ts | 1 + src/util.ts | 15 +++ 14 files changed, 243 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index a8f97411..5fe1cca9 100644 --- a/README.md +++ b/README.md @@ -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` | diff --git a/__tests__/distributors/base-installer.test.ts b/__tests__/distributors/base-installer.test.ts index 281f73e5..1016a8fe 100644 --- a/__tests__/distributors/base-installer.test.ts +++ b/__tests__/distributors/base-installer.test.ts @@ -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}] diff --git a/__tests__/distributors/distribution-factory.test.ts b/__tests__/distributors/distribution-factory.test.ts index c473373a..c60e7f75 100644 --- a/__tests__/distributors/distribution-factory.test.ts +++ b/__tests__/distributors/distribution-factory.test.ts @@ -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( diff --git a/__tests__/distributors/temurin-installer.test.ts b/__tests__/distributors/temurin-installer.test.ts index 815ec26a..2d424b33 100644 --- a/__tests__/distributors/temurin-installer.test.ts +++ b/__tests__/distributors/temurin-installer.test.ts @@ -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 + ) => ({ + 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( { diff --git a/__tests__/util.test.ts b/__tests__/util.test.ts index 5ebd5344..87b10b74 100644 --- a/__tests__/util.test.ts +++ b/__tests__/util.test.ts @@ -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([ [ diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js index d22ce764..f5758761 100644 --- a/dist/cleanup/index.js +++ b/dist/cleanup/index.js @@ -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 diff --git a/dist/setup/242.index.js b/dist/setup/242.index.js index 12e3d457..1b0369a6 100644 --- a/dist/setup/242.index.js +++ b/dist/setup/242.index.js @@ -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`); } diff --git a/dist/setup/463.index.js b/dist/setup/463.index.js index 226dd024..5267d4c8 100644 --- a/dist/setup/463.index.js +++ b/dist/setup/463.index.js @@ -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; +} /***/ }), diff --git a/dist/setup/index.js b/dist/setup/index.js index ee7cd382..1aede103 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -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 diff --git a/src/distributions/base-installer.ts b/src/distributions/base-installer.ts index f2d0c141..32c7455c 100644 --- a/src/distributions/base-installer.ts +++ b/src/distributions/base-installer.ts @@ -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( diff --git a/src/distributions/package-types.ts b/src/distributions/package-types.ts index 9b0d23bd..81ab3784 100644 --- a/src/distributions/package-types.ts +++ b/src/distributions/package-types.ts @@ -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; diff --git a/src/distributions/temurin/installer.ts b/src/distributions/temurin/installer.ts index e01535e9..2b81d772 100644 --- a/src/distributions/temurin/installer.ts +++ b/src/distributions/temurin/installer.ts @@ -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; +} diff --git a/src/distributions/temurin/models.ts b/src/distributions/temurin/models.ts index c5bff902..443d3bb2 100644 --- a/src/distributions/temurin/models.ts +++ b/src/distributions/temurin/models.ts @@ -34,6 +34,7 @@ export interface ITemurinAvailableVersions { minor: number; openjdk_version: string; security: string; + patch?: number; semver: string; }; } diff --git a/src/util.ts b/src/util.ts index e2583849..b71a7362 100644 --- a/src/util.ts +++ b/src/util.ts @@ -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