diff --git a/README.md b/README.md index e2645633..25a3e6c2 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,7 @@ Deprecated aliases `jdkFile`, `server-username`, `server-password`, and `gpg-pas | `microsoft` | [Microsoft Build of OpenJDK](https://www.microsoft.com/openjdk) | [License](https://docs.microsoft.com/java/openjdk/faq) | | `oracle` | [Oracle JDK](https://www.oracle.com/java/technologies/downloads/) | [License](https://java.com/freeuselicense) | | `oracle-openjdk` | [Oracle OpenJDK](https://jdk.java.net/) | [License](https://openjdk.org/legal/gplv2+ce.html) | +| `redhat` | [Red Hat Build of OpenJDK](https://developers.redhat.com/products/openjdk/overview) | [License](https://openjdk.org/legal/gplv2+ce.html) | | `sapmachine` | [SAP SapMachine JDK/JRE](https://sapmachine.io/) | [License](https://github.com/SAP/SapMachine/blob/sapmachine/LICENSE) | | `semeru` | [IBM Semeru Runtime Open Edition](https://developer.ibm.com/languages/java/semeru-runtimes/downloads/) | [License](https://openjdk.java.net/legal/gplv2+ce.html) | | `temurin` | [Eclipse Temurin](https://adoptium.net/) | [License](https://adoptium.net/about.html) | diff --git a/__tests__/data/redhat-package-details.json b/__tests__/data/redhat-package-details.json new file mode 100644 index 00000000..5a954df4 --- /dev/null +++ b/__tests__/data/redhat-package-details.json @@ -0,0 +1,11 @@ +{ + "result": [ + { + "filename": "java-21-openjdk-21.0.8.0.9-1.portable.jdk.x86_64.tar.xz", + "direct_download_uri": "https://developers.redhat.com/content-gateway/file/pub/openjdk/java-21-openjdk-21.0.8.0.9-1.portable.jdk.x86_64.tar.xz", + "checksum": "91a6f3284cbded9de17f65985a4fca48dd4fd0aa295162178631c23ade335aad", + "checksum_type": "sha256" + } + ], + "message": "" +} diff --git a/__tests__/data/redhat-packages.json b/__tests__/data/redhat-packages.json new file mode 100644 index 00000000..427551e3 --- /dev/null +++ b/__tests__/data/redhat-packages.json @@ -0,0 +1,71 @@ +{ + "result": [ + { + "id": "redhat-21.0.8-jdk-linux-x64", + "archive_type": "tar.xz", + "distribution": "redhat", + "java_version": "21.0.8+9", + "release_status": "ga", + "operating_system": "linux", + "architecture": "x64", + "package_type": "jdk", + "directly_downloadable": true + }, + { + "id": "redhat-21.0.7-jdk-linux-x64", + "archive_type": "tar.xz", + "distribution": "redhat", + "java_version": "21.0.7+6", + "release_status": "ga", + "operating_system": "linux", + "architecture": "x64", + "package_type": "jdk", + "directly_downloadable": true + }, + { + "id": "redhat-17.0.16-jdk-linux-x64", + "archive_type": "tar.xz", + "distribution": "redhat", + "java_version": "17.0.16+8", + "release_status": "ga", + "operating_system": "linux", + "architecture": "x64", + "package_type": "jdk", + "directly_downloadable": true + }, + { + "id": "redhat-9-jdk-linux-x64", + "archive_type": "tar.xz", + "distribution": "redhat", + "java_version": "9+181", + "release_status": "ga", + "operating_system": "linux", + "architecture": "x64", + "package_type": "jdk", + "directly_downloadable": true + }, + { + "id": "redhat-21.0.8-jre-linux-x64", + "archive_type": "tar.xz", + "distribution": "redhat", + "java_version": "21.0.8+9", + "release_status": "ga", + "operating_system": "linux", + "architecture": "x64", + "package_type": "jre", + "directly_downloadable": true + }, + { + "id": "redhat-17.0.16-jdk-windows-x64", + "archive_type": "zip", + "distribution": "redhat", + "java_version": "17.0.16+8", + "release_status": "ga", + "operating_system": "windows", + "architecture": "x64", + "package_type": "jdk", + "directly_downloadable": true + } + ], + "message": "6 package(s) found" +} diff --git a/__tests__/distributors/redhat-installer-download.test.ts b/__tests__/distributors/redhat-installer-download.test.ts new file mode 100644 index 00000000..53dc202a --- /dev/null +++ b/__tests__/distributors/redhat-installer-download.test.ts @@ -0,0 +1,109 @@ +import {beforeEach, describe, expect, it, jest} from '@jest/globals'; +import path from 'path'; + +const mockReaddirSync = jest.fn(); +const realFs = await import('fs'); +jest.unstable_mockModule('fs', () => ({ + ...realFs, + default: {...realFs.default, readdirSync: mockReaddirSync}, + readdirSync: mockReaddirSync +})); + +jest.unstable_mockModule('@actions/core', () => ({ + info: jest.fn(), + debug: jest.fn(), + isDebug: jest.fn(() => false), + startGroup: jest.fn(), + endGroup: jest.fn() +})); + +const realUtil = await import('../../src/util.js'); +const mockCacheJdkDir = jest.fn(); +const mockExtractJdkFile = jest.fn(); +const mockRenameWinArchive = jest.fn(); +jest.unstable_mockModule('../../src/util.js', () => ({ + ...realUtil, + cacheJdkDir: mockCacheJdkDir, + extractJdkFile: mockExtractJdkFile, + renameWinArchive: mockRenameWinArchive +})); + +const {RedHatDistribution} = + await import('../../src/distributions/redhat/installer.js'); + +const release = { + version: '21.0.8+9', + url: 'https://developers.redhat.com/openjdk-21.tar.xz', + checksum: { + algorithm: 'sha256' as const, + value: 'a'.repeat(64) + } +}; + +const createDistribution = () => + new RedHatDistribution({ + version: '21', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + +beforeEach(() => { + jest.clearAllMocks(); + mockReaddirSync.mockReturnValue(['jdk-21']); + mockExtractJdkFile.mockResolvedValue('/tmp/extracted'); + mockCacheJdkDir.mockResolvedValue('/toolcache/Java_RedHat_jdk/21.0.8-9/x64'); + mockRenameWinArchive.mockReturnValue('/tmp/download.zip'); +}); + +describe('Red Hat package installation', () => { + it('extracts and caches Linux tar.xz archives', async () => { + const distribution = createDistribution(); + distribution['downloadAndVerify'] = jest + .fn() + .mockResolvedValue('/tmp/download'); + distribution['getArchiveType'] = () => 'tar.xz'; + + const result = await distribution['downloadTool'](release); + + expect(mockExtractJdkFile).toHaveBeenCalledWith('/tmp/download', 'tar.xz'); + expect(mockRenameWinArchive).not.toHaveBeenCalled(); + expect(mockCacheJdkDir).toHaveBeenCalledWith( + path.join('/tmp/extracted', 'jdk-21'), + 'Java_RedHat_jdk', + '21.0.8-9', + 'x64' + ); + expect(result).toEqual({ + version: '21.0.8+9', + path: '/toolcache/Java_RedHat_jdk/21.0.8-9/x64' + }); + }); + + it('renames downloaded Windows ZIP archives before extraction', async () => { + const distribution = createDistribution(); + distribution['downloadAndVerify'] = jest + .fn() + .mockResolvedValue('/tmp/download'); + distribution['getArchiveType'] = () => 'zip'; + + await distribution['downloadTool'](release); + + expect(mockRenameWinArchive).toHaveBeenCalledWith('/tmp/download'); + expect(mockExtractJdkFile).toHaveBeenCalledWith('/tmp/download.zip', 'zip'); + }); + + it('fails when the extracted archive is empty', async () => { + mockReaddirSync.mockReturnValue([]); + const distribution = createDistribution(); + distribution['downloadAndVerify'] = jest + .fn() + .mockResolvedValue('/tmp/download'); + distribution['getArchiveType'] = () => 'tar.xz'; + + await expect(distribution['downloadTool'](release)).rejects.toThrow( + 'The Red Hat archive for Java 21.0.8+9 was empty.' + ); + expect(mockCacheJdkDir).not.toHaveBeenCalled(); + }); +}); diff --git a/__tests__/distributors/redhat-installer.test.ts b/__tests__/distributors/redhat-installer.test.ts new file mode 100644 index 00000000..0890f513 --- /dev/null +++ b/__tests__/distributors/redhat-installer.test.ts @@ -0,0 +1,181 @@ +import {afterEach, describe, expect, it, jest} from '@jest/globals'; +import {HttpClient} from '@actions/http-client'; +import packageDetails from '../data/redhat-package-details.json' with {type: 'json'}; +import packages from '../data/redhat-packages.json' with {type: 'json'}; +import {RedHatDistribution} from '../../src/distributions/redhat/installer.js'; + +const createDistribution = ( + version: string, + packageType = 'jdk' +): RedHatDistribution => { + const distribution = new RedHatDistribution({ + version, + architecture: 'x64', + packageType, + checkLatest: false + }); + distribution['getOperatingSystem'] = () => 'linux'; + distribution['getArchiveType'] = () => 'tar.xz'; + return distribution; +}; + +const mockDisco = ( + packageResponse: unknown = packages, + detailsResponse: unknown = packageDetails +) => + jest.spyOn(HttpClient.prototype, 'getJson').mockImplementation(async url => ({ + result: url.includes('/packages?') ? packageResponse : detailsResponse, + statusCode: 200, + headers: {} + })); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('Red Hat package resolution', () => { + it('resolves the newest matching release and authoritative checksum', async () => { + const getJson = mockDisco(); + const distribution = createDistribution('21'); + + const release = await distribution['findPackageForDownload']('21'); + + expect(release).toEqual({ + version: '21.0.8+9', + url: packageDetails.result[0].direct_download_uri, + checksum: { + algorithm: 'sha256', + value: packageDetails.result[0].checksum, + source: + 'https://api.foojay.io/disco/v3.0/ids/redhat-21.0.8-jdk-linux-x64' + } + }); + expect(getJson).toHaveBeenNthCalledWith( + 1, + expect.stringContaining( + 'distro=redhat&release_status=ga&operating_system=linux&architecture=x64&package_type=jdk&archive_type=tar.xz&directly_downloadable=true' + ) + ); + expect(getJson).toHaveBeenNthCalledWith( + 2, + 'https://api.foojay.io/disco/v3.0/ids/redhat-21.0.8-jdk-linux-x64' + ); + }); + + it('normalizes feature-only Disco versions before matching', async () => { + mockDisco(undefined, { + result: [ + { + ...packageDetails.result[0], + direct_download_uri: 'https://developers.redhat.com/openjdk-9.tar.xz' + } + ], + message: '' + }); + const distribution = createDistribution('9'); + + const release = await distribution['findPackageForDownload']('9'); + + expect(release.version).toBe('9.0.0+181'); + }); + + it('selects the requested package type', async () => { + const getJson = mockDisco(); + const distribution = createDistribution('21', 'jre'); + + await distribution['findPackageForDownload']('21'); + + expect(getJson).toHaveBeenNthCalledWith( + 1, + expect.stringContaining('package_type=jre') + ); + expect(getJson).toHaveBeenNthCalledWith( + 2, + 'https://api.foojay.io/disco/v3.0/ids/redhat-21.0.8-jre-linux-x64' + ); + }); + + it('maps Windows to ZIP packages', async () => { + const getJson = mockDisco(); + const distribution = createDistribution('17'); + distribution['getOperatingSystem'] = () => 'windows'; + distribution['getArchiveType'] = () => 'zip'; + + await distribution['findPackageForDownload']('17'); + + expect(getJson).toHaveBeenNthCalledWith( + 1, + expect.stringContaining( + 'operating_system=windows&architecture=x64&package_type=jdk&archive_type=zip' + ) + ); + expect(getJson).toHaveBeenNthCalledWith( + 2, + 'https://api.foojay.io/disco/v3.0/ids/redhat-17.0.16-jdk-windows-x64' + ); + }); + + it('rejects Alpine before requesting glibc package metadata', async () => { + const getJson = mockDisco(); + const distribution = new RedHatDistribution({ + version: '21', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + + expect(() => distribution['getOperatingSystem'](true)).toThrow( + "Distribution 'redhat' does not support Alpine Linux because Red Hat portable archives require glibc." + ); + expect(getJson).not.toHaveBeenCalled(); + }); + + it('rejects early-access versions before requesting metadata', async () => { + const getJson = mockDisco(); + const distribution = createDistribution('21-ea'); + + await expect(distribution['findPackageForDownload']('21')).rejects.toThrow( + 'Early access versions are not supported' + ); + expect(getJson).not.toHaveBeenCalled(); + }); + + it('reports available versions when no release matches', async () => { + mockDisco(); + const distribution = createDistribution('25'); + + await expect(distribution['findPackageForDownload']('25')).rejects.toThrow( + "No matching version found for SemVer '25'.\nDistribution: RedHat" + ); + }); + + it('fails when the package detail has no direct Red Hat URL', async () => { + mockDisco(packages, {result: [], message: ''}); + const distribution = createDistribution('21'); + + await expect(distribution['findPackageForDownload']('21')).rejects.toThrow( + 'returned no direct Red Hat download URL' + ); + }); + + it('fails when the package detail has no SHA-256 checksum', async () => { + mockDisco(packages, { + result: [{...packageDetails.result[0], checksum: ''}], + message: '' + }); + const distribution = createDistribution('21'); + + await expect(distribution['findPackageForDownload']('21')).rejects.toThrow( + 'returned no SHA-256 checksum' + ); + }); + + it('fails with a targeted error when package metadata is missing', async () => { + mockDisco(null); + const distribution = createDistribution('21'); + + await expect(distribution['findPackageForDownload']('21')).rejects.toThrow( + 'Could not fetch Red Hat package metadata from Foojay Disco' + ); + }); +}); diff --git a/__tests__/util-install.test.ts b/__tests__/util-install.test.ts index b56529cd..b32cead7 100644 --- a/__tests__/util-install.test.ts +++ b/__tests__/util-install.test.ts @@ -405,6 +405,18 @@ describe('extractJdkFile', () => { expect(io.which).not.toHaveBeenCalled(); }); + it('extracts xz-compressed tarballs with xz tar flags', async () => { + (tc.extractTar as jest.Mock).mockResolvedValue('/extracted' as never); + + await expect(extractJdkFile('/tmp/jdk.tar.xz')).resolves.toBe('/extracted'); + expect(tc.extractTar).toHaveBeenCalledWith( + '/tmp/jdk.tar.xz', + undefined, + 'xJ' + ); + expect(io.which).not.toHaveBeenCalled(); + }); + it('uses the bundled tar.exe for zip archives on Windows', async () => { setPlatform('win32'); const systemRoot = path.join(workDir, 'Windows'); diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js index a880a7be..8dae155d 100644 --- a/dist/cleanup/index.js +++ b/dist/cleanup/index.js @@ -30905,7 +30905,9 @@ async function extractJdkFile(toolPath, extension) { if (!extension) { extension = toolPath.endsWith('.tar.gz') ? 'tar.gz' - : path.extname(toolPath); + : toolPath.endsWith('.tar.xz') + ? 'tar.xz' + : path.extname(toolPath); if (extension.startsWith('.')) { extension = extension.substring(1); } @@ -30913,6 +30915,8 @@ async function extractJdkFile(toolPath, extension) { switch (extension) { case 'tar.gz': return await extractTarGz(toolPath); + case 'tar.xz': + return await tc.extractTar(toolPath, undefined, 'xJ'); case 'tar': return await tc.extractTar(toolPath); case 'zip': diff --git a/dist/setup/228.index.js b/dist/setup/228.index.js new file mode 100644 index 00000000..09e4b5d1 --- /dev/null +++ b/dist/setup/228.index.js @@ -0,0 +1,139 @@ +export const id = 228; +export const ids = [228]; +export const modules = { + +/***/ 8228: +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ RedHatDistribution: () => (/* binding */ RedHatDistribution) +/* harmony export */ }); +/* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3838); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9896); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6928); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var semver__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(2088); +/* harmony import */ var semver__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(semver__WEBPACK_IMPORTED_MODULE_3__); +/* harmony import */ var _util_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(4527); +/* harmony import */ var _base_installer_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(6242); +/* harmony import */ var _platform_types_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(7444); + + + + + + + +const DISCO_API_URL = 'https://api.foojay.io/disco/v3.0'; +class RedHatDistribution extends _base_installer_js__WEBPACK_IMPORTED_MODULE_5__/* .JavaBase */ .O { + constructor(installerOptions) { + super('RedHat', installerOptions); + } + async findPackageForDownload(range) { + if (!this.stable) { + throw new Error('Early access versions are not supported'); + } + const availablePackages = await this.getAvailablePackages(); + const normalizedPackages = availablePackages + .map(item => ({ + item, + version: this.normalizeDiscoVersion(item.java_version) + })) + .filter((item) => item.version !== null) + .sort((left, right) => -semver__WEBPACK_IMPORTED_MODULE_3___default().compareBuild(left.version, right.version)); + const selectedPackage = normalizedPackages.find(item => (0,_util_js__WEBPACK_IMPORTED_MODULE_4__/* .isVersionSatisfies */ .y)(range, item.version)); + if (!selectedPackage) { + throw this.createVersionNotFoundError(range, normalizedPackages.map(item => item.version), `Operating system: ${this.getOperatingSystem()}`); + } + const detailsUrl = `${DISCO_API_URL}/ids/${selectedPackage.item.id}`; + const response = await this.http.getJson(detailsUrl); + const details = response.result?.result?.[0]; + if (!details?.direct_download_uri) { + throw new Error(`Foojay Disco returned no direct Red Hat download URL for package '${selectedPackage.item.id}'.`); + } + const checksum = details.checksum?.trim(); + if (details.checksum_type?.toLowerCase() !== 'sha256' || !checksum) { + throw new Error(`Foojay Disco returned no SHA-256 checksum for Red Hat package '${selectedPackage.item.id}'.`); + } + return { + version: selectedPackage.version, + url: details.direct_download_uri, + checksum: { + algorithm: 'sha256', + value: checksum, + source: detailsUrl + } + }; + } + async downloadTool(javaRelease) { + _actions_core__WEBPACK_IMPORTED_MODULE_0__/* .info */ .pq(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); + let javaArchivePath = await this.downloadAndVerify(javaRelease); + _actions_core__WEBPACK_IMPORTED_MODULE_0__/* .info */ .pq('Extracting Java archive...'); + const archiveType = this.getArchiveType(); + if (archiveType === 'zip') { + javaArchivePath = (0,_util_js__WEBPACK_IMPORTED_MODULE_4__/* .renameWinArchive */ .n2)(javaArchivePath); + } + const extractedJavaPath = await (0,_util_js__WEBPACK_IMPORTED_MODULE_4__/* .extractJdkFile */ .PE)(javaArchivePath, archiveType); + const archiveName = fs__WEBPACK_IMPORTED_MODULE_1___default().readdirSync(extractedJavaPath)[0]; + if (!archiveName) { + throw new Error(`The Red Hat archive for Java ${javaRelease.version} was empty.`); + } + const archivePath = path__WEBPACK_IMPORTED_MODULE_2___default().join(extractedJavaPath, archiveName); + const javaPath = await (0,_util_js__WEBPACK_IMPORTED_MODULE_4__/* .cacheJdkDir */ .Vj)(archivePath, this.toolcacheFolderName, this.getToolcacheVersionName(javaRelease.version), this.architecture); + return { version: javaRelease.version, path: javaPath }; + } + async getAvailablePackages() { + const operatingSystem = this.getOperatingSystem(); + const archiveType = this.getArchiveType(); + const query = new URLSearchParams({ + distro: 'redhat', + release_status: 'ga', + operating_system: operatingSystem, + architecture: this.distributionArchitecture(), + package_type: this.packageType, + archive_type: archiveType, + directly_downloadable: 'true' + }); + const url = `${DISCO_API_URL}/packages?${query.toString()}`; + const response = await this.http.getJson(url); + const packages = response.result?.result; + if (!Array.isArray(packages)) { + throw new Error(`Could not fetch Red Hat package metadata from Foojay Disco: ${url}`); + } + return packages.filter(item => item.distribution === 'redhat' && + item.release_status === 'ga' && + item.operating_system === operatingSystem && + item.architecture === this.distributionArchitecture() && + item.package_type === this.packageType && + item.archive_type === archiveType && + item.directly_downloadable); + } + getOperatingSystem(alpine = (0,_platform_types_js__WEBPACK_IMPORTED_MODULE_6__/* .isAlpineLinux */ .G6)()) { + if (alpine) { + throw new Error("Distribution 'redhat' does not support Alpine Linux because Red Hat portable archives require glibc."); + } + return process.platform === 'win32' ? 'windows' : 'linux'; + } + getArchiveType() { + return process.platform === 'win32' ? 'zip' : 'tar.xz'; + } + normalizeDiscoVersion(version) { + let normalizedVersion = version.trim(); + if (/^\d+\+\d+$/.test(normalizedVersion)) { + normalizedVersion = normalizedVersion.replace('+', '.0.0+'); + } + else if (/^\d+$/.test(normalizedVersion)) { + normalizedVersion = `${normalizedVersion}.0.0`; + } + else if (/^\d+\.\d+$/.test(normalizedVersion)) { + normalizedVersion = `${normalizedVersion}.0`; + } + return semver__WEBPACK_IMPORTED_MODULE_3___default().valid(normalizedVersion) ? normalizedVersion : null; + } +} + + +/***/ }) + +}; diff --git a/dist/setup/index.js b/dist/setup/index.js index ab4c3e70..6b2b0649 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -30915,6 +30915,7 @@ var JavaDistribution; JavaDistribution["JetBrains"] = "jetbrains"; JavaDistribution["Kona"] = "kona"; JavaDistribution["OracleOpenJdk"] = "oracle-openjdk"; + JavaDistribution["RedHat"] = "redhat"; })(JavaDistribution || (JavaDistribution = {})); const JAVA_PACKAGE_CAPABILITIES = { [JavaDistribution.Temurin]: ['jdk', 'jre', 'jdk+jmods'], @@ -30946,7 +30947,8 @@ const JAVA_PACKAGE_CAPABILITIES = { 'jre+ft' ], [JavaDistribution.Kona]: ['jdk'], - [JavaDistribution.OracleOpenJdk]: ['jdk'] + [JavaDistribution.OracleOpenJdk]: ['jdk'], + [JavaDistribution.RedHat]: ['jdk', 'jre'] }; function validateJavaPackage(distributionName, packageType, version) { if (!isJavaDistribution(distributionName)) { @@ -31131,6 +31133,19 @@ const JAVA_PLATFORM_CAPABILITIES = { macos: X64_ARM64, windows: ['x64'] } + }, + [_package_types_js__WEBPACK_IMPORTED_MODULE_2__/* .JavaDistribution */ .zS.RedHat]: { + platforms: { + linux: [ + 'x64', + { architecture: 'aarch64', versionRange: '<12' }, + { architecture: 'ppc64le', versionRange: '<12' } + ], + windows: [ + { architecture: 'x64', versionRange: '<22' }, + { architecture: 'x86', versionRange: '<11' } + ] + } } }; const ARCHITECTURE_ALIASES = { @@ -31346,7 +31361,9 @@ async function extractJdkFile(toolPath, extension) { if (!extension) { extension = toolPath.endsWith('.tar.gz') ? 'tar.gz' - : path__WEBPACK_IMPORTED_MODULE_1___default().extname(toolPath); + : toolPath.endsWith('.tar.xz') + ? 'tar.xz' + : path__WEBPACK_IMPORTED_MODULE_1___default().extname(toolPath); if (extension.startsWith('.')) { extension = extension.substring(1); } @@ -31354,6 +31371,8 @@ async function extractJdkFile(toolPath, extension) { switch (extension) { case 'tar.gz': return await extractTarGz(toolPath); + case 'tar.xz': + return await _actions_tool_cache__WEBPACK_IMPORTED_MODULE_5__/* .extractTar */ .nN(toolPath, undefined, 'xJ'); case 'tar': return await _actions_tool_cache__WEBPACK_IMPORTED_MODULE_5__/* .extractTar */ .nN(toolPath); case 'zip': @@ -36309,6 +36328,10 @@ async function getJavaDistribution(distributionName, installerOptions, jdkFile) const { OpenJdkDistribution } = await Promise.all(/* import() */[__nccwpck_require__.e(242), __nccwpck_require__.e(735)]).then(__nccwpck_require__.bind(__nccwpck_require__, 3735)); return new OpenJdkDistribution(normalizedInstallerOptions); } + case package_types/* JavaDistribution */.zS.RedHat: { + const { RedHatDistribution } = await Promise.all(/* import() */[__nccwpck_require__.e(242), __nccwpck_require__.e(228)]).then(__nccwpck_require__.bind(__nccwpck_require__, 8228)); + return new RedHatDistribution(normalizedInstallerOptions); + } default: return null; } diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index def22343..ff198920 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -77,6 +77,19 @@ steps: - run: java --version ``` +### Red Hat Build of OpenJDK + +```yaml +steps: + - uses: actions/checkout@v7 + - uses: actions/setup-java@v6 + with: + distribution: 'redhat' + java-version: '21' + java-package: jdk # optional (jdk or jre) - defaults to jdk + - run: java --version +``` + ### Liberica ```yaml @@ -329,6 +342,7 @@ The package types have these meanings: | `corretto` | `jdk`, `jre` | Accepts major versions only. JDK availability follows Amazon's platform catalog. For the operating systems directly selected by `setup-java`, JRE downloads are limited to Java 8 on Windows; Linux and macOS use `jdk`. | | `oracle` | `jdk` | Stable Oracle JDK 17 and later only. | | `oracle-openjdk` | `jdk` | Installs the GA or early-access JDK builds currently listed or archived on `jdk.java.net`; use a `-ea` version such as `27-ea` for early access. | +| `redhat` | `jdk`, `jre` | Stable builds only. Version availability follows the Foojay Disco catalog for Red Hat Build of OpenJDK and can lag Red Hat's downloads page. | | `dragonwell` | `jdk` | Stable builds only. The current vendor catalog provides Java 8, 11, 17, 21, and 25. | | `sapmachine` | `jdk`, `jre` | Follows the SapMachine catalog. Both editions are represented from Java 10 onward, but individual versions and platforms can differ. | | `graalvm` | `jdk` | Stable Oracle GraalVM for JDK 17 and later only. | @@ -644,6 +658,7 @@ absent from a vendor catalog. | `corretto` | `x64`, `x86`, `armv7`, `aarch64` | `x64`, `aarch64` | `x64`, `x86` | `x86` is limited to Java 11 or earlier; Linux `armv7` is available for Java 11. | | `oracle` | `x64`, `aarch64` | `x64`, `aarch64` | `x64` | | | `oracle-openjdk` | `x64`, `aarch64` | `x64`, `aarch64` | `x64` | | +| `redhat` | `x64`, `aarch64`, `ppc64le` | — | `x64`, `x86` | Linux requires glibc; Alpine is unsupported. Linux `aarch64` and `ppc64le` are limited to Java 11 or earlier. Windows `x64` is limited to Java 21 or earlier and `x86` to Java 10 or earlier. | | `dragonwell` | `x64`, `aarch64` | — | `x64` | | | `sapmachine` | `x64`, `aarch64`, `ppc64le` | `x64`, `aarch64` | `x64`, `aarch64` | | | `graalvm`, `graalvm-community` | `x64`, `aarch64` | `x64`, `aarch64` | `x64` | | diff --git a/src/distributions/distribution-factory.ts b/src/distributions/distribution-factory.ts index 02e9c58e..59dbc802 100644 --- a/src/distributions/distribution-factory.ts +++ b/src/distributions/distribution-factory.ts @@ -98,6 +98,10 @@ export async function getJavaDistribution( const {OpenJdkDistribution} = await import('./openjdk/installer.js'); return new OpenJdkDistribution(normalizedInstallerOptions); } + case JavaDistribution.RedHat: { + const {RedHatDistribution} = await import('./redhat/installer.js'); + return new RedHatDistribution(normalizedInstallerOptions); + } default: return null; } diff --git a/src/distributions/package-types.ts b/src/distributions/package-types.ts index d1f16183..9b0d23bd 100644 --- a/src/distributions/package-types.ts +++ b/src/distributions/package-types.ts @@ -17,7 +17,8 @@ export enum JavaDistribution { GraalVMCommunity = 'graalvm-community', JetBrains = 'jetbrains', Kona = 'kona', - OracleOpenJdk = 'oracle-openjdk' + OracleOpenJdk = 'oracle-openjdk', + RedHat = 'redhat' } export const JAVA_PACKAGE_CAPABILITIES = { @@ -50,7 +51,8 @@ export const JAVA_PACKAGE_CAPABILITIES = { 'jre+ft' ], [JavaDistribution.Kona]: ['jdk'], - [JavaDistribution.OracleOpenJdk]: ['jdk'] + [JavaDistribution.OracleOpenJdk]: ['jdk'], + [JavaDistribution.RedHat]: ['jdk', 'jre'] } as const satisfies Record; export function validateJavaPackage( diff --git a/src/distributions/platform-types.ts b/src/distributions/platform-types.ts index 9806d53c..a48b4595 100644 --- a/src/distributions/platform-types.ts +++ b/src/distributions/platform-types.ts @@ -145,6 +145,19 @@ export const JAVA_PLATFORM_CAPABILITIES: Record< macos: X64_ARM64, windows: ['x64'] } + }, + [JavaDistribution.RedHat]: { + platforms: { + linux: [ + 'x64', + {architecture: 'aarch64', versionRange: '<12'}, + {architecture: 'ppc64le', versionRange: '<12'} + ], + windows: [ + {architecture: 'x64', versionRange: '<22'}, + {architecture: 'x86', versionRange: '<11'} + ] + } } }; diff --git a/src/distributions/redhat/installer.ts b/src/distributions/redhat/installer.ts new file mode 100644 index 00000000..49fa274d --- /dev/null +++ b/src/distributions/redhat/installer.ts @@ -0,0 +1,180 @@ +import * as core from '@actions/core'; +import fs from 'fs'; +import path from 'path'; +import semver from 'semver'; +import { + cacheJdkDir, + extractJdkFile, + isVersionSatisfies, + renameWinArchive +} from '../../util.js'; +import {JavaBase} from '../base-installer.js'; +import { + JavaDownloadRelease, + JavaInstallerOptions, + JavaInstallerResults +} from '../base-models.js'; +import { + IDiscoPackage, + IDiscoPackageDetailsResponse, + IDiscoPackageListResponse +} from './models.js'; +import {isAlpineLinux} from '../platform-types.js'; + +const DISCO_API_URL = 'https://api.foojay.io/disco/v3.0'; + +export class RedHatDistribution extends JavaBase { + constructor(installerOptions: JavaInstallerOptions) { + super('RedHat', installerOptions); + } + + protected async findPackageForDownload( + range: string + ): Promise { + if (!this.stable) { + throw new Error('Early access versions are not supported'); + } + + const availablePackages = await this.getAvailablePackages(); + const normalizedPackages = availablePackages + .map(item => ({ + item, + version: this.normalizeDiscoVersion(item.java_version) + })) + .filter( + (item): item is {item: IDiscoPackage; version: string} => + item.version !== null + ) + .sort((left, right) => -semver.compareBuild(left.version, right.version)); + + const selectedPackage = normalizedPackages.find(item => + isVersionSatisfies(range, item.version) + ); + if (!selectedPackage) { + throw this.createVersionNotFoundError( + range, + normalizedPackages.map(item => item.version), + `Operating system: ${this.getOperatingSystem()}` + ); + } + + const detailsUrl = `${DISCO_API_URL}/ids/${selectedPackage.item.id}`; + const response = + await this.http.getJson(detailsUrl); + const details = response.result?.result?.[0]; + if (!details?.direct_download_uri) { + throw new Error( + `Foojay Disco returned no direct Red Hat download URL for package '${selectedPackage.item.id}'.` + ); + } + const checksum = details.checksum?.trim(); + if (details.checksum_type?.toLowerCase() !== 'sha256' || !checksum) { + throw new Error( + `Foojay Disco returned no SHA-256 checksum for Red Hat package '${selectedPackage.item.id}'.` + ); + } + + return { + version: selectedPackage.version, + url: details.direct_download_uri, + checksum: { + algorithm: 'sha256', + value: checksum, + source: detailsUrl + } + }; + } + + protected async downloadTool( + javaRelease: JavaDownloadRelease + ): Promise { + core.info( + `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` + ); + let javaArchivePath = await this.downloadAndVerify(javaRelease); + + core.info('Extracting Java archive...'); + const archiveType = this.getArchiveType(); + if (archiveType === 'zip') { + javaArchivePath = renameWinArchive(javaArchivePath); + } + const extractedJavaPath = await extractJdkFile( + javaArchivePath, + archiveType + ); + const archiveName = fs.readdirSync(extractedJavaPath)[0]; + if (!archiveName) { + throw new Error( + `The Red Hat archive for Java ${javaRelease.version} was empty.` + ); + } + const archivePath = path.join(extractedJavaPath, archiveName); + const javaPath = await cacheJdkDir( + archivePath, + this.toolcacheFolderName, + this.getToolcacheVersionName(javaRelease.version), + this.architecture + ); + + return {version: javaRelease.version, path: javaPath}; + } + + private async getAvailablePackages(): Promise { + const operatingSystem = this.getOperatingSystem(); + const archiveType = this.getArchiveType(); + const query = new URLSearchParams({ + distro: 'redhat', + release_status: 'ga', + operating_system: operatingSystem, + architecture: this.distributionArchitecture(), + package_type: this.packageType, + archive_type: archiveType, + directly_downloadable: 'true' + }); + const url = `${DISCO_API_URL}/packages?${query.toString()}`; + const response = await this.http.getJson(url); + const packages = response.result?.result; + if (!Array.isArray(packages)) { + throw new Error( + `Could not fetch Red Hat package metadata from Foojay Disco: ${url}` + ); + } + + return packages.filter( + item => + item.distribution === 'redhat' && + item.release_status === 'ga' && + item.operating_system === operatingSystem && + item.architecture === this.distributionArchitecture() && + item.package_type === this.packageType && + item.archive_type === archiveType && + item.directly_downloadable + ); + } + + private getOperatingSystem(alpine = isAlpineLinux()): string { + if (alpine) { + throw new Error( + "Distribution 'redhat' does not support Alpine Linux because Red Hat portable archives require glibc." + ); + } + return process.platform === 'win32' ? 'windows' : 'linux'; + } + + private getArchiveType(): string { + return process.platform === 'win32' ? 'zip' : 'tar.xz'; + } + + private normalizeDiscoVersion(version: string): string | null { + let normalizedVersion = version.trim(); + if (/^\d+\+\d+$/.test(normalizedVersion)) { + normalizedVersion = normalizedVersion.replace('+', '.0.0+'); + } else if (/^\d+$/.test(normalizedVersion)) { + normalizedVersion = `${normalizedVersion}.0.0`; + } else if (/^\d+\.\d+$/.test(normalizedVersion)) { + normalizedVersion = `${normalizedVersion}.0`; + } + + return semver.valid(normalizedVersion) ? normalizedVersion : null; + } +} diff --git a/src/distributions/redhat/models.ts b/src/distributions/redhat/models.ts new file mode 100644 index 00000000..295caba5 --- /dev/null +++ b/src/distributions/redhat/models.ts @@ -0,0 +1,28 @@ +export interface IDiscoPackage { + id: string; + archive_type: string; + distribution: string; + java_version: string; + release_status: string; + operating_system: string; + architecture: string; + package_type: string; + directly_downloadable: boolean; +} + +export interface IDiscoPackageListResponse { + result: IDiscoPackage[]; + message: string; +} + +export interface IDiscoPackageDetails { + filename: string; + direct_download_uri: string; + checksum: string; + checksum_type: string; +} + +export interface IDiscoPackageDetailsResponse { + result: IDiscoPackageDetails[]; + message: string; +} diff --git a/src/util.ts b/src/util.ts index 3374eb5e..e2583849 100644 --- a/src/util.ts +++ b/src/util.ts @@ -59,7 +59,9 @@ export async function extractJdkFile(toolPath: string, extension?: string) { if (!extension) { extension = toolPath.endsWith('.tar.gz') ? 'tar.gz' - : path.extname(toolPath); + : toolPath.endsWith('.tar.xz') + ? 'tar.xz' + : path.extname(toolPath); if (extension.startsWith('.')) { extension = extension.substring(1); } @@ -68,6 +70,8 @@ export async function extractJdkFile(toolPath: string, extension?: string) { switch (extension) { case 'tar.gz': return await extractTarGz(toolPath); + case 'tar.xz': + return await tc.extractTar(toolPath, undefined, 'xJ'); case 'tar': return await tc.extractTar(toolPath); case 'zip':