diff --git a/__tests__/distributors/liberica-installer.test.ts b/__tests__/distributors/liberica-installer.test.ts index 2005f5343..2ce57c2c4 100644 --- a/__tests__/distributors/liberica-installer.test.ts +++ b/__tests__/distributors/liberica-installer.test.ts @@ -15,6 +15,7 @@ import type { } from '../../src/distributions/liberica/models.js'; import {HttpClient} from '@actions/http-client'; import os from 'os'; +import path from 'path'; import manifestData from '../data/liberica.json' with {type: 'json'}; @@ -253,35 +254,68 @@ describe('findPackageForDownload', () => { expect(result.version).toBe(expected); }); - describe('compound build versions', () => { + describe.each([ + [17, 20], + [25, 4] + ])('patch releases for Java %s.0.%s', (featureVersion, updateVersion) => { + const mainVersion = `${featureVersion}.0.${updateVersion}`; + beforeEach(() => { distribution['getAvailableVersions'] = async () => - ['1', '1.1', '1.2', '1.10'].map(build => ({ - featureVersion: 25, + ['+1', '+10', '.1+1', '.1+2', '.1+10', '.2+1', '.10+1'].map(suffix => ({ + featureVersion, interimVersion: 0, - updateVersion: 4, - buildVersion: 1, - version: `25.0.4+${build}`, - downloadUrl: `https://download.bell-sw.com/java/25.0.4+${build}/bellsoft-jdk25.0.4+${build}-macos-aarch64.tar.gz` + updateVersion, + buildVersion: Number(suffix.split('+')[1]), + version: `${mainVersion}${suffix}`, + downloadUrl: `https://download.bell-sw.com/java/${mainVersion}${suffix}/bellsoft-jdk${mainVersion}${suffix}-macos-aarch64.tar.gz` })); }); it.each([ - ['25.0.4+1.1', '25.0.4+1.1'], - ['25.0.4+1', '25.0.4+1'], - ['25.0.4', '25.0.4+1.10'], - ['25', '25.0.4+1.10'] - ])('version is %s -> %s', async (input, expected) => { + [`${mainVersion}+1.1`, `${mainVersion}+1.1`, '.1+1'], + [`${mainVersion}+1.2`, `${mainVersion}+1.2`, '.1+2'], + [`${mainVersion}+1.10`, `${mainVersion}+1.10`, '.1+10'], + [`${mainVersion}+1`, `${mainVersion}+1`, '+1'], + [mainVersion, `${mainVersion}+10.1`, '.10+1'], + [`${featureVersion}`, `${mainVersion}+10.1`, '.10+1'] + ])('version is %s -> %s', async (input, expected, suffix) => { const result = await distribution['findPackageForDownload'](input); expect(result).toEqual({ version: expected, - url: `https://download.bell-sw.com/java/${expected}/bellsoft-jdk${expected}-macos-aarch64.tar.gz` + url: `https://download.bell-sw.com/java/${mainVersion}${suffix}/bellsoft-jdk${mainVersion}${suffix}-macos-aarch64.tar.gz` }); }); + it('sorts builds within the same patch numerically', async () => { + const versions = await distribution['getAvailableVersions'](); + distribution['getAvailableVersions'] = async () => + versions.filter(item => item.version.startsWith(`${mainVersion}.1+`)); + + const result = await distribution['findPackageForDownload'](mainVersion); + expect(result.version).toBe(`${mainVersion}+1.10`); + }); + + it('selects the newest build for a four-component patch-only request', async () => { + const patchDistribution = new LibericaDistributions({ + version: `${mainVersion}.1`, + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + patchDistribution['getAvailableVersions'] = + distribution['getAvailableVersions']; + + await expect( + patchDistribution['findPackageForDownload']( + patchDistribution['version'] + ) + ).resolves.toMatchObject({version: `${mainVersion}+1.10`}); + }); + it('does not substitute a different compound build', async () => { await expect( - distribution['findPackageForDownload']('25.0.4+1.3') + distribution['findPackageForDownload'](`${mainVersion}+1.3`) ).rejects.toThrow(/No matching version found for SemVer/); }); }); @@ -293,6 +327,160 @@ describe('findPackageForDownload', () => { }); }); +describe('patch request resolution from the constructor', () => { + const releases: LibericaVersion[] = ['17.0.20+10', '17.0.20.1+1'].map( + version => ({ + featureVersion: 17, + interimVersion: 0, + updateVersion: 20, + buildVersion: Number(version.split('+')[1]), + version, + downloadUrl: `https://download.bell-sw.com/java/${version}/jdk.tar.gz` + }) + ); + let cacheRoot: string; + let previousToolcache: string | undefined; + + beforeEach(async () => { + previousToolcache = process.env['RUNNER_TOOL_CACHE']; + cacheRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'liberica-')); + process.env['RUNNER_TOOL_CACHE'] = cacheRoot; + for (const version of ['17.0.20-10', '17.0.20-1.1']) { + const javaPath = path.join( + cacheRoot, + 'Java_Liberica_jdk', + version, + 'x64' + ); + await fs.promises.mkdir(javaPath, {recursive: true}); + await fs.promises.writeFile(`${javaPath}.complete`, ''); + } + }); + + afterEach(async () => { + if (previousToolcache === undefined) { + delete process.env['RUNNER_TOOL_CACHE']; + } else { + process.env['RUNNER_TOOL_CACHE'] = previousToolcache; + } + await fs.promises.rm(cacheRoot, {recursive: true, force: true}); + }); + + it.each([ + ['17', '17.0.20+1.1', '17.0.20.1+1'], + ['17.0.20', '17.0.20+1.1', '17.0.20.1+1'], + ['17.0.20+10', '17.0.20+10', '17.0.20+10'], + ['17.0.20+1.1', '17.0.20+1.1', '17.0.20.1+1'], + ['17.0.20.1', '17.0.20+1.1', '17.0.20.1+1'], + ['17.0.20.1+1', '17.0.20+1.1', '17.0.20.1+1'], + ['17.0.20.0', '17.0.20+10', '17.0.20+10'] + ])( + 'resolves %s consistently remotely and in cache', + async (input, expected, apiVersion) => { + const distribution = new LibericaDistributions({ + version: input, + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + distribution['getAvailableVersions'] = async () => releases; + + await expect( + distribution['findPackageForDownload'](distribution['version']) + ).resolves.toEqual({ + version: expected, + url: `https://download.bell-sw.com/java/${apiVersion}/jdk.tar.gz` + }); + expect(distribution['findInToolcache']()).toEqual({ + version: expected, + path: path.join( + cacheRoot, + 'Java_Liberica_jdk', + expected.replace('+', '-'), + 'x64' + ) + }); + } + ); + + it.each(['17.0.20.2', '17.0.20+1.2'])( + 'does not substitute a different patch or exact build for %s', + async version => { + const distribution = new LibericaDistributions({ + version, + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + distribution['getAvailableVersions'] = async () => releases; + + await expect( + distribution['findPackageForDownload'](distribution['version']) + ).rejects.toThrow(/No matching version found/); + expect(distribution['findInToolcache']()).toBeNull(); + } + ); + + it('keeps patch-only and exact-build resolution cache identities distinct', () => { + const distribution = new LibericaDistributions({ + version: '17.0.20.1', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + expect(distribution['version']).toBe('17.0.20.1'); + }); + + it('orders main versions, patches, and builds consistently in cache', async () => { + for (const version of ['17.0.20-1.2', '17.0.20-1.10', '17.0.21-1']) { + const javaPath = path.join( + cacheRoot, + 'Java_Liberica_jdk', + version, + 'x64' + ); + await fs.promises.mkdir(javaPath, {recursive: true}); + await fs.promises.writeFile(`${javaPath}.complete`, ''); + } + + for (const [version, expected] of [ + ['17', '17.0.21+1'], + ['17.0.20', '17.0.20+1.10'], + ['17.0.20.1', '17.0.20+1.10'] + ]) { + const distribution = new LibericaDistributions({ + version, + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + expect(distribution['findInToolcache']()?.version).toBe(expected); + } + }); + + it('preserves early-access handling for patch-only requests', async () => { + const javaPath = path.join( + cacheRoot, + 'Java_Liberica_jdk', + '17.0.20-ea.1.2', + 'x64' + ); + await fs.promises.mkdir(javaPath, {recursive: true}); + await fs.promises.writeFile(`${javaPath}.complete`, ''); + const distribution = new LibericaDistributions({ + version: '17.0.20.1-ea', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + expect(distribution['stable']).toBe(false); + expect(distribution['findInToolcache']()).toEqual({ + version: '17.0.20+1.2', + path: javaPath + }); + }); +}); + describe('getPlatformOption', () => { beforeEach(() => { // The linux row below is glibc, so pin the Alpine probe rather than @@ -369,6 +557,36 @@ describe('convertVersionToSemver', () => { }, '11.0.0+13' ], + [ + { + version: '17.0.20.1+1', + featureVersion: 17, + interimVersion: 0, + updateVersion: 20, + buildVersion: 1 + }, + '17.0.20+1.1' + ], + [ + { + version: '25.0.4.1+1', + featureVersion: 25, + interimVersion: 0, + updateVersion: 4, + buildVersion: 1 + }, + '25.0.4+1.1' + ], + [ + { + version: '17.0.20.1+2', + featureVersion: 17, + interimVersion: 0, + updateVersion: 20, + buildVersion: 2 + }, + '17.0.20+1.2' + ], [ { version: '25.0.4+1.1', diff --git a/dist/setup/242.index.js b/dist/setup/242.index.js index 1b0369a65..ccbf91db4 100644 --- a/dist/setup/242.index.js +++ b/dist/setup/242.index.js @@ -378,7 +378,7 @@ class JavaBase { core/* info */.pq(`Java ${foundJava.version} was downloaded`); if (javaRelease.floating) { if (!semver_default().valid(foundJava.version) || - !(0,util/* isVersionSatisfies */.y)(this.version, foundJava.version)) { + !this.isVersionSatisfied(this.version, foundJava.version)) { throw new Error(`The downloaded ${this.distribution} artifact reported Java ${foundJava.version}, which does not satisfy '${this.version}'.`); } javaRelease = { ...javaRelease, version: foundJava.version }; @@ -501,7 +501,7 @@ class JavaBase { return javaRelease; } if (!semver_default().valid(restored.release.version) || - !(0,util/* isVersionSatisfies */.y)(this.version, restored.release.version)) { + !this.isVersionSatisfied(this.version, restored.release.version)) { core/* debug */.Yz(`Ignoring the cached concrete version '${restored.release.version}' for ${this.distribution} ${this.version}.`); return javaRelease; } @@ -684,10 +684,10 @@ class JavaBase { }) .filter(item => item.stable === this.stable); const satisfiedVersions = availableVersions - .filter(item => (0,util/* isVersionSatisfies */.y)(this.version, item.version)) + .filter(item => this.isVersionSatisfied(this.version, item.version)) .filter(item => item.path) .sort((a, b) => { - return -semver_default().compareBuild(a.version, b.version); + return -this.compareVersions(a.version, b.version); }); if (!satisfiedVersions || satisfiedVersions.length === 0) { return null; @@ -697,6 +697,12 @@ class JavaBase { path: satisfiedVersions[0].path }; } + isVersionSatisfied(range, version) { + return (0,util/* isVersionSatisfies */.y)(range, version); + } + compareVersions(a, b) { + return semver_default().compareBuild(a, b); + } normalizeVersion(version) { let stable = true; const latest = false; diff --git a/dist/setup/63.index.js b/dist/setup/63.index.js index 3b8ac9758..44eb30a17 100644 --- a/dist/setup/63.index.js +++ b/dist/setup/63.index.js @@ -52,14 +52,46 @@ class LibericaDistributions extends _base_installer_js__WEBPACK_IMPORTED_MODULE_ version: this.convertVersionToSemver(item) })); const satisfiedVersion = availableVersions - .filter(item => (0,_util_js__WEBPACK_IMPORTED_MODULE_2__/* .isVersionSatisfies */ .y)(range, item.version)) - .sort((a, b) => -semver__WEBPACK_IMPORTED_MODULE_1___default().compareBuild(a.version, b.version))[0]; + .filter(item => this.isVersionSatisfied(range, item.version)) + .sort((a, b) => -this.compareVersions(a.version, b.version))[0]; if (!satisfiedVersion) { const availableVersionStrings = availableVersions.map(item => item.version); throw this.createVersionNotFoundError(range, availableVersionStrings); } return satisfiedVersion; } + normalizeVersion(version) { + const normalized = super.normalizeVersion(version); + if (/^\d+\.\d+\.\d+\.\d+(?:-ea)?$/.test(version)) { + // Keep patch-only requests distinct from exact SDKMAN build requests. + normalized.version = version.replace(/-ea$/, ''); + } + return normalized; + } + isVersionSatisfied(range, version) { + const patchRequest = range.match(/^(\d+\.\d+\.\d+)\.(\d+)$/); + if (patchRequest) { + const candidate = semver__WEBPACK_IMPORTED_MODULE_1___default().parse(version); + if (!candidate || !semver__WEBPACK_IMPORTED_MODULE_1___default().eq(patchRequest[1], candidate)) { + return false; + } + const patch = candidate.build.length > 1 ? candidate.build[0] : '0'; + return patch === patchRequest[2]; + } + return super.isVersionSatisfied(range, version); + } + compareVersions(a, b) { + const mainComparison = semver__WEBPACK_IMPORTED_MODULE_1___default().compare(a, b); + if (mainComparison !== 0) { + return mainComparison; + } + // SDKMAN encodes patch.build, but unpatched releases contain only build. + const patchAndBuild = (version) => { + const build = new (semver__WEBPACK_IMPORTED_MODULE_1___default().SemVer)(version).build; + return (build.length > 1 ? build : ['0', ...build]).join('.'); + }; + return semver__WEBPACK_IMPORTED_MODULE_1___default().compareBuild(`0.0.0+${patchAndBuild(a)}`, `0.0.0+${patchAndBuild(b)}`); + } async getAvailableVersions() { if (_actions_core__WEBPACK_IMPORTED_MODULE_3__/* .isDebug */ ._o()) { console.time('Retrieving available versions for Liberica took'); // eslint-disable-line no-console @@ -131,6 +163,10 @@ class LibericaDistributions extends _base_installer_js__WEBPACK_IMPORTED_MODULE_ const { featureVersion, interimVersion, updateVersion } = version; const buildVersion = version.version.split('+')[1] || version.buildVersion; const mainVersion = [featureVersion, interimVersion, updateVersion].join('.'); + const patchVersion = version.version.split('+')[0].split('.')[3]; + if (patchVersion) { + return `${mainVersion}+${patchVersion}.${buildVersion}`; + } if (buildVersion != 0) { return `${mainVersion}+${buildVersion}`; } diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index dedca4ed4..8e1a61507 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -83,6 +83,12 @@ steps: ### Liberica +Exact patch releases use SDKMAN's `X.Y.Z+P.B` notation, including in `.sdkmanrc`. +For example, `java=17.0.20+1.1-librca` selects BellSoft's `17.0.20.1+1` release. +Alternatively, `java-version: '17.0.20.1'` selects the newest build of patch 1, +whereas `java-version: '17.0.20+1'` selects the exact unpatched build 1. +Floating versions such as `17` prefer newer patches before comparing build numbers. + ```yaml steps: - uses: actions/checkout@v7 diff --git a/src/distributions/base-installer.ts b/src/distributions/base-installer.ts index 32c7455c1..42f4b7de9 100644 --- a/src/distributions/base-installer.ts +++ b/src/distributions/base-installer.ts @@ -241,7 +241,7 @@ export abstract class JavaBase { if (javaRelease.floating) { if ( !semver.valid(foundJava.version) || - !isVersionSatisfies(this.version, foundJava.version) + !this.isVersionSatisfied(this.version, foundJava.version) ) { throw new Error( `The downloaded ${this.distribution} artifact reported Java ${foundJava.version}, which does not satisfy '${this.version}'.` @@ -401,7 +401,7 @@ export abstract class JavaBase { } if ( !semver.valid(restored.release.version) || - !isVersionSatisfies(this.version, restored.release.version) + !this.isVersionSatisfied(this.version, restored.release.version) ) { core.debug( `Ignoring the cached concrete version '${restored.release.version}' for ${this.distribution} ${this.version}.` @@ -622,10 +622,10 @@ export abstract class JavaBase { .filter(item => item.stable === this.stable); const satisfiedVersions = availableVersions - .filter(item => isVersionSatisfies(this.version, item.version)) + .filter(item => this.isVersionSatisfied(this.version, item.version)) .filter(item => item.path) .sort((a, b) => { - return -semver.compareBuild(a.version, b.version); + return -this.compareVersions(a.version, b.version); }); if (!satisfiedVersions || satisfiedVersions.length === 0) { return null; @@ -637,6 +637,14 @@ export abstract class JavaBase { }; } + protected isVersionSatisfied(range: string, version: string): boolean { + return isVersionSatisfies(range, version); + } + + protected compareVersions(a: string, b: string): number { + return semver.compareBuild(a, b); + } + protected normalizeVersion(version: string) { let stable = true; const latest = false; diff --git a/src/distributions/liberica/installer.ts b/src/distributions/liberica/installer.ts index b4320d66a..41737cea3 100644 --- a/src/distributions/liberica/installer.ts +++ b/src/distributions/liberica/installer.ts @@ -9,7 +9,6 @@ import { cacheJdkDir, extractJdkFile, getDownloadArchiveExtension, - isVersionSatisfies, renameWinArchive } from '../../util.js'; import * as core from '@actions/core'; @@ -66,8 +65,8 @@ export class LibericaDistributions extends JavaBase { })); const satisfiedVersion = availableVersions - .filter(item => isVersionSatisfies(range, item.version)) - .sort((a, b) => -semver.compareBuild(a.version, b.version))[0]; + .filter(item => this.isVersionSatisfied(range, item.version)) + .sort((a, b) => -this.compareVersions(a.version, b.version))[0]; if (!satisfiedVersion) { const availableVersionStrings = availableVersions.map( @@ -79,6 +78,45 @@ export class LibericaDistributions extends JavaBase { return satisfiedVersion; } + protected normalizeVersion(version: string) { + const normalized = super.normalizeVersion(version); + if (/^\d+\.\d+\.\d+\.\d+(?:-ea)?$/.test(version)) { + // Keep patch-only requests distinct from exact SDKMAN build requests. + normalized.version = version.replace(/-ea$/, ''); + } + return normalized; + } + + protected isVersionSatisfied(range: string, version: string): boolean { + const patchRequest = range.match(/^(\d+\.\d+\.\d+)\.(\d+)$/); + if (patchRequest) { + const candidate = semver.parse(version); + if (!candidate || !semver.eq(patchRequest[1], candidate)) { + return false; + } + const patch = candidate.build.length > 1 ? candidate.build[0] : '0'; + return patch === patchRequest[2]; + } + return super.isVersionSatisfied(range, version); + } + + protected compareVersions(a: string, b: string): number { + const mainComparison = semver.compare(a, b); + if (mainComparison !== 0) { + return mainComparison; + } + + // SDKMAN encodes patch.build, but unpatched releases contain only build. + const patchAndBuild = (version: string): string => { + const build = new semver.SemVer(version).build; + return (build.length > 1 ? build : ['0', ...build]).join('.'); + }; + return semver.compareBuild( + `0.0.0+${patchAndBuild(a)}`, + `0.0.0+${patchAndBuild(b)}` + ); + } + private async getAvailableVersions(): Promise { if (core.isDebug()) { console.time('Retrieving available versions for Liberica took'); // eslint-disable-line no-console @@ -171,6 +209,10 @@ export class LibericaDistributions extends JavaBase { const mainVersion = [featureVersion, interimVersion, updateVersion].join( '.' ); + const patchVersion = version.version.split('+')[0].split('.')[3]; + if (patchVersion) { + return `${mainVersion}+${patchVersion}.${buildVersion}`; + } if (buildVersion != 0) { return `${mainVersion}+${buildVersion}`; }