Add Red Hat Build of OpenJDK support (#1241)

* Add Red Hat Build of OpenJDK support

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Fix Red Hat tests on Windows

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Update compiled Red Hat distribution

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Bruno Borges
2026-08-17 20:39:36 -04:00
committed by GitHub
co-authored by Copilot App Copilot Autofix powered by AI
parent 5f75b27283
commit 416c6d1e8a
16 changed files with 803 additions and 6 deletions
@@ -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": ""
}
+71
View File
@@ -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"
}
@@ -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();
});
});
@@ -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'
);
});
});
+12
View File
@@ -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');