From 089b010dc8ec94f911a8fd04f5501e320b7ff99f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:43:47 -0400 Subject: [PATCH 1/2] Add force-download option for reproducible JDK builds (#1148) * Initial plan * Add force-download input * Build force-download action bundles --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Bruno Borges --- README.md | 2 ++ __tests__/distributors/base-installer.test.ts | 29 +++++++++++++++++++ .../distributors/local-installer.test.ts | 23 +++++++++++++++ action.yml | 4 +++ dist/cleanup/index.js | 1 + dist/setup/index.js | 16 +++++++--- src/constants.ts | 1 + src/distributions/base-installer.ts | 9 ++++-- src/distributions/base-models.ts | 1 + src/distributions/local/installer.ts | 2 +- src/setup-java.ts | 9 ++++++ 11 files changed, 90 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 461338a9b..dba72b63b 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ For more details, see the full release notes on the [releases page](https://git - `check-latest`: Setting this option makes the action to check for the latest available version for the version spec. + - `force-download`: Set to `true` to always download Java and replace any matching version in the tool cache. This can help make builds reproducible when a runner image has modified a pre-installed JDK, such as its `cacerts` file. Default value: `false`. + - `set-default`: Set to `false` to install a JDK without making it the default. When `false`, `JAVA_HOME` and `PATH` are not updated, but `JAVA_HOME__` is still set so the JDK remains discoverable. Default value: `true`. See [Installing JDK without setting as default](docs/advanced-usage.md#Installing-JDK-without-setting-as-default) for more details. - `problem-matcher`: Set to `false` to disable Java problem matcher annotations (compiler diagnostics and uncaught exceptions). Default value: `true`. See [Java problem matcher](docs/advanced-usage.md#java-problem-matcher-compiler-annotations) for details and annotation limits. diff --git a/__tests__/distributors/base-installer.test.ts b/__tests__/distributors/base-installer.test.ts index ad3b0afc5..457a2b249 100644 --- a/__tests__/distributors/base-installer.test.ts +++ b/__tests__/distributors/base-installer.test.ts @@ -443,6 +443,35 @@ describe('setupJava', () => { ); }); + it('should download java when force-download is enabled, even if the version is cached', async () => { + mockJavaBase = new EmptyJavaBase({ + version: actualJavaVersion, + architecture: 'x86', + packageType: 'jdk', + checkLatest: false, + forceDownload: true + }); + const findInToolcache = jest.fn(() => ({ + version: actualJavaVersion, + path: javaPathInstalled + })); + mockJavaBase['findInToolcache'] = findInToolcache; + + await expect(mockJavaBase.setupJava()).resolves.toEqual({ + version: actualJavaVersion, + path: javaPathInstalled + }); + + expect(findInToolcache).not.toHaveBeenCalled(); + expect(spyCoreInfo).toHaveBeenCalledWith('Trying to download...'); + expect(spyCoreInfo).toHaveBeenCalledWith( + `Java ${actualJavaVersion} was downloaded` + ); + expect(spyCoreInfo).not.toHaveBeenCalledWith( + `Resolved Java ${actualJavaVersion} from tool-cache` + ); + }); + it.each([ [ { diff --git a/__tests__/distributors/local-installer.test.ts b/__tests__/distributors/local-installer.test.ts index 3266f8a89..47997888e 100644 --- a/__tests__/distributors/local-installer.test.ts +++ b/__tests__/distributors/local-installer.test.ts @@ -208,6 +208,29 @@ describe('setupJava', () => { ); }); + it('java is unpacked from jdkfile when force-download is enabled', async () => { + const inputs = { + version: actualJavaVersion, + architecture: 'x86', + packageType: 'jdk', + checkLatest: false, + forceDownload: true + }; + + mockJavaBase = new LocalDistribution(inputs, expectedJdkFile); + await expect(mockJavaBase.setupJava()).resolves.toEqual({ + version: actualJavaVersion, + path: javaPath + }); + + expect(spyGetToolcachePath).not.toHaveBeenCalled(); + expect(spyUtilsExtractJdkFile).toHaveBeenCalledWith(expectedJdkFile); + expect(spyTcCacheDir).toHaveBeenCalled(); + expect(spyCoreInfo).not.toHaveBeenCalledWith( + `Resolved Java ${actualJavaVersion} from tool-cache` + ); + }); + it("java is resolved from toolcache, jdkfile doesn't exist", async () => { const inputs = { version: actualJavaVersion, diff --git a/action.yml b/action.yml index 98a95e0db..2412e8409 100644 --- a/action.yml +++ b/action.yml @@ -30,6 +30,10 @@ inputs: description: 'Set this option if you want the action to check for the latest available version that satisfies the version spec' required: false default: false + force-download: + description: 'Set this option to always download Java and replace any matching version in the tool cache' + required: false + default: false set-default: description: 'Set this option to false if you want to install a JDK but not make it the default. When false, JAVA_HOME and PATH are not updated, but JAVA_HOME__ is still set.' required: false diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js index fe389517b..717544706 100644 --- a/dist/cleanup/index.js +++ b/dist/cleanup/index.js @@ -97311,6 +97311,7 @@ const INPUT_DISTRIBUTION = 'distribution'; const INPUT_JDK_FILE = 'jdk-file'; const INPUT_JDK_FILE_DEPRECATED = 'jdkFile'; const INPUT_CHECK_LATEST = 'check-latest'; +const INPUT_FORCE_DOWNLOAD = 'force-download'; const INPUT_SET_DEFAULT = 'set-default'; const INPUT_PROBLEM_MATCHER = 'problem-matcher'; const INPUT_VERIFY_SIGNATURE = 'verify-signature'; diff --git a/dist/setup/index.js b/dist/setup/index.js index c547ea81f..10be04550 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -72088,6 +72088,7 @@ const INPUT_DISTRIBUTION = 'distribution'; const INPUT_JDK_FILE = 'jdk-file'; const INPUT_JDK_FILE_DEPRECATED = 'jdkFile'; const INPUT_CHECK_LATEST = 'check-latest'; +const INPUT_FORCE_DOWNLOAD = 'force-download'; const INPUT_SET_DEFAULT = 'set-default'; const INPUT_PROBLEM_MATCHER = 'problem-matcher'; const INPUT_VERIFY_SIGNATURE = 'verify-signature'; @@ -129290,6 +129291,7 @@ class JavaBase { stable; latest; checkLatest; + forceDownload; setDefault; verifySignature; verifySignaturePublicKey; @@ -129307,6 +129309,7 @@ class JavaBase { this.architecture = installerOptions.architecture || external_os_default().arch(); this.packageType = installerOptions.packageType; this.checkLatest = installerOptions.checkLatest; + this.forceDownload = installerOptions.forceDownload ?? false; this.setDefault = installerOptions.setDefault !== undefined ? installerOptions.setDefault @@ -129318,7 +129321,7 @@ class JavaBase { if (this.verifySignature && !this.supportsSignatureVerification()) { throw new Error(`Input 'verify-signature' is not supported for distribution '${this.distribution}'.`); } - let foundJava = this.findInToolcache(); + let foundJava = this.forceDownload ? null : this.findInToolcache(); if (foundJava && !this.checkLatest && !this.latest) { info(`Resolved Java ${foundJava.version} from tool-cache`); } @@ -129342,7 +129345,8 @@ class JavaBase { } const javaRelease = await this.findPackageForDownload(this.version); info(`Resolved latest version as ${javaRelease.version}`); - if (foundJava?.version === javaRelease.version) { + if (!this.forceDownload && + foundJava?.version === javaRelease.version) { info(`Resolved Java ${foundJava.version} from tool-cache`); } else { @@ -129617,7 +129621,7 @@ class LocalDistribution extends JavaBase { if (this.latest) { throw new Error("The 'latest' version alias is not supported for the 'jdkfile' distribution. Please specify a concrete version."); } - let foundJava = this.findInToolcache(); + let foundJava = this.forceDownload ? null : this.findInToolcache(); if (foundJava) { info(`Resolved Java ${foundJava.version} from tool-cache`); } @@ -132041,6 +132045,7 @@ async function run() { const cache = getInput(INPUT_CACHE); const cacheDependencyPath = getInput(INPUT_CACHE_DEPENDENCY_PATH); const checkLatest = util_getBooleanInput(INPUT_CHECK_LATEST, false); + const forceDownload = util_getBooleanInput(INPUT_FORCE_DOWNLOAD, false); const setDefault = util_getBooleanInput(INPUT_SET_DEFAULT, true); const verifySignature = util_getBooleanInput(INPUT_VERIFY_SIGNATURE, false); const verifySignaturePublicKey = getInput(INPUT_VERIFY_SIGNATURE_PUBLIC_KEY) || undefined; @@ -132072,6 +132077,7 @@ async function run() { architecture, packageType, checkLatest, + forceDownload, setDefault, verifySignature, verifySignaturePublicKey, @@ -132090,6 +132096,7 @@ async function run() { architecture, packageType, checkLatest, + forceDownload, setDefault, verifySignature, verifySignaturePublicKey, @@ -132124,11 +132131,12 @@ function getJdkFileInput() { return jdkFile || deprecatedJdkFile; } async function installVersion(version, options, toolchainId = 0) { - const { distributionName, jdkFile, architecture, packageType, checkLatest, setDefault, verifySignature, verifySignaturePublicKey, toolchainIds } = options; + const { distributionName, jdkFile, architecture, packageType, checkLatest, forceDownload, setDefault, verifySignature, verifySignaturePublicKey, toolchainIds } = options; const installerOptions = { architecture, packageType, checkLatest, + forceDownload, setDefault, verifySignature, verifySignaturePublicKey, diff --git a/src/constants.ts b/src/constants.ts index 145d78b4c..e1688db4a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,6 +7,7 @@ export const INPUT_DISTRIBUTION = 'distribution'; export const INPUT_JDK_FILE = 'jdk-file'; export const INPUT_JDK_FILE_DEPRECATED = 'jdkFile'; export const INPUT_CHECK_LATEST = 'check-latest'; +export const INPUT_FORCE_DOWNLOAD = 'force-download'; export const INPUT_SET_DEFAULT = 'set-default'; export const INPUT_PROBLEM_MATCHER = 'problem-matcher'; export const INPUT_VERIFY_SIGNATURE = 'verify-signature'; diff --git a/src/distributions/base-installer.ts b/src/distributions/base-installer.ts index 07e7c444b..c00d21d24 100644 --- a/src/distributions/base-installer.ts +++ b/src/distributions/base-installer.ts @@ -25,6 +25,7 @@ export abstract class JavaBase { protected stable: boolean; protected latest: boolean; protected checkLatest: boolean; + protected forceDownload: boolean; protected setDefault: boolean; protected verifySignature: boolean; protected verifySignaturePublicKey: string | undefined; @@ -46,6 +47,7 @@ export abstract class JavaBase { this.architecture = installerOptions.architecture || os.arch(); this.packageType = installerOptions.packageType; this.checkLatest = installerOptions.checkLatest; + this.forceDownload = installerOptions.forceDownload ?? false; this.setDefault = installerOptions.setDefault !== undefined ? installerOptions.setDefault @@ -68,7 +70,7 @@ export abstract class JavaBase { ); } - let foundJava = this.findInToolcache(); + let foundJava = this.forceDownload ? null : this.findInToolcache(); if (foundJava && !this.checkLatest && !this.latest) { core.info(`Resolved Java ${foundJava.version} from tool-cache`); } else { @@ -91,7 +93,10 @@ export abstract class JavaBase { } const javaRelease = await this.findPackageForDownload(this.version); core.info(`Resolved latest version as ${javaRelease.version}`); - if (foundJava?.version === javaRelease.version) { + if ( + !this.forceDownload && + foundJava?.version === javaRelease.version + ) { core.info(`Resolved Java ${foundJava.version} from tool-cache`); } else { core.info('Trying to download...'); diff --git a/src/distributions/base-models.ts b/src/distributions/base-models.ts index b2ca3f0ff..3bfe4bb9c 100644 --- a/src/distributions/base-models.ts +++ b/src/distributions/base-models.ts @@ -3,6 +3,7 @@ export interface JavaInstallerOptions { architecture: string; packageType: string; checkLatest: boolean; + forceDownload?: boolean; setDefault?: boolean; verifySignature?: boolean; verifySignaturePublicKey?: string; diff --git a/src/distributions/local/installer.ts b/src/distributions/local/installer.ts index 6447c4293..840c19dcb 100644 --- a/src/distributions/local/installer.ts +++ b/src/distributions/local/installer.ts @@ -28,7 +28,7 @@ export class LocalDistribution extends JavaBase { ); } - let foundJava = this.findInToolcache(); + let foundJava = this.forceDownload ? null : this.findInToolcache(); if (foundJava) { core.info(`Resolved Java ${foundJava.version} from tool-cache`); diff --git a/src/setup-java.ts b/src/setup-java.ts index 3efaf912a..cc0f93704 100644 --- a/src/setup-java.ts +++ b/src/setup-java.ts @@ -29,6 +29,10 @@ async function run() { constants.INPUT_CACHE_DEPENDENCY_PATH ); const checkLatest = getBooleanInput(constants.INPUT_CHECK_LATEST, false); + const forceDownload = getBooleanInput( + constants.INPUT_FORCE_DOWNLOAD, + false + ); const setDefault = getBooleanInput(constants.INPUT_SET_DEFAULT, true); const verifySignature = getBooleanInput( constants.INPUT_VERIFY_SIGNATURE, @@ -83,6 +87,7 @@ async function run() { architecture, packageType, checkLatest, + forceDownload, setDefault, verifySignature, verifySignaturePublicKey, @@ -102,6 +107,7 @@ async function run() { architecture, packageType, checkLatest, + forceDownload, setDefault, verifySignature, verifySignaturePublicKey, @@ -159,6 +165,7 @@ async function installVersion( architecture, packageType, checkLatest, + forceDownload, setDefault, verifySignature, verifySignaturePublicKey, @@ -169,6 +176,7 @@ async function installVersion( architecture, packageType, checkLatest, + forceDownload, setDefault, verifySignature, verifySignaturePublicKey, @@ -212,6 +220,7 @@ interface installerInputsOptions { architecture: string; packageType: string; checkLatest: boolean; + forceDownload: boolean; setDefault: boolean; verifySignature: boolean; verifySignaturePublicKey: string | undefined; From 0b0bd2592777d1620e3e3e6e8388341020b58b1c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:55:33 -0400 Subject: [PATCH 2/2] Add OpenJDK distribution (#1147) * Initial plan * Add OpenJDK distribution * Support archived OpenJDK release formats * Handle legacy OpenJDK URL layout * Resolve legacy OpenJDK build metadata * Rename OpenJDK distribution to oracle-openjdk Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c93ae0c-bbf5-40f5-bf6e-40168d0e267f * Make OpenJDK tests platform independent Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c93ae0c-bbf5-40f5-bf6e-40168d0e267f * Document Oracle OpenJDK early access builds Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c93ae0c-bbf5-40f5-bf6e-40168d0e267f * Clarify Oracle OpenJDK security note Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c93ae0c-bbf5-40f5-bf6e-40168d0e267f --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Bruno Borges Copilot-Session: 2c93ae0c-bbf5-40f5-bf6e-40168d0e267f --- README.md | 2 + .../distributors/openjdk-installer.test.ts | 216 ++++++++++++++++++ dist/setup/index.js | 107 +++++++++ docs/advanced-usage.md | 34 ++- src/distributions/distribution-factory.ts | 6 +- src/distributions/openjdk/installer.ts | 181 +++++++++++++++ 6 files changed, 527 insertions(+), 19 deletions(-) create mode 100644 __tests__/distributors/openjdk-installer.test.ts create mode 100644 src/distributions/openjdk/installer.ts diff --git a/README.md b/README.md index dba72b63b..ded2fa82d 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,7 @@ Currently, the following distributions are supported: | `corretto` | [Amazon Corretto Build of OpenJDK](https://aws.amazon.com/corretto/) | [`corretto` license](https://aws.amazon.com/corretto/faqs/) | `semeru` | [IBM Semeru Runtime Open Edition](https://developer.ibm.com/languages/java/semeru-runtimes/downloads/) | [`semeru` license](https://openjdk.java.net/legal/gplv2+ce.html) | | `oracle` | [Oracle JDK](https://www.oracle.com/java/technologies/downloads/) | [`oracle` license](https://java.com/freeuselicense) +| `oracle-openjdk` | [Oracle OpenJDK](https://jdk.java.net/) | [`oracle-openjdk` license](https://openjdk.org/legal/gplv2+ce.html) | `dragonwell` | [Alibaba Dragonwell JDK](https://dragonwell-jdk.io/) | [`dragonwell` license](https://www.aliyun.com/product/dragonwell/) | `sapmachine` | [SAP SapMachine JDK/JRE](https://sapmachine.io/) | [`sapmachine` license](https://github.com/SAP/SapMachine/blob/sapmachine/LICENSE) | `graalvm` | [Oracle GraalVM](https://www.graalvm.org/) | [`graalvm` license](https://www.oracle.com/downloads/licenses/graal-free-license.html) @@ -159,6 +160,7 @@ Currently, the following distributions are supported: > [!NOTE] > - The different distributors can provide discrepant list of available versions / supported configurations. Please refer to the official documentation to see the list of supported versions. > - AdoptOpenJDK got moved to Eclipse Temurin and won't be updated anymore. It is highly recommended to migrate workflows from `adopt` and `adopt-openj9`, to `temurin` and `semeru` respectively, to keep receiving software and security updates. See more details in the [Good-bye AdoptOpenJDK post](https://blog.adoptopenjdk.net/2021/08/goodbye-adoptopenjdk-hello-adoptium/). +> - Oracle OpenJDK builds are created and hosted by Oracle. After a limited number of releases, Oracle archives these builds and no longer provides security updates. To continue receiving security patches, users must move to Oracle JDK or choose a different vendor. > - For Azul Zulu OpenJDK, architecture `arm64` is mapped to `aarch64` when querying the Azul Metadata API. > - To comply with the GraalVM Free Terms and Conditions (GFTC) license, it is recommended to use GraalVM JDK 17 version 17.0.12, as this is the only version of GraalVM JDK 17 available under the GFTC license. Additionally, it is encouraged to consider upgrading to GraalVM JDK 21, which offers the latest features and improvements. > - GraalVM Community is available as `distribution: 'graalvm-community'` for stable JDK 17 and later releases published on GitHub. diff --git a/__tests__/distributors/openjdk-installer.test.ts b/__tests__/distributors/openjdk-installer.test.ts new file mode 100644 index 000000000..48720a410 --- /dev/null +++ b/__tests__/distributors/openjdk-installer.test.ts @@ -0,0 +1,216 @@ +import {afterEach, beforeEach, describe, expect, it, jest} from '@jest/globals'; +import {HttpClient} from '@actions/http-client'; + +jest.unstable_mockModule('@actions/core', () => ({ + info: jest.fn(), + warning: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + notice: jest.fn(), + setFailed: jest.fn(), + setOutput: jest.fn(), + getInput: jest.fn(), + getBooleanInput: jest.fn(), + getMultilineInput: jest.fn(), + addPath: jest.fn(), + exportVariable: jest.fn(), + saveState: jest.fn(), + getState: jest.fn(), + setSecret: jest.fn(), + isDebug: jest.fn(() => false), + startGroup: jest.fn(), + endGroup: jest.fn(), + group: jest.fn((_name: string, fn: () => Promise) => fn()), + toPlatformPath: jest.fn((value: string) => value), + toWin32Path: jest.fn((value: string) => value), + toPosixPath: jest.fn((value: string) => value) +})); + +const {OpenJdkDistribution} = + await import('../../src/distributions/openjdk/installer.js'); +const {getJavaDistribution} = + await import('../../src/distributions/distribution-factory.js'); + +const homePage = ` + JDK 26 + JDK + 27 +`; +const currentPage = ` + tar.gz + tar.gz +`; +const earlyAccessPage = ` + tar.gz +`; +const archivePage = ` + tar.gz + tar.gz + tar.gz + 9.0.4 (build 9.0.4+11) + tar.gz +`; + +function createDistribution( + version = '26', + architecture = 'x64', + packageType = 'jdk', + useFixturePlatform = true +) { + const distribution = new OpenJdkDistribution({ + version, + architecture, + packageType, + checkLatest: false + }); + if (useFixturePlatform) { + distribution['getPlatform'] = jest.fn(() => 'linux'); + } + return distribution; +} + +describe('OpenJdkDistribution', () => { + let getSpy: jest.SpiedFunction; + + beforeEach(() => { + getSpy = jest + .spyOn(HttpClient.prototype, 'get') + .mockImplementation(async url => { + const pages: Record = { + 'https://jdk.java.net/': homePage, + 'https://jdk.java.net/26/': currentPage, + 'https://jdk.java.net/27/': earlyAccessPage, + 'https://jdk.java.net/archive/': archivePage + }; + return { + readBody: async () => pages[url] ?? '' + } as Awaited>; + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('resolves the newest matching GA release', async () => { + const result = await createDistribution()['findPackageForDownload']('26'); + + expect(result).toEqual({ + version: '26.0.2+10', + url: 'https://download.java.net/java/GA/jdk26.0.2/hash/10/GPL/openjdk-26.0.2_linux-x64_bin.tar.gz' + }); + }); + + it('resolves an archived GA release', async () => { + const result = + await createDistribution('26.0.1')['findPackageForDownload']('26.0.1'); + + expect(result.version).toBe('26.0.1+8'); + expect(result.url).toContain('/openjdk-26.0.1_linux-x64_bin.tar.gz'); + }); + + it('resolves an exact GA build', async () => { + const result = + await createDistribution('26.0.2+10')['findPackageForDownload']( + '26.0.2+10' + ); + + expect(result.version).toBe('26.0.2+10'); + }); + + it('resolves an exact build from a legacy archive heading', async () => { + const result = + await createDistribution('9.0.4+11')['findPackageForDownload']( + '9.0.4+11' + ); + + expect(result.version).toBe('9.0.4+11'); + expect(result.url).toContain('/binaries/openjdk-9.0.4_linux-x64_bin'); + }); + + it('resolves a four-field Java version', async () => { + const result = + await createDistribution('18.0.1.1')['findPackageForDownload']( + '18.0.1+1' + ); + + expect(result.version).toBe('18.0.1+1'); + expect(result.url).toContain('/openjdk-18.0.1.1_linux-x64_bin.tar.gz'); + }); + + it('resolves an early-access release without requesting the archive', async () => { + const result = + await createDistribution('27-ea')['findPackageForDownload']('27'); + + expect(result).toEqual({ + version: '27.0.0+32', + url: 'https://download.java.net/java/early_access/jdk27/32/GPL/openjdk-27-ea+32_linux-x64_bin.tar.gz' + }); + expect(getSpy).not.toHaveBeenCalledWith('https://jdk.java.net/archive/'); + }); + + it('reports available versions when no release matches', async () => { + await expect( + createDistribution()['findPackageForDownload']('24') + ).rejects.toThrow( + "No matching version found for SemVer '24'.\nDistribution: Oracle OpenJDK" + ); + }); + + it.each([ + ['jre', 'Oracle OpenJDK provides only the `jdk` package type'], + ['jdk+fx', 'Oracle OpenJDK provides only the `jdk` package type'] + ])('rejects the %s package type', async (packageType, message) => { + await expect( + createDistribution('26', 'x64', packageType)['findPackageForDownload']( + '26' + ) + ).rejects.toThrow(message); + }); + + it('rejects unsupported architectures', async () => { + await expect( + createDistribution('26', 'x86')['findPackageForDownload']('26') + ).rejects.toThrow('Unsupported architecture: x86'); + }); + + it('maps supported platforms', () => { + const distribution = createDistribution('26', 'x64', 'jdk', false); + + expect(distribution['getPlatform']('linux')).toBe('linux'); + expect(distribution['getPlatform']('darwin')).toBe('macos'); + expect(distribution['getPlatform']('win32')).toBe('windows'); + expect(() => distribution['getPlatform']('freebsd')).toThrow( + "Platform 'freebsd' is not supported" + ); + }); + + it('parses legacy platform names and archive formats', () => { + const distribution = createDistribution(); + const macRelease = distribution['parseReleases']( + 'tar.gz', + 'macos', + 'x64' + ); + const windowsRelease = distribution['parseReleases']( + 'tar.gz', + 'windows', + 'x64' + ); + + expect(macRelease[0].version).toBe('16.0.0+7'); + expect(windowsRelease[0].version).toBe('10.0.2+13'); + expect(windowsRelease[0].url.endsWith('.tar.gz')).toBe(true); + }); + + it('is registered in the distribution factory', () => { + const distribution = getJavaDistribution('oracle-openjdk', { + version: '26', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + + expect(distribution).toBeInstanceOf(OpenJdkDistribution); + }); +}); diff --git a/dist/setup/index.js b/dist/setup/index.js index 10be04550..d311f7fe7 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -131882,6 +131882,109 @@ class KonaDistribution extends JavaBase { } } +;// CONCATENATED MODULE: ./src/distributions/openjdk/installer.ts + + + + + + + +const OPENJDK_BASE_URL = 'https://jdk.java.net'; +class OpenJdkDistribution extends JavaBase { + constructor(installerOptions) { + super('Oracle OpenJDK', installerOptions); + } + async findPackageForDownload(range) { + if (this.packageType !== 'jdk') { + throw new Error('Oracle OpenJDK provides only the `jdk` package type'); + } + const arch = this.distributionArchitecture(); + if (!['x64', 'aarch64'].includes(arch)) { + throw new Error(`Unsupported architecture: ${this.architecture}`); + } + const platform = this.getPlatform(); + const releases = await this.getAvailableVersions(platform, arch); + const matchingReleases = releases + .filter(release => isVersionSatisfies(range, release.version)) + .sort((left, right) => -semver_default().compareBuild(left.version, right.version)); + if (!matchingReleases.length) { + throw this.createVersionNotFoundError(range, releases.map(release => release.version), `Platform: ${platform}`); + } + return matchingReleases[0]; + } + async downloadTool(javaRelease) { + info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); + let javaArchivePath = await downloadTool(javaRelease.url); + info(`Extracting Java archive...`); + const extension = javaRelease.url.endsWith('.zip') ? 'zip' : 'tar.gz'; + if (extension === 'zip') { + javaArchivePath = renameWinArchive(javaArchivePath); + } + const extractedJavaPath = await extractJdkFile(javaArchivePath, extension); + const archiveName = external_fs_default().readdirSync(extractedJavaPath)[0]; + const archivePath = external_path_default().join(extractedJavaPath, archiveName); + const javaPath = await cacheDir(archivePath, this.toolcacheFolderName, this.getToolcacheVersionName(javaRelease.version), this.architecture); + return { version: javaRelease.version, path: javaPath }; + } + async getAvailableVersions(platform, arch) { + const homePage = await this.fetchPage(`${OPENJDK_BASE_URL}/`); + const releasePageUrls = Array.from(homePage.matchAll(/href="\/(\d+)\/">JDK\s+\d+/g), match => `${OPENJDK_BASE_URL}/${match[1]}/`); + const pages = await Promise.all(releasePageUrls.map(url => this.fetchPage(url))); + if (this.stable) { + pages.push(await this.fetchPage(`${OPENJDK_BASE_URL}/archive/`)); + } + const releases = pages.flatMap(page => this.parseReleases(page, platform, arch)); + return releases.filter(release => release.url.includes('/early_access/') !== this.stable); + } + async fetchPage(url) { + const response = await this.http.get(url); + return response.readBody(); + } + parseReleases(html, platform, arch) { + const platformPattern = platform === 'macos' ? '(?:macos|osx)' : platform; + const extensionPattern = platform === 'windows' ? '(?:zip|tar\\.gz)' : 'tar\\.gz'; + const pattern = new RegExp(`href="(https://download\\.java\\.net/[^"]+/openjdk-([^"_]+)_${platformPattern}-${arch}_bin\\.${extensionPattern})"`, 'g'); + return Array.from(html.matchAll(pattern), match => { + const url = match[1]; + const build = url.match(/\/(\d+)\/(?:GPL\/)?openjdk-/)?.[1] ?? + this.findBuildInArchiveHeading(html, match.index, match[2]); + return { + version: this.toSemver(match[2], build), + url + }; + }); + } + findBuildInArchiveHeading(html, assetIndex, version) { + const headings = Array.from(html.slice(0, assetIndex).matchAll(/\(build\s+([^)]+)\)/g)); + const headingVersion = headings.at(-1)?.[1]; + if (!headingVersion) { + return undefined; + } + const [javaVersion, build] = headingVersion.split('+'); + return javaVersion === version ? build : undefined; + } + toSemver(version, urlBuild) { + const [javaVersion, filenameBuild] = version.replace('-ea', '').split('+'); + const versionParts = javaVersion.split('.'); + const normalizedVersion = convertVersionToSemver(versionParts.length === 1 ? `${javaVersion}.0.0` : javaVersion); + const build = filenameBuild ?? (versionParts.length <= 3 ? urlBuild : undefined); + return build ? `${normalizedVersion}+${build}` : normalizedVersion; + } + getPlatform(platform = process.platform) { + switch (platform) { + case 'darwin': + return 'macos'; + case 'linux': + return 'linux'; + case 'win32': + return 'windows'; + default: + throw new Error(`Platform '${platform}' is not supported. Supported platforms: 'linux', 'macos', 'windows'`); + } + } +} + ;// CONCATENATED MODULE: ./src/distributions/distribution-factory.ts @@ -131898,6 +132001,7 @@ class KonaDistribution extends JavaBase { + var JavaDistribution; (function (JavaDistribution) { JavaDistribution["Adopt"] = "adopt"; @@ -131918,6 +132022,7 @@ var JavaDistribution; JavaDistribution["GraalVMCommunity"] = "graalvm-community"; JavaDistribution["JetBrains"] = "jetbrains"; JavaDistribution["Kona"] = "kona"; + JavaDistribution["OracleOpenJdk"] = "oracle-openjdk"; })(JavaDistribution || (JavaDistribution = {})); function getJavaDistribution(distributionName, installerOptions, jdkFile) { switch (distributionName) { @@ -131956,6 +132061,8 @@ function getJavaDistribution(distributionName, installerOptions, jdkFile) { return new JetBrainsDistribution(installerOptions); case JavaDistribution.Kona: return new KonaDistribution(installerOptions); + case JavaDistribution.OracleOpenJdk: + return new OpenJdkDistribution(installerOptions); default: return null; } diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 36549c668..6ef042a0f 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -154,6 +154,21 @@ steps: - run: java --version ``` +### Oracle OpenJDK +Oracle OpenJDK builds are created and hosted by Oracle under GPLv2+CE. To install the latest early-access build for a feature release, append `-ea` to the Java version: + +```yaml +steps: + - uses: actions/checkout@v7 + - uses: actions/setup-java@v6 + with: + distribution: 'oracle-openjdk' + java-version: '27-ea' + - run: java --version +``` + +Using `27` without the `-ea` suffix selects a stable (GA) release. Oracle archives OpenJDK builds after a limited number of releases and no longer provides security updates for them. To continue receiving security patches, move to Oracle JDK or choose a different vendor. + ### Alibaba Dragonwell **NOTE:** Alibaba Dragonwell only provides jdk. @@ -469,7 +484,7 @@ In this example, `JAVA_HOME` and `java` on `PATH` point to Java 17, while Java 2 If your use-case requires a custom distribution or a version that is not provided by setup-java, you can download it manually and setup-java will take care of the installation and caching on the VM: > [!NOTE] -> This approach also lets you use builds that setup-java does not provide directly, such as **Early Access (EA)** or other unreleased JDK builds (for example, an upcoming feature release or a Loom/Valhalla preview build). Download the desired archive in a prior step and point `jdk-file` at it; setup-java will extract, install, and cache it just like a supported distribution. When targeting multiple architectures, select the correct binary per architecture in your workflow (for example, with a build matrix). +> This approach also lets you use builds that setup-java does not provide directly, such as unreleased Loom/Valhalla preview builds or early-access builds not exposed by a supported distribution. Download the desired archive in a prior step and point `jdk-file` at it; setup-java will extract, install, and cache it just like a supported distribution. When targeting multiple architectures, select the correct binary per architecture in your workflow (for example, with a build matrix). ```yaml steps: @@ -486,23 +501,6 @@ steps: - run: java --version ``` -For example, to use an **Early Access** build from [jdk.java.net](https://jdk.java.net/), download the archive for your runner OS/architecture and install it via `distribution: 'jdkfile'` (example below assumes Linux x64): - -```yaml -steps: - - run: | - download_url="https://download.java.net/java/early_access/jdk25/36/GPL/openjdk-25-ea+36_linux-x64_bin.tar.gz" - wget -O $RUNNER_TEMP/java_package.tar.gz $download_url - - uses: actions/setup-java@v6 - with: - distribution: 'jdkfile' - jdk-file: ${{ runner.temp }}/java_package.tar.gz - java-version: '25.0.0-ea.36' - architecture: x64 - - - run: java --version -``` - If your use-case requires a custom distribution (in the example, alpine-linux is used) or a version that is not provided by setup-java and you want to always install the latest version during runtime, then you can use the following code to auto-download the latest JDK, determine the semver needed for setup-java, and setup-java will take care of the installation and caching on the VM: ```yaml diff --git a/src/distributions/distribution-factory.ts b/src/distributions/distribution-factory.ts index 7ad5db03f..d27844eed 100644 --- a/src/distributions/distribution-factory.ts +++ b/src/distributions/distribution-factory.ts @@ -21,6 +21,7 @@ import { } from './graalvm/installer.js'; import {JetBrainsDistribution} from './jetbrains/installer.js'; import {KonaDistribution} from './kona/installer.js'; +import {OpenJdkDistribution} from './openjdk/installer.js'; enum JavaDistribution { Adopt = 'adopt', @@ -40,7 +41,8 @@ enum JavaDistribution { GraalVM = 'graalvm', GraalVMCommunity = 'graalvm-community', JetBrains = 'jetbrains', - Kona = 'kona' + Kona = 'kona', + OracleOpenJdk = 'oracle-openjdk' } export function getJavaDistribution( @@ -93,6 +95,8 @@ export function getJavaDistribution( return new JetBrainsDistribution(installerOptions); case JavaDistribution.Kona: return new KonaDistribution(installerOptions); + case JavaDistribution.OracleOpenJdk: + return new OpenJdkDistribution(installerOptions); default: return null; } diff --git a/src/distributions/openjdk/installer.ts b/src/distributions/openjdk/installer.ts new file mode 100644 index 000000000..8c8abea69 --- /dev/null +++ b/src/distributions/openjdk/installer.ts @@ -0,0 +1,181 @@ +import * as core from '@actions/core'; +import * as tc from '@actions/tool-cache'; +import fs from 'fs'; +import path from 'path'; +import semver from 'semver'; + +import {JavaBase} from '../base-installer.js'; +import { + JavaDownloadRelease, + JavaInstallerOptions, + JavaInstallerResults +} from '../base-models.js'; +import { + convertVersionToSemver, + extractJdkFile, + isVersionSatisfies, + renameWinArchive +} from '../../util.js'; + +const OPENJDK_BASE_URL = 'https://jdk.java.net'; + +export class OpenJdkDistribution extends JavaBase { + constructor(installerOptions: JavaInstallerOptions) { + super('Oracle OpenJDK', installerOptions); + } + + protected async findPackageForDownload( + range: string + ): Promise { + if (this.packageType !== 'jdk') { + throw new Error('Oracle OpenJDK provides only the `jdk` package type'); + } + + const arch = this.distributionArchitecture(); + if (!['x64', 'aarch64'].includes(arch)) { + throw new Error(`Unsupported architecture: ${this.architecture}`); + } + + const platform = this.getPlatform(); + const releases = await this.getAvailableVersions(platform, arch); + const matchingReleases = releases + .filter(release => isVersionSatisfies(range, release.version)) + .sort((left, right) => -semver.compareBuild(left.version, right.version)); + + if (!matchingReleases.length) { + throw this.createVersionNotFoundError( + range, + releases.map(release => release.version), + `Platform: ${platform}` + ); + } + + return matchingReleases[0]; + } + + protected async downloadTool( + javaRelease: JavaDownloadRelease + ): Promise { + core.info( + `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` + ); + let javaArchivePath = await tc.downloadTool(javaRelease.url); + + core.info(`Extracting Java archive...`); + const extension = javaRelease.url.endsWith('.zip') ? 'zip' : 'tar.gz'; + if (extension === 'zip') { + javaArchivePath = renameWinArchive(javaArchivePath); + } + const extractedJavaPath = await extractJdkFile(javaArchivePath, extension); + + const archiveName = fs.readdirSync(extractedJavaPath)[0]; + const archivePath = path.join(extractedJavaPath, archiveName); + const javaPath = await tc.cacheDir( + archivePath, + this.toolcacheFolderName, + this.getToolcacheVersionName(javaRelease.version), + this.architecture + ); + + return {version: javaRelease.version, path: javaPath}; + } + + private async getAvailableVersions( + platform: string, + arch: string + ): Promise { + const homePage = await this.fetchPage(`${OPENJDK_BASE_URL}/`); + const releasePageUrls = Array.from( + homePage.matchAll(/href="\/(\d+)\/">JDK\s+\d+/g), + match => `${OPENJDK_BASE_URL}/${match[1]}/` + ); + + const pages = await Promise.all( + releasePageUrls.map(url => this.fetchPage(url)) + ); + if (this.stable) { + pages.push(await this.fetchPage(`${OPENJDK_BASE_URL}/archive/`)); + } + + const releases = pages.flatMap(page => + this.parseReleases(page, platform, arch) + ); + + return releases.filter( + release => release.url.includes('/early_access/') !== this.stable + ); + } + + private async fetchPage(url: string): Promise { + const response = await this.http.get(url); + return response.readBody(); + } + + private parseReleases( + html: string, + platform: string, + arch: string + ): JavaDownloadRelease[] { + const platformPattern = platform === 'macos' ? '(?:macos|osx)' : platform; + const extensionPattern = + platform === 'windows' ? '(?:zip|tar\\.gz)' : 'tar\\.gz'; + const pattern = new RegExp( + `href="(https://download\\.java\\.net/[^"]+/openjdk-([^"_]+)_${platformPattern}-${arch}_bin\\.${extensionPattern})"`, + 'g' + ); + + return Array.from(html.matchAll(pattern), match => { + const url = match[1]; + const build = + url.match(/\/(\d+)\/(?:GPL\/)?openjdk-/)?.[1] ?? + this.findBuildInArchiveHeading(html, match.index, match[2]); + return { + version: this.toSemver(match[2], build), + url + }; + }); + } + + private findBuildInArchiveHeading( + html: string, + assetIndex: number, + version: string + ): string | undefined { + const headings = Array.from( + html.slice(0, assetIndex).matchAll(/\(build\s+([^)]+)\)/g) + ); + const headingVersion = headings.at(-1)?.[1]; + if (!headingVersion) { + return undefined; + } + + const [javaVersion, build] = headingVersion.split('+'); + return javaVersion === version ? build : undefined; + } + + private toSemver(version: string, urlBuild?: string): string { + const [javaVersion, filenameBuild] = version.replace('-ea', '').split('+'); + const versionParts = javaVersion.split('.'); + const normalizedVersion = convertVersionToSemver( + versionParts.length === 1 ? `${javaVersion}.0.0` : javaVersion + ); + const build = + filenameBuild ?? (versionParts.length <= 3 ? urlBuild : undefined); + return build ? `${normalizedVersion}+${build}` : normalizedVersion; + } + + private getPlatform(platform: NodeJS.Platform = process.platform): string { + switch (platform) { + case 'darwin': + return 'macos'; + case 'linux': + return 'linux'; + case 'win32': + return 'windows'; + default: + throw new Error( + `Platform '${platform}' is not supported. Supported platforms: 'linux', 'macos', 'windows'` + ); + } + } +}