From 61632ad9b25f9050f423d8d390d890e7ea4af2ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Nov 2026 04:10:24 +0000 Subject: [PATCH] Fix JetBrains hotfix version selection and cache precedence --- .../distributors/jetbrains-installer.test.ts | 186 ++++++++++++++++++ dist/setup/282.index.js | 44 ++++- src/distributions/jetbrains/installer.ts | 65 +++++- 3 files changed, 288 insertions(+), 7 deletions(-) diff --git a/__tests__/distributors/jetbrains-installer.test.ts b/__tests__/distributors/jetbrains-installer.test.ts index ebc39365..9c3b5abf 100644 --- a/__tests__/distributors/jetbrains-installer.test.ts +++ b/__tests__/distributors/jetbrains-installer.test.ts @@ -9,6 +9,9 @@ import { afterAll } from '@jest/globals'; import https from 'https'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; import {HttpClient, HttpClientResponse} from '@actions/http-client'; import type {IncomingMessage} from 'http'; import {Readable} from 'stream'; @@ -476,6 +479,109 @@ describe('findPackageForDownload', () => { } ); + describe('hotfix releases', () => { + beforeEach(() => { + jest.spyOn(HttpClient.prototype, 'getJson').mockResolvedValue({ + statusCode: 200, + headers: {}, + result: [ + release('jbr-release-25.0.4b508.27', false), + release('jbr-release-25.0.4.1b635.9', false), + release('jbr-release-25.0.4.1b635.79', false), + release('jbr-release-25.0.3.2b1000.1', false) + ] + }); + jest.spyOn(HttpClient.prototype, 'head').mockResolvedValue(response(200)); + }); + + it.each(['linux', 'osx', 'windows'])( + 'selects the patched JBR archive and checksum on %s', + async platform => { + const distribution = new JetBrainsDistribution({ + version: '25', + architecture: 'x64', + packageType: 'jdk', + checkLatest: true + }); + distribution['getPlatformOption'] = () => platform; + const url = `https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-25.0.4.1-${platform}-x64-b635.79.tar.gz`; + + const result = await distribution['findPackageForDownload']('25'); + + expect(result).toEqual({ + version: '25.0.4+1.635.79', + url, + checksum: { + algorithm: 'sha512', + value: JETBRAINS_CHECKSUM, + source: `${url}.checksum` + } + }); + expect(HttpClient.prototype.head).toHaveBeenCalledWith(url); + expect(spyHttpClientGet).toHaveBeenCalledTimes(1); + expect(spyHttpClientGet).toHaveBeenCalledWith(`${url}.checksum`); + } + ); + + it.each([ + ['latest', '25.0.4+1.635.79'], + ['25.0.4', '25.0.4+1.635.79'], + ['25.0.4.0', '25.0.4+508.27'], + ['25.0.4.1', '25.0.4+1.635.79'], + ['25.0.4.1+635.79', '25.0.4+1.635.79'], + ['25.0.4+1.635.9', '25.0.4+1.635.9'], + ['25.0.4+508.27', '25.0.4+508.27'] + ])('resolves %s to %s', async (input, expected) => { + const distribution = new JetBrainsDistribution({ + version: input, + architecture: 'x64', + packageType: 'jdk', + checkLatest: true + }); + + await expect( + distribution['findPackageForDownload'](distribution['version']) + ).resolves.toMatchObject({version: expected}); + }); + + it('resolves early-access hotfix releases separately', async () => { + jest.spyOn(HttpClient.prototype, 'getJson').mockResolvedValue({ + statusCode: 200, + headers: {}, + result: [ + release('jbr-release-25.0.4.1b635.79', true), + release('jbr-release-25.0.4.2b636.1', false) + ] + }); + const distribution = new JetBrainsDistribution({ + version: '25.0.4.1-ea', + architecture: 'x64', + packageType: 'jdk', + checkLatest: true + }); + + await expect( + distribution['findPackageForDownload'](distribution['version']) + ).resolves.toMatchObject({version: '25.0.4+1.635.79'}); + }); + + it.each(['25.0.4.2', '25.0.4.1+635.80'])( + 'does not substitute another patch or build for %s', + async input => { + const distribution = new JetBrainsDistribution({ + version: input, + architecture: 'x64', + packageType: 'jdk', + checkLatest: true + }); + + await expect( + distribution['findPackageForDownload'](distribution['version']) + ).rejects.toThrow(/No matching version found/); + } + ); + }); + it('version is not found', async () => { const distribution = new JetBrainsDistribution({ version: '8.0.452', @@ -569,4 +675,84 @@ describe('findPackageForDownload', () => { source: `${result.url}.checksum` }); }); + + describe('compareVersions', () => { + it.each([ + ['25.0.4+1.635.79', '25.0.4+508.27', 1], + ['25.0.5+1.1', '25.0.4+1.635.79', 1], + ['25.0.4+10.1.1', '25.0.4+2.999.99', 1], + ['25.0.4+1.635.79', '25.0.4+1.635.9', 1], + ['25.0.4+1.636.1', '25.0.4+1.635.79', 1], + ['25.0.4+0.635.79', '25.0.4+635.79', 0], + ['11.0.11+1522', '11.0.11+1522.1', -1], + ['25.0.4', '25.0.4+508.27', -1] + ])('compares %s with %s', (first, second, expected) => { + const distribution = new JetBrainsDistribution({ + version: '25', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + + expect(Math.sign(distribution['compareVersions'](first, second))).toBe( + expected + ); + expect( + Math.sign(distribution['compareVersions'](second, first)) || 0 + ).toBe(-expected || 0); + }); + }); + + describe('hotfix releases in the tool cache', () => { + let cacheRoot: string; + const previousToolcache = process.env['RUNNER_TOOL_CACHE']; + + beforeEach(async () => { + cacheRoot = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'jetbrains-') + ); + process.env['RUNNER_TOOL_CACHE'] = cacheRoot; + for (const version of ['25.0.4-508.27', '25.0.4-1.635.79']) { + const javaPath = path.join( + cacheRoot, + 'Java_JetBrains_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(['25', '25.0.4.1', '25.0.4.1+635.79'])( + 'reuses the patched JBR for %s', + input => { + const distribution = new JetBrainsDistribution({ + version: input, + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + + expect(distribution['findInToolcache']()).toEqual({ + version: '25.0.4+1.635.79', + path: path.join( + cacheRoot, + 'Java_JetBrains_jdk', + '25.0.4-1.635.79', + 'x64' + ) + }); + } + ); + }); }); diff --git a/dist/setup/282.index.js b/dist/setup/282.index.js index 68edab3e..fc7217a9 100644 --- a/dist/setup/282.index.js +++ b/dist/setup/282.index.js @@ -34,16 +34,16 @@ class JetBrainsDistribution extends _base_installer_js__WEBPACK_IMPORTED_MODULE_ async findPackageForDownload(range) { const versionsRaw = await this.getAvailableVersions(); const versions = versionsRaw.map(v => { - const formattedVersion = `${v.semver}+${v.build}`; + const formattedVersion = (0,_util_js__WEBPACK_IMPORTED_MODULE_5__/* .normalizeJavaVersionToSemver */ .zZ)(`${v.semver}+${v.build}`); return { version: formattedVersion, url: v.url }; }); const satisfiedVersions = versions - .filter(item => (0,_util_js__WEBPACK_IMPORTED_MODULE_5__/* .isVersionSatisfies */ .y)(range, item.version)) + .filter(item => this.isVersionSatisfied(range, item.version)) .sort((a, b) => { - return -semver__WEBPACK_IMPORTED_MODULE_3___default().compareBuild(a.version, b.version); + return -this.compareVersions(a.version, b.version); }); const resolvedFullVersion = satisfiedVersions.length > 0 ? satisfiedVersions[0] : null; if (!resolvedFullVersion) { @@ -59,6 +59,44 @@ class JetBrainsDistribution extends _base_installer_js__WEBPACK_IMPORTED_MODULE_ checksum: await this.fetchChecksum(`${resolvedFullVersion.url}.checksum`, ['sha512', 'sha256']) }; } + normalizeVersion(version) { + const normalized = super.normalizeVersion(version); + if (/^\d+\.\d+\.\d+(?:\.\d+)+(?:-ea)?$/.test(version)) { + // A patch-only request must match any JBR build of that patch. + 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_3___default().parse(version); + return (candidate !== null && + semver__WEBPACK_IMPORTED_MODULE_3___default().eq(patchRequest[1], candidate) && + (candidate.build.slice(0, -2).join('.') || '0') === + patchRequest[2].slice(1)); + } + return super.isVersionSatisfied(range, version); + } + compareVersions(a, b) { + const mainComparison = semver__WEBPACK_IMPORTED_MODULE_3___default().compare(a, b); + if (mainComparison !== 0) { + return mainComparison; + } + // JBR builds have two components (one in older releases). Any preceding + // metadata components are Java patch fields and take priority over builds. + const first = new (semver__WEBPACK_IMPORTED_MODULE_3___default().SemVer)(a).build; + const second = new (semver__WEBPACK_IMPORTED_MODULE_3___default().SemVer)(b).build; + const firstPatch = first.slice(0, -2); + const secondPatch = second.slice(0, -2); + for (let index = 0; index < Math.max(firstPatch.length, secondPatch.length); index++) { + const patchComparison = semver__WEBPACK_IMPORTED_MODULE_3___default().compareIdentifiers(firstPatch[index] ?? '0', secondPatch[index] ?? '0'); + if (patchComparison !== 0) { + return patchComparison; + } + } + return semver__WEBPACK_IMPORTED_MODULE_3___default().compareBuild(first.length ? `0.0.0+${first.slice(-2).join('.')}` : '0.0.0', second.length ? `0.0.0+${second.slice(-2).join('.')}` : '0.0.0'); + } async downloadTool(javaRelease) { _actions_core__WEBPACK_IMPORTED_MODULE_0__/* .info */ .pq(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); const javaArchivePath = await this.downloadAndVerify(javaRelease); diff --git a/src/distributions/jetbrains/installer.ts b/src/distributions/jetbrains/installer.ts index 79b27d67..47a4fd52 100644 --- a/src/distributions/jetbrains/installer.ts +++ b/src/distributions/jetbrains/installer.ts @@ -16,8 +16,8 @@ import { extractJdkFile, getGitHubToken, getNextPageUrlFromLinkHeader, - isVersionSatisfies, MAX_PAGINATION_PAGES, + normalizeJavaVersionToSemver, validatePaginationUrl } from '../../util.js'; import {OutgoingHttpHeaders} from 'http'; @@ -38,7 +38,9 @@ export class JetBrainsDistribution extends JavaBase { const versionsRaw = await this.getAvailableVersions(); const versions = versionsRaw.map(v => { - const formattedVersion = `${v.semver}+${v.build}`; + const formattedVersion = normalizeJavaVersionToSemver( + `${v.semver}+${v.build}` + ); return { version: formattedVersion, @@ -47,9 +49,9 @@ export class JetBrainsDistribution extends JavaBase { }); const satisfiedVersions = versions - .filter(item => isVersionSatisfies(range, item.version)) + .filter(item => this.isVersionSatisfied(range, item.version)) .sort((a, b) => { - return -semver.compareBuild(a.version, b.version); + return -this.compareVersions(a.version, b.version); }); const resolvedFullVersion = @@ -74,6 +76,61 @@ export class JetBrainsDistribution extends JavaBase { }; } + protected normalizeVersion(version: string) { + const normalized = super.normalizeVersion(version); + if (/^\d+\.\d+\.\d+(?:\.\d+)+(?:-ea)?$/.test(version)) { + // A patch-only request must match any JBR build of that patch. + 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); + return ( + candidate !== null && + semver.eq(patchRequest[1], candidate) && + (candidate.build.slice(0, -2).join('.') || '0') === + patchRequest[2].slice(1) + ); + } + return super.isVersionSatisfied(range, version); + } + + protected compareVersions(a: string, b: string): number { + const mainComparison = semver.compare(a, b); + if (mainComparison !== 0) { + return mainComparison; + } + + // JBR builds have two components (one in older releases). Any preceding + // metadata components are Java patch fields and take priority over builds. + const first = new semver.SemVer(a).build; + const second = new semver.SemVer(b).build; + const firstPatch = first.slice(0, -2); + const secondPatch = second.slice(0, -2); + for ( + let index = 0; + index < Math.max(firstPatch.length, secondPatch.length); + index++ + ) { + const patchComparison = semver.compareIdentifiers( + firstPatch[index] ?? '0', + secondPatch[index] ?? '0' + ); + if (patchComparison !== 0) { + return patchComparison; + } + } + + return semver.compareBuild( + first.length ? `0.0.0+${first.slice(-2).join('.')}` : '0.0.0', + second.length ? `0.0.0+${second.slice(-2).join('.')}` : '0.0.0' + ); + } + protected async downloadTool( javaRelease: JavaDownloadRelease ): Promise {