Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ steps:
- Added Oracle OpenJDK (`oracle-openjdk`), Red Hat Build of OpenJDK (`redhat`), and Liberica Native Image Kit (`liberica-nik`), and expanded Tencent Kona support through JDK 25.
- Added `java-version: latest` to resolve the newest stable GA release from the distribution's remote metadata.
- Expanded install compatibility with JEP 322 multi-field versions such as `18.0.1.1`, Temurin `jdk+jmods` packages, and native musl artifacts on Alpine for Dragonwell, Corretto, Zulu, and Liberica.
- JDK downloads now automatically verify authoritative checksums, and package signature verification defaults to enabled for Temurin and Microsoft builds.
- JDK downloads now automatically verify authoritative checksums. Package signature verification is supported for Temurin and Microsoft builds, with configurable strict enforcement.
- Added `force-download: true` to bypass the tool cache and perform a reproducible fresh install.
- Dependency caching now supports custom paths with `cache-path` and restore-only operation with `cache-read-only: true`.
- Dependency cache keys now include `.mvn/extensions.xml` and `gradle.properties`, preventing stale restores when Maven extensions or Gradle dependency properties change.
Expand Down Expand Up @@ -156,8 +156,8 @@ steps:
| `force-download` | Always download Java and replace any matching version in the tool cache. | `false` |
| `set-default` | Add Java to `PATH` and set `JAVA_HOME`. When `false`, only version-specific `JAVA_HOME_<major>_<arch>` variables are set. | `true` |
| `problem-matcher` | Register Java compiler and uncaught exception problem matchers. | `true` |
| `verify-signature` | Verify downloaded Java package signatures when supported. Defaults to `true` for `temurin` and `microsoft`, and `false` for other distributions. | Automatically enabled for `temurin` and `microsoft` |
| `verify-signature-public-key` | ASCII-armored GPG public key to use for signature verification. Overrides the bundled key. | |
| `verify-signature` | Verify downloaded Java package signatures when supported. Explicitly setting this to `true` makes verification failures fatal. | Distribution-dependent; see [Download integrity and signatures](#download-integrity-and-signatures) |
| `verify-signature-public-key` | One or more ASCII-armored GPG public keys used for signature verification. Concatenate multiple armored key blocks. Custom keys replace the bundled distribution keys. | |
| `token` | Token for fetching GitHub.com-hosted version manifests, useful on GitHub Enterprise Server when unauthenticated requests are rate-limited. | `${{ github.token }}` on GitHub.com; empty string on GHES |
| `cache` | Enable dependency caching for `maven`, `gradle`, or `sbt`. | |
| `cache-jdk` | Cache downloaded JDK installations between jobs. When omitted, JDK caching is enabled only if `cache` is set. Set explicitly to `true` or `false` to override. | Enabled when `cache` is set |
Expand Down Expand Up @@ -247,7 +247,34 @@ GitHub-hosted runners primarily pre-cache Eclipse Temurin JDKs. See the installe

Distributions or individual releases without an authoritative checksum continue to install normally, with the omission reported in debug logs. Installations resolved directly from the runner tool cache — including JDKs preinstalled on the runner image and JDKs installed by an earlier step of the same job — are not downloaded again and are not reverified, even when `verify-signature: true` is set. Use `force-download: true` to always download and verify the archive.

Use `verify-signature: true` to verify package signatures for distributions that support it. Currently supported distributions are `temurin` and `microsoft`; setting it for an unsupported distribution fails the workflow.
Package signature verification is supported for `temurin` and `microsoft`. When `verify-signature` is omitted, the action checks the signature and warns if GPG is unavailable or verification fails, but does not enforce the result. Explicitly setting `verify-signature: true` enforces verification and makes these failures fatal. Setting `verify-signature: true` for an unsupported distribution also fails the workflow.

> [!WARNING]
> Requesting explicit signature verification with verify-signature can fail a build after an unexpected but legitimate vendor signing-key rotation, because the action's bundled keys may not yet include the new key. Confirm a new key through the vendor's trusted documentation before using it.

After confirming a legitimate rotation, configure the updated key with `verify-signature-public-key`. The input accepts one or more ASCII-armored public keys; concatenate complete armored key blocks when both old and new vendor keys are needed during a transition. Custom keys replace, rather than extend, the keys bundled with the selected distribution.

```yaml
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: '25'
verify-signature: true
verify-signature-public-key: |
-----BEGIN PGP PUBLIC KEY BLOCK-----
...vendor key material...
-----END PGP PUBLIC KEY BLOCK-----
```

As a temporary fallback while a legitimate rotation is being investigated, set `verify-signature: false`. This disables package signature verification, although authoritative checksum verification still applies when the vendor publishes a checksum.

```yaml
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: '25'
verify-signature: false
```

## Caching

Expand Down
2 changes: 1 addition & 1 deletion __tests__/cleanup-java.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ function createRegisteredJdk(version = '21.0.8+9') {
architecture: 'x64',
version,
source: `sha256:${path.basename(root)}`,
verification: 'unverified',
verification: 'disabled',
path: jdkPath
};
registerJdk(jdk);
Expand Down
19 changes: 12 additions & 7 deletions __tests__/distributors/base-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,11 @@ jest.unstable_mockModule('@actions/tool-cache', () => ({
}));

jest.unstable_mockModule('../../src/jdk-cache.js', () => ({
getJdkVerificationIdentity: jest.fn((verified: boolean, key?: string) =>
verified ? (key ? 'verified:custom' : 'verified:bundled') : 'unverified'
getJdkVerificationIdentity: jest.fn(
(verified: boolean, enforced: boolean, key?: string) =>
verified
? `${enforced ? 'enforced' : 'check-and-warn'}:${key ? 'custom' : 'bundled'}`
: 'disabled'
),
registerJdk: jest.fn(),
restoreJdk: jest.fn()
Expand Down Expand Up @@ -395,8 +398,10 @@ describe('setupJava', () => {

beforeEach(() => {
(jdkCache.getJdkVerificationIdentity as jest.Mock).mockImplementation(
(verified: boolean, key?: string) =>
verified ? (key ? 'verified:custom' : 'verified:bundled') : 'unverified'
(verified: boolean, enforced: boolean, key?: string) =>
verified
? `${enforced ? 'enforced' : 'check-and-warn'}:${key ? 'custom' : 'bundled'}`
: 'disabled'
);
spyGetToolcachePath = util.getToolcachePath as jest.Mock;
spyGetToolcachePath.mockImplementation(
Expand Down Expand Up @@ -826,7 +831,7 @@ describe('setupJava', () => {
expect(jdkCache.registerJdk).toHaveBeenCalledWith(
expect.objectContaining({
version: actualJavaVersion,
verification: 'unverified'
verification: 'disabled'
})
);
});
Expand Down Expand Up @@ -886,7 +891,7 @@ describe('setupJava', () => {
architecture: 'x86',
version: actualJavaVersion,
source: `some/random_url/java/${actualJavaVersion}`,
verification: 'unverified',
verification: 'disabled',
path: path.join(toolCachePath, 'Java_Empty_jdk', actualJavaVersion)
});
expect(downloadTool).not.toHaveBeenCalled();
Expand Down Expand Up @@ -919,7 +924,7 @@ describe('setupJava', () => {
architecture: 'x86',
version: actualJavaVersion,
source: `some/random_url/java/${actualJavaVersion}`,
verification: 'unverified',
verification: 'disabled',
path: path.join(toolCachePath, 'Java_Empty_jdk', actualJavaVersion)
};
expect(jdkCache.restoreJdk).toHaveBeenCalledWith(expectedIdentity);
Expand Down
6 changes: 3 additions & 3 deletions __tests__/distributors/local-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jest.unstable_mockModule('@actions/tool-cache', () => ({
}));

jest.unstable_mockModule('../../src/jdk-cache.js', () => ({
getJdkVerificationIdentity: jest.fn(() => 'unverified'),
getJdkVerificationIdentity: jest.fn(() => 'disabled'),
registerJdk: jest.fn(),
restoreJdk: jest.fn()
}));
Expand Down Expand Up @@ -106,7 +106,7 @@ describe('setupJava', () => {

beforeEach(() => {
(jdkCache.getJdkVerificationIdentity as jest.Mock).mockReturnValue(
'unverified'
'disabled'
);
spyGetToolcachePath = util.getToolcachePath as jest.Mock;
spyGetToolcachePath.mockImplementation(
Expand Down Expand Up @@ -283,7 +283,7 @@ describe('setupJava', () => {
expect.objectContaining({
distribution: 'jdkfile',
version: actualJavaVersion,
verification: 'unverified'
verification: 'disabled'
})
);
} finally {
Expand Down
58 changes: 58 additions & 0 deletions __tests__/distributors/microsoft-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,64 @@ describe('downloadTool', () => {
);
});

it('warns with key rotation recovery guidance when default verification fails', async () => {
spyVerifySignature.mockRejectedValue(new Error('bad signature'));
const signedDistribution = new MicrosoftDistributions({
version: '17',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
});

await signedDistribution['downloadTool']({
version: '17.0.14+7',
url: 'https://example.com/jdk.tar.gz',
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
});

expect(core.warning).toHaveBeenCalledWith(
expect.stringMatching(
/bad signature.*https:\/\/github\.com\/actions\/setup-java#download-integrity-and-signatures/
)
);
expect(spyExtractJdkFile).toHaveBeenCalled();
});

it('fails with recovery guidance when verification is explicitly enabled', async () => {
spyVerifySignature.mockRejectedValue(new Error('bad signature'));
const signedDistribution = new MicrosoftDistributions({
version: '17',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false,
verifySignature: true
});

await expect(
signedDistribution['downloadTool']({
version: '17.0.14+7',
url: 'https://example.com/jdk.tar.gz',
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
})
).rejects.toThrow(
/bad signature.*https:\/\/github\.com\/actions\/setup-java#download-integrity-and-signatures/
);
expect(spyExtractJdkFile).not.toHaveBeenCalled();
});

it('warns when the signature is missing during default verification', async () => {
await distribution['downloadTool']({
version: '17.0.14+7',
url: 'https://example.com/jdk.tar.gz'
});

expect(core.warning).toHaveBeenCalledWith(
"Input 'verify-signature' is enabled, but no signature URL was found for Microsoft Build of OpenJDK version 17.0.14+7."
);
expect(spyVerifySignature).not.toHaveBeenCalled();
expect(spyExtractJdkFile).toHaveBeenCalled();
});

it('fails when signature is missing and verification is enabled', async () => {
const signedDistribution = new MicrosoftDistributions({
version: '17',
Expand Down
129 changes: 129 additions & 0 deletions __tests__/distributors/temurin-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ jest.unstable_mockModule('../../src/util.js', () => ({
jest.unstable_mockModule('../../src/gpg.js', () => ({
importKey: jest.fn(),
removeGpgHome: jest.fn(),
isGpgAvailable: jest.fn(),
verifyPackageSignature: jest.fn()
}));

Expand Down Expand Up @@ -437,6 +438,7 @@ describe('downloadTool', () => {
beforeEach(() => {
spyDownloadTool = tc.downloadTool as jest.Mock;
spyDownloadTool.mockResolvedValue('/tmp/jdk.tar.gz');
(gpg.isGpgAvailable as jest.Mock).mockResolvedValue(true);
spyVerifySignature = gpg.verifyPackageSignature as jest.Mock;
spyVerifySignature.mockResolvedValue(undefined);
spyExtractJdkFile = util.extractJdkFile as jest.Mock;
Expand Down Expand Up @@ -502,6 +504,133 @@ describe('downloadTool', () => {
expect(spyVerifySignature).not.toHaveBeenCalled();
});

it('skips implicit signature verification when gpg is unavailable', async () => {
(gpg.isGpgAvailable as jest.Mock).mockResolvedValue(false);
const distribution = new TemurinDistribution(
{
version: '17',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
},
TemurinImplementation.Hotspot
);

await expect(
distribution['downloadTool']({
version: '17.0.14+7',
url: 'https://example.com/jdk.tar.gz',
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
})
).resolves.toEqual({version: '17.0.14+7', path: '/tmp/toolcache'});

expect(spyVerifySignature).not.toHaveBeenCalled();
expect(core.warning).toHaveBeenCalledWith(
"Input 'verify-signature' is enabled, but gpg is not available."
);
});

it('fails when signature verification is explicitly enabled without gpg', async () => {
(gpg.isGpgAvailable as jest.Mock).mockResolvedValue(false);
const distribution = new TemurinDistribution(
{
version: '17',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false,
verifySignature: true
},
TemurinImplementation.Hotspot
);

await expect(
distribution['downloadTool']({
version: '17.0.14+7',
url: 'https://example.com/jdk.tar.gz',
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
})
).rejects.toThrow(
"Input 'verify-signature' is enabled, but gpg is not available."
);

expect(spyVerifySignature).not.toHaveBeenCalled();
});

it('warns when implicit signature verification fails', async () => {
spyVerifySignature.mockRejectedValue(new Error('bad signature'));
const distribution = new TemurinDistribution(
{
version: '17',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
},
TemurinImplementation.Hotspot
);

await expect(
distribution['downloadTool']({
version: '17.0.14+7',
url: 'https://example.com/jdk.tar.gz',
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
})
).resolves.toEqual({version: '17.0.14+7', path: '/tmp/toolcache'});

expect(core.warning).toHaveBeenCalledWith(
expect.stringContaining(
'https://github.com/actions/setup-java#download-integrity-and-signatures'
)
);
});

it('fails when explicitly requested signature verification fails', async () => {
spyVerifySignature.mockRejectedValue(new Error('bad signature'));
const distribution = new TemurinDistribution(
{
version: '17',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false,
verifySignature: true
},
TemurinImplementation.Hotspot
);

await expect(
distribution['downloadTool']({
version: '17.0.14+7',
url: 'https://example.com/jdk.tar.gz',
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
})
).rejects.toThrow(
/Failed to verify signature for Temurin version 17\.0\.14\+7.*bad signature.*https:\/\/github\.com\/actions\/setup-java#download-integrity-and-signatures/
);
});

it('warns when a signature is missing and verification is implicit', async () => {
const distribution = new TemurinDistribution(
{
version: '17',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
},
TemurinImplementation.Hotspot
);

await expect(
distribution['downloadTool']({
version: '17.0.14+7',
url: 'https://example.com/jdk.tar.gz'
})
).resolves.toEqual({version: '17.0.14+7', path: '/tmp/toolcache'});

expect(core.warning).toHaveBeenCalledWith(
"Input 'verify-signature' is enabled, but no signature URL was found for Temurin version 17.0.14+7."
);
expect(spyVerifySignature).not.toHaveBeenCalled();
});

it('downloads and adds matching JMODs to the JDK', async () => {
spyDownloadTool
.mockResolvedValueOnce('/tmp/jdk.tar.gz')
Expand Down
Loading