Add conditional JDK caching (#1201)

* Add JDK caching

Cache resolved JDK tool-cache entries by exact platform and release identity, with a default-on cache-jdk input and explicit opt-out.

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

* Apply batched suggestions from code review

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

* Fix JDK cache CI validation

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

* Update brace-expansion security fix

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

* Refresh brace-expansion license metadata

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

* Refine JDK cache semantics

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

* Refine JDK cache documentation

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

* Simplify JDK cache identity

Use one normalized runner OS dimension, reset the internal cache key schema for the unreleased feature, and align documentation, tests, and bundles.

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

* Align JDK cache OS identity

Use the established RUNNER_OS value directly and retain process.platform only as a non-Actions fallback.

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

* Harden JDK cache saves and document tool-cache reuse

Bind each JDK cache key to the installation identity it was computed for,
keep post-job saves best-effort per entry, and state the real reuse and
verification guarantee in the documentation.

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

* docs: restructure README caching section

Rename '## Caching dependencies' to '## Caching' and add a what-gets-cached
overview table covering the dependency, wrapper, and JDK caches. Lead with the
common 'cache: maven' example and the dependency-cache material, and demote JDK
caching into its own subsection.

Also corrects the IMPORTANT callout, which implied JDK caching required an
explicit opt-in; it is enabled implicitly whenever 'cache' is set.

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

* docs: fix caching documentation defects

- Remove pull-request framing that compared behavior to `main`; state the
  tool-cache and `jdkfile` behavior directly and unconditionally.
- Clarify that the JDK cache is a separate cache *entry* from the dependency
  and wrapper caches, while its *enablement* is coupled to `cache`, so the
  opening paragraph agrees with the enablement matrix.
- Cite the actions/setup-java-benchmarks repository instead of an open PR and
  a self-referential PR comment, keeping the measured figures and caveats.
- Keep the `cache`/`cache-jdk` matrix only in docs/advanced-usage.md and
  summarize the rules in prose in README.md to avoid divergence.
- Describe the guarantee that a cache key is only saved with the installation
  it was computed for, instead of documenting inode/size/timestamp internals.

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

* docs: add V6 what's new entry for JDK caching

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e2755464-4e83-47b6-ba71-731bb481b418

---------

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>
Copilot-Session: e2755464-4e83-47b6-ba71-731bb481b418
This commit is contained in:
Bruno Borges
2026-08-04 21:54:10 -04:00
committed by GitHub
parent 7a9a8b1dcc
commit 955f34f16f
26 changed files with 2756 additions and 827 deletions
+38 -18
View File
@@ -33,7 +33,7 @@ steps:
- [Inputs](#inputs)
- [Supported distributions](#supported-distributions)
- [Supported version syntax](#supported-version-syntax)
- [Caching dependencies](#caching-dependencies)
- [Caching](#caching)
- [Multiple JDKs and Maven toolchains](#multiple-jdks-and-maven-toolchains)
- [Publishing packages](#publishing-packages)
- [Advanced usage](#advanced-usage)
@@ -61,6 +61,7 @@ steps:
- JDK downloads now automatically verify authoritative checksums for [supported distributions](#download-integrity-and-signatures).
- 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`.
- Downloaded JDKs are now [cached](#caching-jdk-installations) automatically when `cache` is set; use `cache-jdk` to enable or disable it independently.
- Set `problem-matcher: false` to disable Java compiler and uncaught-exception annotations.
- GraalVM distributions now set `GRAALVM_HOME` in addition to `JAVA_HOME`.
- Invalid boolean values, unsupported distribution/package/platform combinations, and mismatched Maven toolchain ID counts now fail with targeted errors.
@@ -161,9 +162,10 @@ steps:
| `verify-signature-public-key` | ASCII-armored GPG public key to use for signature verification. Overrides the bundled key. | |
| `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 |
| `cache-dependency-path` | Dependency file paths used for cache key hashing. Supports globs and multiline values. | Auto-detected by package manager |
| `cache-path` | Cache paths to use instead of the package manager's default dependency cache path. Supports multiline values and exclusions. | |
| `cache-read-only` | Restore caches without saving changes in the post step. | `false` |
| `cache-read-only` | Restore dependency, wrapper, and JDK caches without saving changes in the post step. | `false` |
| `server-id` | Maven repository ID used in generated `settings.xml`. | `github` |
| `server-username-env-var` | Environment variable name for Maven repository username. | `GITHUB_ACTOR` |
| `server-password-env-var` | Environment variable name for Maven repository password or token. | `GITHUB_TOKEN` |
@@ -175,6 +177,8 @@ steps:
| `mvn-toolchain-vendor` | Maven Toolchain vendor value. | `${distribution}` |
| `show-download-progress` | Keep Maven artifact download and transfer progress in logs. When `false`, the action adds `-ntp` to `MAVEN_ARGS`. | `false` |
- `java-package`: Supported package types are `jdk`, `jre`, `jdk+fx`, `jre+fx`, `jdk+crac`, `jre+crac`, `jdk+jmods`, `jdk+jcef`, `jre+jcef`, `jdk+ft`, and `jre+ft`. Availability varies by distribution.
Deprecated aliases `jdkFile`, `server-username`, `server-password`, and `gpg-passphrase` remain accepted for compatibility, but should be replaced with the current input names.
## Outputs
@@ -238,11 +242,19 @@ GitHub-hosted runners primarily pre-cache Eclipse Temurin JDKs. See the installe
`setup-java` automatically verifies downloaded archive checksums when a selected distribution publishes an authoritative checksum. Automatic checksum verification currently applies to `temurin`, `semeru`, `corretto`, `dragonwell`, `kona`, `sapmachine`, `graalvm`, `graalvm-community`, `zulu`, `oracle`, `oracle-openjdk`, `microsoft`, and `jetbrains`.
Distributions or individual releases without an authoritative checksum continue to install normally, with the omission reported in debug logs. Archives resolved directly from the runner tool cache are not downloaded again and are not reverified.
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.
## Caching dependencies
## Caching
`setup-java` manages three kinds of caches. Each one is restored and saved as a separate cache entry.
| Cache | What it stores | Key based on | How it is enabled |
| --- | --- | --- | --- |
| Dependency cache | Downloaded dependencies, such as `~/.m2/repository`, `~/.gradle/caches`, or the sbt cache paths | Runner OS, architecture, package manager, and a hash of the dependency files | Set `cache` to `maven`, `gradle`, or `sbt` |
| Wrapper caches | Maven and Gradle wrapper distributions (`~/.m2/wrapper/dists`, `~/.gradle/wrapper`) | Runner OS, architecture, wrapper cache name, and a hash of the wrapper properties | Set `cache` to `maven` or `gradle` |
| JDK cache | The downloaded JDK installation | Runner OS, architecture, distribution, package type, resolved version, release identity, and signature-verification identity | Enabled implicitly whenever `cache` is set, or explicitly with `cache-jdk: true`. Opt out with `cache-jdk: false` |
Set `cache` to `maven`, `gradle`, or `sbt` to cache dependencies with minimal configuration.
@@ -294,9 +306,30 @@ Use `cache-path` when the build tool stores dependencies outside the default loc
`cache-path` changes what is restored and saved, but not the cache key. Jobs that should share a cache key must use the same OS, architecture, package manager, dependency files, and cache paths.
### Wrapper caches
Maven and Gradle wrapper distributions are restored and saved as additional cache entries, separate from the primary dependency cache. These entries have their own keys in the form `setup-java-<runner-os>-<node-arch>-<wrapper-cache-name>-<file-hash>`.
| Package manager | Wrapper cache name | Cached path | Files used for wrapper-cache key |
| --- | --- | --- | --- |
| Maven | `maven-wrapper` | `~/.m2/wrapper/dists` | `**/.mvn/wrapper/maven-wrapper.properties` |
| Gradle | `gradle-wrapper` | `~/.gradle/wrapper` | `**/gradle-wrapper.properties` |
These wrapper caches are independent from dependency caches, so they remain useful even when dependency files change frequently. The wrapper properties are also part of the Maven and Gradle primary dependency-cache key because wrapper changes can affect how dependencies are resolved, but the wrapper distribution files themselves are stored in the separate wrapper cache entries above.
For advanced Gradle caching features such as build output caching, configuration cache support, encrypted cache storage, cleanup, and fine-grained cache control, consider [`gradle/actions/setup-gradle`](https://github.com/gradle/actions/tree/main/setup-gradle).
### Caching JDK installations
The JDK cache stores the downloaded JDK installation so later runs skip the download. It is enabled implicitly whenever dependency `cache` is set, so most workflows that cache dependencies are already caching the JDK. Set `cache-jdk: true` to enable it without dependency caching, or `cache-jdk: false` to opt out while keeping dependency caching. With neither `cache` nor `cache-jdk` set, nothing is cached.
> [!IMPORTANT]
> Because JDK caching is on by default whenever `cache` is set, review [Caching JDK installations](docs/advanced-usage.md#caching-jdk-installations)
> for the full `cache`/`cache-jdk` matrix, cache identity and storage impact.
### Read-only caches
Set `cache-read-only: true` to restore dependency caches without saving changes in the post action. This is useful for pull requests, merge queues, short-lived branches, and matrix fan-out jobs that should only consume caches produced elsewhere.
Set `cache-read-only: true` to restore dependency, wrapper, and JDK caches without saving changes in the post action. This is useful for pull requests, merge queues, short-lived branches, and matrix fan-out jobs that should only consume caches produced elsewhere.
```yaml
- uses: actions/setup-java@v5
@@ -339,19 +372,6 @@ jobs:
- run: mvn ${{ matrix.goal }}
```
### Wrapper caches
Maven and Gradle wrapper distributions are restored and saved as additional cache entries, separate from the primary dependency cache. These entries have their own keys in the form `setup-java-<runner-os>-<node-arch>-<wrapper-cache-name>-<file-hash>`.
| Package manager | Wrapper cache name | Cached path | Files used for wrapper-cache key |
| --- | --- | --- | --- |
| Maven | `maven-wrapper` | `~/.m2/wrapper/dists` | `**/.mvn/wrapper/maven-wrapper.properties` |
| Gradle | `gradle-wrapper` | `~/.gradle/wrapper` | `**/gradle-wrapper.properties` |
These wrapper caches are independent from dependency caches, so they remain useful even when dependency files change frequently. The wrapper properties are also part of the Maven and Gradle primary dependency-cache key because wrapper changes can affect how dependencies are resolved, but the wrapper distribution files themselves are stored in the separate wrapper cache entries above.
For advanced Gradle caching features such as build output caching, configuration cache support, encrypted cache storage, cleanup, and fine-grained cache control, consider [`gradle/actions/setup-gradle`](https://github.com/gradle/actions/tree/main/setup-gradle).
### Cache segment restore timeout
Cache downloads are split into segments. To reduce the chance of a stuck segment blocking a workflow, set `SEGMENT_DOWNLOAD_TIMEOUT_MINS`:
+152
View File
@@ -8,6 +8,9 @@ import {
beforeAll,
afterAll
} from '@jest/globals';
import fs from 'fs';
import os from 'os';
import path from 'path';
// Mock @actions/cache before importing source modules
const real_cache_module = await import('@actions/cache');
@@ -60,6 +63,9 @@ const core = await import('@actions/core');
const cache = await import('@actions/cache');
const {run: cleanup} = await import('../src/cleanup-java.js');
const util = await import('../src/util.js');
const {registerJdk, buildJdkCacheKey} = await import('../src/jdk-cache.js');
const jdkTempRoots: string[] = [];
describe('cleanup', () => {
let spyWarning: any;
@@ -88,6 +94,9 @@ describe('cleanup', () => {
});
afterEach(() => {
while (jdkTempRoots.length) {
fs.rmSync(jdkTempRoots.pop()!, {recursive: true, force: true});
}
resetState();
jest.resetAllMocks();
jest.clearAllMocks();
@@ -163,6 +172,103 @@ describe('cleanup', () => {
expect(spyCacheSave).toHaveBeenCalled();
});
it('saves the JDK cache without dependency caching', async () => {
const {key, path: jdkPath, state} = createRegisteredJdk();
(core.getInput as jest.Mock<any>).mockImplementation((name: string) =>
name === 'cache-jdk' ? 'true' : ''
);
(core.getState as jest.Mock<any>).mockImplementation((name: string) =>
name === 'jdk-caches' ? state : ''
);
spyCacheSave.mockResolvedValue(1);
await cleanup();
expect(spyCacheSave).toHaveBeenCalledWith([jdkPath], key);
});
it('does not save a JDK cache when cache-jdk is disabled', async () => {
(core.getInput as jest.Mock<any>).mockImplementation((name: string) =>
name === 'cache-jdk' ? 'false' : ''
);
await cleanup();
expect(spyCacheSave).not.toHaveBeenCalled();
});
it.each([
['', '', false],
['', 'true', true],
['', 'false', false],
['maven', '', true],
['maven', 'true', true],
['maven', 'false', false]
])(
'uses effective JDK caching for cache=%j and cache-jdk=%j',
async (cacheInput, cacheJdkInput, expectedJdkSave) => {
const {key: jdkKey, path: jdkPath, state} = createRegisteredJdk();
(core.getInput as jest.Mock<any>).mockImplementation((name: string) => {
if (name === 'cache') return cacheInput;
if (name === 'cache-jdk') return cacheJdkInput;
return '';
});
(core.getState as jest.Mock<any>).mockImplementation((name: string) =>
name === 'jdk-caches' ? state : ''
);
spyCacheSave.mockResolvedValue(1);
await cleanup();
const jdkSaveCalls = spyCacheSave.mock.calls.filter(
([, key]) => key === jdkKey
);
expect(jdkSaveCalls).toHaveLength(expectedJdkSave ? 1 : 0);
if (expectedJdkSave) {
expect(spyCacheSave).toHaveBeenCalledWith([jdkPath], jdkKey);
}
}
);
it('keeps saving the remaining JDK caches when one save fails', async () => {
const first = createRegisteredJdk();
const second = createRegisteredJdk('17.0.19+9');
(core.getInput as jest.Mock<any>).mockImplementation((name: string) =>
name === 'cache-jdk' ? 'true' : ''
);
(core.getState as jest.Mock<any>).mockImplementation((name: string) =>
name === 'jdk-caches' ? second.state : ''
);
spyCacheSave.mockImplementation(async (paths: string[]) => {
if (paths[0] === first.path) {
throw new Error('Unexpected save failure');
}
return 1;
});
await cleanup();
expect(spyCacheSave).toHaveBeenCalledWith([first.path], first.key);
expect(spyCacheSave).toHaveBeenCalledWith([second.path], second.key);
expect(spyCoreError).not.toHaveBeenCalled();
});
it('does not save a JDK installation that was replaced after registration', async () => {
const {key, path: jdkPath, state, replace} = createRegisteredJdk();
(core.getInput as jest.Mock<any>).mockImplementation((name: string) =>
name === 'cache-jdk' ? 'true' : ''
);
(core.getState as jest.Mock<any>).mockImplementation((name: string) =>
name === 'jdk-caches' ? state : ''
);
spyCacheSave.mockResolvedValue(1);
replace();
await cleanup();
expect(spyCacheSave).not.toHaveBeenCalledWith([jdkPath], key);
});
});
function resetState() {
@@ -199,3 +305,49 @@ function createStateForSuccessfulRestoreWithWrapper(packageManager: string) {
}
});
}
/**
* Register a real JDK installation in a temporary tool cache so the post-job
* save sees the same installation identity that setup recorded.
*/
function createRegisteredJdk(version = '21.0.8+9') {
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'setup-java-cleanup-jdk-')
);
jdkTempRoots.push(root);
const jdkPath = path.join(
root,
'Java_temurin_jdk',
version.replace('+', '-')
);
const write = (marker: string) => {
const architecturePath = path.join(jdkPath, 'x64');
fs.rmSync(architecturePath, {recursive: true, force: true});
fs.rmSync(`${architecturePath}.complete`, {force: true});
fs.mkdirSync(architecturePath, {recursive: true});
fs.writeFileSync(path.join(architecturePath, 'release'), marker);
fs.writeFileSync(`${architecturePath}.complete`, marker);
};
write('installed');
const jdk = {
distribution: 'temurin',
packageType: 'jdk',
architecture: 'x64',
version,
source: `sha256:${path.basename(root)}`,
verification: 'unverified',
path: jdkPath
};
registerJdk(jdk);
const state = (
(core.saveState as jest.Mock).mock.calls.at(-1) as string[]
)[1];
return {
key: buildJdkCacheKey(jdk),
path: jdkPath,
state,
replace: () => write('replaced-by-a-later-step')
};
}
+121 -1
View File
@@ -70,6 +70,14 @@ 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'
),
registerJdk: jest.fn(),
restoreJdk: jest.fn()
}));
const real_util_module = await import('../../src/util.js');
jest.unstable_mockModule('../../src/util.js', () => ({
...real_util_module,
@@ -86,6 +94,7 @@ jest.unstable_mockModule('../../src/util.js', () => ({
const core = await import('@actions/core');
const tc = await import('@actions/tool-cache');
const util = await import('../../src/util.js');
const jdkCache = await import('../../src/jdk-cache.js');
const {JavaBase} = await import('../../src/distributions/base-installer.js');
class EmptyJavaBase extends JavaBase {
@@ -336,6 +345,10 @@ describe('setupJava', () => {
let spyCoreError: any;
beforeEach(() => {
(jdkCache.getJdkVerificationIdentity as jest.Mock).mockImplementation(
(verified: boolean, key?: string) =>
verified ? (key ? 'verified:custom' : 'verified:bundled') : 'unverified'
);
spyGetToolcachePath = util.getToolcachePath as jest.Mock;
spyGetToolcachePath.mockImplementation(
(toolname: string, javaVersion: string, architecture: string) => {
@@ -463,8 +476,10 @@ describe('setupJava', () => {
architecture: 'x86',
packageType: 'jdk',
checkLatest: false,
forceDownload: true
forceDownload: true,
cacheJdk: true
});
const findInToolcache = jest.fn(() => ({
version: actualJavaVersion,
path: javaPathInstalled
@@ -484,6 +499,111 @@ describe('setupJava', () => {
expect(spyCoreInfo).not.toHaveBeenCalledWith(
`Resolved Java ${actualJavaVersion} from tool-cache`
);
expect(jdkCache.restoreJdk).not.toHaveBeenCalled();
expect(jdkCache.registerJdk).toHaveBeenCalledWith(
expect.objectContaining({
version: actualJavaVersion,
verification: 'unverified'
})
);
});
it.each([
[false, false, false, false],
[false, true, true, true],
[true, false, false, false],
[true, true, false, true]
])(
'handles force-download=%s and cache-jdk=%s',
async (forceDownload, cacheJdkEnabled, restores, registers) => {
mockJavaBase = new EmptyJavaBase({
version: actualJavaVersion,
architecture: 'x86',
packageType: 'jdk',
checkLatest: true,
forceDownload,
cacheJdk: cacheJdkEnabled
});
(jdkCache.restoreJdk as jest.Mock).mockResolvedValue(false);
await mockJavaBase.setupJava();
expect(jdkCache.restoreJdk).toHaveBeenCalledTimes(restores ? 1 : 0);
expect(jdkCache.registerJdk).toHaveBeenCalledTimes(registers ? 1 : 0);
}
);
it('restores the exact resolved JDK before downloading', async () => {
const toolCachePath = path.join('toolcache');
jest.replaceProperty(process, 'env', {
...process.env,
RUNNER_TOOL_CACHE: toolCachePath
});
mockJavaBase = new EmptyJavaBase({
version: '11',
architecture: 'x86',
packageType: 'jdk',
checkLatest: true,
cacheJdk: true
});
const downloadTool = jest.spyOn(mockJavaBase as any, 'downloadTool');
(jdkCache.restoreJdk as jest.Mock).mockResolvedValue(true);
jest
.spyOn(mockJavaBase as any, 'getRestoredJdkPath')
.mockReturnValue(javaPathInstalled);
await expect(mockJavaBase.setupJava()).resolves.toEqual({
version: actualJavaVersion,
path: javaPathInstalled
});
expect(jdkCache.restoreJdk).toHaveBeenCalledWith({
distribution: 'Empty',
packageType: 'jdk',
architecture: 'x86',
version: actualJavaVersion,
source: `some/random_url/java/${actualJavaVersion}`,
verification: 'unverified',
path: path.join(toolCachePath, 'Java_Empty_jdk', actualJavaVersion)
});
expect(downloadTool).not.toHaveBeenCalled();
expect(spyCoreInfo).not.toHaveBeenCalledWith('Trying to download...');
// A restored entry is already stored under its key; it must not be
// re-registered for a post-job save.
expect(jdkCache.registerJdk).not.toHaveBeenCalled();
});
it('registers the downloaded JDK identity after a JDK cache miss', async () => {
const toolCachePath = path.join('toolcache');
jest.replaceProperty(process, 'env', {
...process.env,
RUNNER_TOOL_CACHE: toolCachePath
});
mockJavaBase = new EmptyJavaBase({
version: '11',
architecture: 'x86',
packageType: 'jdk',
checkLatest: true,
cacheJdk: true
});
(jdkCache.restoreJdk as jest.Mock).mockResolvedValue(false);
await mockJavaBase.setupJava();
const expectedIdentity = {
distribution: 'Empty',
packageType: 'jdk',
architecture: 'x86',
version: actualJavaVersion,
source: `some/random_url/java/${actualJavaVersion}`,
verification: 'unverified',
path: path.join(toolCachePath, 'Java_Empty_jdk', actualJavaVersion)
};
expect(jdkCache.restoreJdk).toHaveBeenCalledWith(expectedIdentity);
// Registration happens after the installation exists, so the post-job save
// can detect a later step replacing it.
expect(jdkCache.registerJdk).toHaveBeenCalledWith(expectedIdentity);
expect(spyCoreInfo).toHaveBeenCalledWith('Trying to download...');
});
it.each([
@@ -12,6 +12,9 @@ import fs from 'fs';
import path from 'path';
import * as semver from 'semver';
import os from 'os';
const realStatSync = fs.statSync;
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
@@ -54,6 +57,12 @@ jest.unstable_mockModule('@actions/tool-cache', () => ({
evaluateVersions: jest.fn()
}));
jest.unstable_mockModule('../../src/jdk-cache.js', () => ({
getJdkVerificationIdentity: jest.fn(() => 'unverified'),
registerJdk: jest.fn(),
restoreJdk: jest.fn()
}));
const real_util_module = await import('../../src/util.js');
jest.unstable_mockModule('../../src/util.js', () => ({
...real_util_module,
@@ -70,6 +79,7 @@ jest.unstable_mockModule('../../src/util.js', () => ({
const core = await import('@actions/core');
const tc = await import('@actions/tool-cache');
const util = await import('../../src/util.js');
const jdkCache = await import('../../src/jdk-cache.js');
const {LocalDistribution} =
await import('../../src/distributions/local/installer.js');
@@ -95,6 +105,9 @@ describe('setupJava', () => {
const expectedJdkFile = 'JavaLocalJdkFile';
beforeEach(() => {
(jdkCache.getJdkVerificationIdentity as jest.Mock).mockReturnValue(
'unverified'
);
spyGetToolcachePath = util.getToolcachePath as jest.Mock;
spyGetToolcachePath.mockImplementation(
(toolname: string, javaVersion: string, architecture: string) => {
@@ -231,6 +244,72 @@ describe('setupJava', () => {
);
});
it.each([
[false, true, true],
[true, false, true]
])(
'handles jdkfile caching with force-download=%s',
async (forceDownload, restores, registers) => {
const temporaryDirectory = fs.mkdtempSync(
path.join(os.tmpdir(), 'setup-java-local-cache-')
);
const jdkFile = path.join(temporaryDirectory, 'java.tar.gz');
fs.writeFileSync(jdkFile, 'jdk archive');
spyGetToolcachePath.mockReturnValue('');
spyFsStat.mockImplementation((file: string) => realStatSync(file));
(jdkCache.restoreJdk as jest.Mock).mockResolvedValue(false);
try {
mockJavaBase = new LocalDistribution(
{
version: actualJavaVersion,
architecture: 'x86',
packageType: 'jdk',
checkLatest: false,
forceDownload,
cacheJdk: true
},
jdkFile
);
await mockJavaBase.setupJava();
expect(jdkCache.restoreJdk).toHaveBeenCalledTimes(restores ? 1 : 0);
expect(jdkCache.registerJdk).toHaveBeenCalledTimes(registers ? 1 : 0);
expect(
(jdkCache.restoreJdk as jest.Mock).mock.calls[0]?.[0] ??
(jdkCache.registerJdk as jest.Mock).mock.calls[0]?.[0]
).toEqual(
expect.objectContaining({
distribution: 'jdkfile',
version: actualJavaVersion,
verification: 'unverified'
})
);
} finally {
fs.rmSync(temporaryDirectory, {recursive: true});
}
}
);
it('rejects signature verification for jdkfile archives', async () => {
mockJavaBase = new LocalDistribution(
{
version: actualJavaVersion,
architecture: 'x86',
packageType: 'jdk',
checkLatest: false,
verifySignature: true
},
expectedJdkFile
);
await expect(mockJavaBase.setupJava()).rejects.toThrow(
"Input 'verify-signature' is not supported for distribution 'jdkfile'."
);
expect(spyGetToolcachePath).not.toHaveBeenCalled();
});
it("java is resolved from toolcache, jdkfile doesn't exist", async () => {
const inputs = {
version: actualJavaVersion,
+325
View File
@@ -0,0 +1,325 @@
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
import fs from 'fs';
import os from 'os';
import path from 'path';
jest.unstable_mockModule('@actions/cache', () => ({
restoreCache: jest.fn(),
saveCache: jest.fn(),
ReserveCacheError: class ReserveCacheError extends Error {
constructor(message: string) {
super(message);
this.name = 'ReserveCacheError';
}
}
}));
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
saveState: jest.fn(),
getState: jest.fn()
}));
jest.unstable_mockModule('../src/cache-feature.js', () => ({
isCacheFeatureAvailable: jest.fn()
}));
const cache = await import('@actions/cache');
const core = await import('@actions/core');
const cacheFeature = await import('../src/cache-feature.js');
const {
buildJdkCacheKey,
getJdkVerificationIdentity,
registerJdk,
restoreJdk,
saveJdkCaches
} = await import('../src/jdk-cache.js');
const jdk = {
distribution: 'temurin',
packageType: 'jdk',
architecture: 'x64',
version: '21.0.8+9',
source: 'sha256:abc123',
verification: 'unverified',
path: '/toolcache/Java_temurin_jdk/21.0.8-9'
};
describe('JDK cache', () => {
const tempRoots: string[] = [];
const createInstallation = (marker = 'a'): string => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'setup-java-jdk-'));
tempRoots.push(root);
const jdkPath = path.join(root, 'Java_temurin_jdk', '21.0.8-9');
writeInstallation(jdkPath, marker);
return jdkPath;
};
const writeInstallation = (jdkPath: string, marker: string): void => {
const architecturePath = path.join(jdkPath, 'x64');
fs.rmSync(architecturePath, {recursive: true, force: true});
fs.rmSync(`${architecturePath}.complete`, {force: true});
fs.mkdirSync(path.join(architecturePath, 'bin'), {recursive: true});
fs.writeFileSync(path.join(architecturePath, 'bin', 'java'), marker);
fs.writeFileSync(`${architecturePath}.complete`, marker);
};
const lastState = (): string =>
((core.saveState as jest.Mock).mock.calls.at(-1) as string[])[1];
beforeEach(() => {
jest.resetAllMocks();
(cacheFeature.isCacheFeatureAvailable as jest.Mock).mockReturnValue(true);
process.env['RUNNER_OS'] = 'Linux';
});
afterEach(() => {
jest.restoreAllMocks();
delete process.env['RUNNER_OS'];
while (tempRoots.length) {
fs.rmSync(tempRoots.pop()!, {recursive: true, force: true});
}
});
it('builds distinct keys for incompatible JDK identities', () => {
const key = buildJdkCacheKey(jdk);
expect(key).toMatch(/^setup-java-jdk-v1-Linux-x64-[a-f0-9]{64}$/);
expect(buildJdkCacheKey({...jdk, architecture: 'aarch64'})).not.toBe(key);
expect(buildJdkCacheKey({...jdk, distribution: 'zulu'})).not.toBe(key);
expect(buildJdkCacheKey({...jdk, packageType: 'jre'})).not.toBe(key);
expect(buildJdkCacheKey({...jdk, version: '21.0.7+6'})).not.toBe(key);
expect(buildJdkCacheKey({...jdk, source: 'sha256:def456'})).not.toBe(key);
});
it('preserves canonical runner OS values and separates operating systems', () => {
process.env['RUNNER_OS'] = 'Linux';
const linux = buildJdkCacheKey(jdk);
process.env['RUNNER_OS'] = 'Windows';
const windows = buildJdkCacheKey(jdk);
process.env['RUNNER_OS'] = 'macOS';
const macos = buildJdkCacheKey(jdk);
expect(new Set([linux, windows, macos])).toHaveProperty('size', 3);
expect(linux).toMatch(/^setup-java-jdk-v1-Linux-x64-/);
expect(windows).toMatch(/^setup-java-jdk-v1-Windows-x64-/);
expect(macos).toMatch(/^setup-java-jdk-v1-macOS-x64-/);
});
it('falls back to process.platform without RUNNER_OS', () => {
delete process.env['RUNNER_OS'];
expect(buildJdkCacheKey(jdk)).toMatch(
new RegExp(`^setup-java-jdk-v1-${process.platform}-x64-`)
);
});
it('separates unverified, bundled-key, and custom-key caches', () => {
const unverified = getJdkVerificationIdentity(false);
const bundled = getJdkVerificationIdentity(true);
const customA = getJdkVerificationIdentity(
true,
'-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nkey-a\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n'
);
const customANormalized = getJdkVerificationIdentity(
true,
'-----BEGIN PGP PUBLIC KEY BLOCK-----\nkey-a\n-----END PGP PUBLIC KEY BLOCK-----'
);
const customB = getJdkVerificationIdentity(true, 'different-key');
expect(new Set([unverified, bundled, customA, customB])).toHaveProperty(
'size',
4
);
expect(customA).toBe(customANormalized);
expect(customA).not.toContain('key-a');
expect(
new Set(
[unverified, bundled, customA, customB].map(verification =>
buildJdkCacheKey({...jdk, verification})
)
)
).toHaveProperty('size', 4);
});
it('restores and records an exact JDK cache hit', async () => {
(cache.restoreCache as jest.Mock).mockResolvedValue(buildJdkCacheKey(jdk));
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
await expect(restoreJdk(jdk)).resolves.toBe(true);
expect(cache.restoreCache).toHaveBeenCalledWith(
[jdk.path],
buildJdkCacheKey(jdk)
);
const architecturePath = path.join(jdk.path, 'x64');
expect(fs.existsSync).toHaveBeenCalledWith(architecturePath);
expect(fs.existsSync).toHaveBeenCalledWith(`${architecturePath}.complete`);
expect(core.saveState).toHaveBeenCalledWith(
'jdk-caches',
expect.stringContaining(buildJdkCacheKey(jdk))
);
});
it('falls back to download when restoration fails', async () => {
(cache.restoreCache as jest.Mock).mockRejectedValue(
new Error('cache unavailable')
);
await expect(restoreJdk(jdk)).resolves.toBe(false);
expect(core.warning).toHaveBeenCalledWith(
'Failed to restore JDK cache: cache unavailable'
);
});
it('saves a downloaded JDK registered after installation', async () => {
const jdkPath = createInstallation();
const installed = {...jdk, path: jdkPath};
const key = buildJdkCacheKey(installed);
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreJdk(installed);
registerJdk(installed);
(core.getState as jest.Mock).mockReturnValue(lastState());
(cache.saveCache as jest.Mock).mockResolvedValue(1);
await saveJdkCaches();
expect(cache.saveCache).toHaveBeenCalledWith([jdkPath], key);
});
it('does not save an installation that was replaced after registration', async () => {
const jdkPath = createInstallation();
const installed = {...jdk, path: jdkPath};
const key = buildJdkCacheKey(installed);
registerJdk(installed);
(core.getState as jest.Mock).mockReturnValue(lastState());
writeInstallation(jdkPath, 'replaced-by-a-later-step');
await saveJdkCaches();
expect(cache.saveCache).not.toHaveBeenCalledWith([jdkPath], key);
expect(core.warning).toHaveBeenCalledWith(
expect.stringContaining('was replaced after it was registered')
);
});
it('saves only the key matching the installation that occupies the path', async () => {
const jdkPath = createInstallation();
const verified = {...jdk, path: jdkPath, verification: 'verified:bundled'};
const unverified = {...jdk, path: jdkPath};
registerJdk(verified);
writeInstallation(jdkPath, 'force-downloaded-without-verification');
registerJdk(unverified);
(core.getState as jest.Mock).mockReturnValue(lastState());
(cache.saveCache as jest.Mock).mockResolvedValue(1);
await saveJdkCaches();
expect(cache.saveCache).not.toHaveBeenCalledWith(
[jdkPath],
buildJdkCacheKey(verified)
);
expect(cache.saveCache).toHaveBeenCalledWith(
[jdkPath],
buildJdkCacheKey(unverified)
);
});
it('does not save a path that was never registered as installed', async () => {
const jdkPath = createInstallation();
const installed = {...jdk, path: jdkPath};
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreJdk(installed);
(core.getState as jest.Mock).mockReturnValue(lastState());
await saveJdkCaches();
expect(cache.saveCache).not.toHaveBeenCalledWith(
[jdkPath],
buildJdkCacheKey(installed)
);
});
it('keeps saving the remaining JDK caches when one save fails', async () => {
const failingPath = createInstallation();
const succeedingPath = createInstallation();
const failing = {...jdk, path: failingPath};
const succeeding = {...jdk, path: succeedingPath, version: '17.0.19+9'};
registerJdk(failing);
registerJdk(succeeding);
(core.getState as jest.Mock).mockReturnValue(lastState());
(cache.saveCache as jest.Mock).mockImplementation(
async (paths: unknown) => {
if ((paths as string[])[0] === failingPath) {
throw new Error('cache service unavailable');
}
return 1;
}
);
await expect(saveJdkCaches()).resolves.toBeUndefined();
expect(cache.saveCache).toHaveBeenCalledWith(
[succeedingPath],
buildJdkCacheKey(succeeding)
);
expect(core.warning).toHaveBeenCalledWith(
expect.stringContaining('cache service unavailable')
);
expect(core.info).toHaveBeenCalledWith(
`JDK cache saved with the key: ${buildJdkCacheKey(succeeding)}`
);
});
it('reports a reserved cache key without failing the remaining saves', async () => {
const reservedPath = createInstallation();
const reserved = {...jdk, path: reservedPath};
registerJdk(reserved);
(core.getState as jest.Mock).mockReturnValue(lastState());
(cache.saveCache as jest.Mock).mockRejectedValue(
new cache.ReserveCacheError('Unable to reserve cache')
);
await expect(saveJdkCaches()).resolves.toBeUndefined();
expect(core.info).toHaveBeenCalledWith('Unable to reserve cache');
});
it('registers a force-downloaded JDK without restoring it', () => {
const jdkPath = createInstallation();
registerJdk({...jdk, path: jdkPath});
expect(cache.restoreCache).not.toHaveBeenCalled();
expect(core.saveState).toHaveBeenCalledWith(
'jdk-caches',
expect.stringContaining(buildJdkCacheKey({...jdk, path: jdkPath}))
);
});
it('does not save an exact JDK cache hit again', async () => {
const key = buildJdkCacheKey(jdk);
(core.getState as jest.Mock).mockReturnValue(
JSON.stringify([
{
key,
path: jdk.path,
architecture: jdk.architecture,
matchedKey: key
}
])
);
await saveJdkCaches();
expect(cache.saveCache).not.toHaveBeenCalled();
});
});
+3 -1
View File
@@ -33,7 +33,8 @@ jest.unstable_mockModule('fs', () => ({
jest.unstable_mockModule('../src/util.js', () => ({
getBooleanInput: jest.fn(),
getVersionFromFileContent: jest.fn()
getVersionFromFileContent: jest.fn(),
isJdkCacheEnabled: jest.fn()
}));
jest.unstable_mockModule('../src/toolchains.js', () => ({
@@ -98,6 +99,7 @@ describe('setup-java conditional module loading', () => {
return booleanInputs.get(name as string) ?? defaultValue;
}
);
(util.isJdkCacheEnabled as jest.Mock).mockReturnValue(false);
(toolchains.configureToolchains as jest.Mock).mockResolvedValue(undefined);
});
+51 -1
View File
@@ -33,7 +33,8 @@ jest.unstable_mockModule('fs', () => ({
jest.unstable_mockModule('../src/util.js', () => ({
getBooleanInput: jest.fn(),
getVersionFromFileContent: jest.fn()
getVersionFromFileContent: jest.fn(),
isJdkCacheEnabled: jest.fn()
}));
jest.unstable_mockModule('../src/toolchains.js', () => ({
@@ -113,6 +114,14 @@ describe('setup action orchestration', () => {
return booleanInputs.get(name as string) ?? defaultValue;
}
);
(util.isJdkCacheEnabled as jest.Mock).mockImplementation(
(cache: string) => {
const explicit = inputs.get('cache-jdk');
return explicit
? (booleanInputs.get('cache-jdk') ?? explicit === 'true')
: Boolean(cache);
}
);
(cacheFeature.isCacheFeatureAvailable as jest.Mock).mockReturnValue(true);
(toolchainIds.validateToolchainIds as jest.Mock).mockImplementation(
() => undefined
@@ -217,6 +226,7 @@ describe('setup action orchestration', () => {
packageType: 'jdk',
checkLatest: true,
forceDownload: true,
cacheJdk: false,
setDefault: false,
verifySignature: true,
verifySignaturePublicKey: 'public-key'
@@ -457,6 +467,7 @@ describe('setup action orchestration', () => {
it('does not initialize cache modules when cache input is absent', async () => {
inputs.set('distribution', 'temurin');
multilineInputs.set('java-version', ['21']);
booleanInputs.set('cache-jdk', false);
(factory.getJavaDistribution as jest.Mock).mockReturnValue({
setupJava: jest.fn(async () => ({
version: '21.0.4+7',
@@ -468,8 +479,47 @@ describe('setup action orchestration', () => {
expect(cacheFeature.isCacheFeatureAvailable).not.toHaveBeenCalled();
expect(cache.restore).not.toHaveBeenCalled();
expect(factory.getJavaDistribution).toHaveBeenCalledWith(
'temurin',
expect.objectContaining({cacheJdk: false}),
''
);
});
it.each([
['', '', false],
['', 'true', true],
['', 'false', false],
['maven', '', true],
['maven', 'true', true],
['maven', 'false', false]
])(
'passes effective JDK caching for cache=%j and cache-jdk=%j',
async (cacheInput, cacheJdkInput, expected) => {
inputs.set('distribution', 'temurin');
inputs.set('cache', cacheInput);
inputs.set('cache-jdk', cacheJdkInput);
multilineInputs.set('java-version', ['21']);
if (cacheJdkInput) {
booleanInputs.set('cache-jdk', cacheJdkInput === 'true');
}
(factory.getJavaDistribution as jest.Mock).mockReturnValue({
setupJava: jest.fn(async () => ({
version: '21.0.4+7',
path: '/opt/java/21'
}))
});
await run();
expect(factory.getJavaDistribution).toHaveBeenCalledWith(
'temurin',
expect.objectContaining({cacheJdk: expected}),
''
);
}
);
it('reports unsupported distributions through core.setFailed', async () => {
inputs.set('distribution', 'unsupported');
multilineInputs.set('java-version', ['21']);
+33 -1
View File
@@ -49,7 +49,8 @@ const {
isGhes,
validatePaginationUrl,
getLatestMajorVersion,
getBooleanInput
getBooleanInput,
isJdkCacheEnabled
} = await import('../src/util.js');
describe('getBooleanInput', () => {
@@ -115,6 +116,37 @@ describe('getBooleanInput', () => {
});
});
describe('isJdkCacheEnabled', () => {
let inputs: Record<string, string>;
beforeEach(() => {
inputs = {};
(core.getInput as jest.Mock).mockImplementation(
(name: string) => inputs[name] ?? ''
);
});
afterEach(() => {
jest.resetAllMocks();
});
it.each([
['', '', false],
['', 'true', true],
['', 'false', false],
['maven', '', true],
['maven', 'true', true],
['maven', 'false', false]
])(
'resolves cache=%j and cache-jdk=%j to %s',
(cache, cacheJdk, expected) => {
inputs['cache-jdk'] = cacheJdk;
expect(isJdkCacheEnabled(cache)).toBe(expected);
}
);
});
describe('isVersionSatisfies', () => {
it.each([
['x', '11.0.0', true],
+4 -1
View File
@@ -84,6 +84,9 @@ inputs:
cache:
description: 'Name of the build platform to cache dependencies. It can be "maven", "gradle" or "sbt".'
required: false
cache-jdk:
description: 'Cache downloaded JDK installations between jobs. Defaults to enabled when dependency caching is configured with `cache`; set explicitly to "true" or "false" to override.'
required: false
cache-dependency-path:
description: 'The path to a dependency file: pom.xml, build.gradle, build.sbt, etc. This option can be used with the `cache` option. If this option is omitted, the action searches for the dependency file in the entire repository. This option supports wildcards and a list of file names for caching multiple dependencies.'
required: false
@@ -91,7 +94,7 @@ inputs:
description: 'The path to cache instead of the default dependency cache path for the selected package manager. This option can be used with the `cache` option and supports a list of paths and exclusion patterns.'
required: false
cache-read-only:
description: 'Restore dependency caches without saving cache changes in the post action.'
description: 'Restore caches without saving cache changes in the post action.'
required: false
default: false
job-status:
+224
View File
@@ -0,0 +1,224 @@
export const id = 314;
export const ids = [314];
export const modules = {
/***/ 2314:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// EXPORTS
__webpack_require__.d(__webpack_exports__, {
saveJdkCaches: () => (/* binding */ saveJdkCaches)
});
// UNUSED EXPORTS: buildJdkCacheKey, getJdkVerificationIdentity, registerJdk, restoreJdk
// EXTERNAL MODULE: external "crypto"
var external_crypto_ = __webpack_require__(6982);
// EXTERNAL MODULE: external "fs"
var external_fs_ = __webpack_require__(9896);
var external_fs_default = /*#__PURE__*/__webpack_require__.n(external_fs_);
// EXTERNAL MODULE: external "path"
var external_path_ = __webpack_require__(6928);
var external_path_default = /*#__PURE__*/__webpack_require__.n(external_path_);
// EXTERNAL MODULE: ./node_modules/@actions/cache/lib/cache.js + 291 modules
var lib_cache = __webpack_require__(5767);
// EXTERNAL MODULE: ./node_modules/@actions/core/lib/core.js + 7 modules
var lib_core = __webpack_require__(3838);
// EXTERNAL MODULE: ./src/util.ts
var util = __webpack_require__(4527);
;// CONCATENATED MODULE: ./src/cache-feature.ts
function cache_feature_isCacheFeatureAvailable() {
if (cache.isFeatureAvailable()) {
return true;
}
if (isGhes()) {
core.warning('Caching is only supported on GHES version >= 3.5. If you are on a version >= 3.5, please check with your GHES admin if the Actions cache service is enabled or not.');
return false;
}
core.warning('The runner was not able to contact the cache service. Caching will be skipped');
return false;
}
;// CONCATENATED MODULE: ./src/jdk-cache.ts
const STATE_JDK_CACHES = 'jdk-caches';
const JDK_CACHE_KEY_VERSION = 1;
const restoredCaches = (/* unused pure expression or super */ null && ([]));
async function restoreJdk(jdk) {
if (!jdk.path || !isCacheFeatureAvailable()) {
return false;
}
const key = buildJdkCacheKey(jdk);
let matchedKey;
try {
matchedKey = await cache.restoreCache([jdk.path], key);
}
catch (error) {
core.warning(`Failed to restore JDK cache: ${error.message}`);
}
const architecturePath = path.join(jdk.path, jdk.architecture);
if (matchedKey &&
(!fs.existsSync(architecturePath) ||
!fs.existsSync(`${architecturePath}.complete`))) {
core.warning(`JDK cache key ${matchedKey} was restored without the expected tool-cache path; downloading the JDK instead.`);
matchedKey = undefined;
}
recordJdkCache({
key,
path: jdk.path,
architecture: jdk.architecture,
matchedKey
});
if (matchedKey) {
core.info(`JDK cache restored from key: ${matchedKey}`);
return true;
}
core.info(`JDK cache is not found for ${jdk.distribution} ${jdk.version}`);
return false;
}
function registerJdk(jdk) {
if (!jdk.path) {
return;
}
recordJdkCache({
key: buildJdkCacheKey(jdk),
path: jdk.path,
architecture: jdk.architecture,
installation: getInstallationIdentity(jdk.path, jdk.architecture)
});
}
/**
* Cheap fingerprint of the installation stored at a tool-cache path. The
* `<architecture>.complete` marker is (re)created by `tc.cacheDir` every time an
* installation is written, so its inode and timestamps change whenever the
* installation is replaced. This avoids rehashing a multi-hundred-megabyte JDK
* directory while still detecting that the bytes behind a key were swapped.
*/
function getInstallationIdentity(jdkPath, architecture) {
const architecturePath = external_path_default().join(jdkPath, architecture);
try {
const marker = external_fs_default().statSync(`${architecturePath}.complete`);
const installation = external_fs_default().statSync(architecturePath);
return [
marker.ino,
marker.mtimeMs,
marker.ctimeMs,
marker.size,
installation.ino,
installation.mtimeMs,
installation.ctimeMs
].join(':');
}
catch {
return undefined;
}
}
function getJdkVerificationIdentity(verifySignature, publicKey) {
if (!verifySignature) {
return 'unverified';
}
if (!publicKey) {
return 'verified:bundled';
}
const normalizedKey = publicKey.replace(/\r\n?/g, '\n').trim();
const fingerprint = createHash('sha256').update(normalizedKey).digest('hex');
return `verified:custom:sha256:${fingerprint}`;
}
async function saveJdkCaches() {
const state = lib_core/* getState */.Gu(STATE_JDK_CACHES);
if (!state) {
return;
}
const caches = parseJdkCacheState(state);
for (const jdk of caches) {
if (jdk.matchedKey === jdk.key) {
lib_core/* info */.pq(`Cache hit occurred on the JDK primary key ${jdk.key}, not saving cache.`);
continue;
}
if (!external_fs_default().existsSync(jdk.path)) {
lib_core/* debug */.Yz(`JDK cache path does not exist, not saving: ${jdk.path}`);
continue;
}
if (!jdk.installation) {
lib_core/* debug */.Yz(`No JDK installation was registered for the key ${jdk.key}, not saving cache.`);
continue;
}
if (getInstallationIdentity(jdk.path, jdk.architecture) !== jdk.installation) {
lib_core/* warning */.$e(`The JDK installation in ${jdk.path} was replaced after it was registered for the key ${jdk.key}; not saving cache.`);
continue;
}
try {
const cacheId = await lib_cache/* saveCache */.Io([jdk.path], jdk.key);
if (cacheId !== -1) {
lib_core/* info */.pq(`JDK cache saved with the key: ${jdk.key}`);
}
}
catch (error) {
const err = error;
if (err.name === lib_cache/* ReserveCacheError */.Zh.name) {
lib_core/* info */.pq(err.message);
}
else {
// Saving is best-effort and per entry: one failure must not suppress
// the remaining JDK caches.
lib_core/* warning */.$e(`Failed to save the JDK cache with the key ${jdk.key}: ${err.message}`);
}
}
}
}
function buildJdkCacheKey(jdk) {
const runnerOs = process.env['RUNNER_OS'] ?? process.platform;
const normalizedArchitecture = jdk.architecture.toLowerCase();
const identity = JSON.stringify({
keyVersion: JDK_CACHE_KEY_VERSION,
runnerOs,
distribution: jdk.distribution.toLowerCase(),
packageType: jdk.packageType.toLowerCase(),
architecture: normalizedArchitecture,
version: jdk.version,
source: jdk.source,
verification: jdk.verification
});
const digest = createHash('sha256').update(identity).digest('hex');
return `setup-java-jdk-v${JDK_CACHE_KEY_VERSION}-${runnerOs}-${normalizedArchitecture}-${digest}`;
}
function recordJdkCache(jdk) {
const existing = restoredCaches.findIndex(item => item.key === jdk.key && item.path === jdk.path);
if (existing === -1) {
restoredCaches.push(jdk);
}
else {
restoredCaches[existing] = { ...restoredCaches[existing], ...jdk };
}
core.saveState(STATE_JDK_CACHES, JSON.stringify(restoredCaches));
}
function parseJdkCacheState(state) {
const value = JSON.parse(state);
if (!Array.isArray(value) ||
!value.every(item => typeof item === 'object' &&
item !== null &&
typeof item.key === 'string' &&
typeof item.path === 'string' &&
typeof item.architecture === 'string' &&
(item.matchedKey === undefined ||
typeof item.matchedKey === 'string') &&
(item.installation === undefined ||
typeof item.installation === 'string'))) {
throw new Error('Invalid JDK cache information retrieved from state.');
}
return value;
}
/***/ })
};
+655 -579
View File
File diff suppressed because it is too large Load Diff
+61 -12
View File
@@ -16,7 +16,11 @@ export const modules = {
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__);
/* harmony import */ var _base_installer_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(6242);
/* harmony import */ var _util_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(4527);
/* harmony import */ var _constants_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(7242);
/* harmony import */ var _constants_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(7242);
/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(6982);
/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(crypto__WEBPACK_IMPORTED_MODULE_6__);
@@ -34,6 +38,9 @@ class LocalDistribution extends _base_installer_js__WEBPACK_IMPORTED_MODULE_4__/
if (this.latest) {
throw new Error("The 'latest' version alias is not supported for the 'jdkfile' distribution. Please specify a concrete version.");
}
if (this.verifySignature) {
throw new Error(`Input 'verify-signature' is not supported for distribution '${this.distribution}'.`);
}
let foundJava = this.forceDownload ? null : this.findInToolcache();
if (foundJava) {
_actions_core__WEBPACK_IMPORTED_MODULE_1__/* .info */ .pq(`Resolved Java ${foundJava.version} from tool-cache`);
@@ -48,19 +55,54 @@ class LocalDistribution extends _base_installer_js__WEBPACK_IMPORTED_MODULE_4__/
if (!stats.isFile()) {
throw new Error(`JDK file was not found in path '${jdkFilePath}'`);
}
_actions_core__WEBPACK_IMPORTED_MODULE_1__/* .info */ .pq(`Extracting Java from '${jdkFilePath}'`);
const extractedJavaPath = await (0,_util_js__WEBPACK_IMPORTED_MODULE_5__/* .extractJdkFile */ .PE)(jdkFilePath);
const archiveName = fs__WEBPACK_IMPORTED_MODULE_2___default().readdirSync(extractedJavaPath)[0];
const archivePath = path__WEBPACK_IMPORTED_MODULE_3___default().join(extractedJavaPath, archiveName);
const javaVersion = this.version;
const javaPath = await _actions_tool_cache__WEBPACK_IMPORTED_MODULE_0__/* .cacheDir */ .e8(archivePath, this.toolcacheFolderName, this.getToolcacheVersionName(javaVersion), this.architecture);
foundJava = {
version: javaVersion,
path: javaPath
};
let jdkCache;
if (this.cacheJdk) {
const [{ getJdkVerificationIdentity }, source] = await Promise.all([
Promise.all(/* import() */[__webpack_require__.e(824), __webpack_require__.e(971), __webpack_require__.e(779)]).then(__webpack_require__.bind(__webpack_require__, 5779)),
hashFile(jdkFilePath)
]);
jdkCache = {
distribution: this.distribution,
packageType: this.packageType,
architecture: this.architecture,
version: this.version,
source,
verification: getJdkVerificationIdentity(false),
path: this.getJdkCachePath(this.version)
};
}
if (!this.forceDownload && jdkCache) {
const { restoreJdk } = await Promise.all(/* import() */[__webpack_require__.e(824), __webpack_require__.e(971), __webpack_require__.e(779)]).then(__webpack_require__.bind(__webpack_require__, 5779));
const restored = await restoreJdk(jdkCache);
const restoredPath = restored
? this.getRestoredJdkPath(this.version)
: undefined;
if (restoredPath) {
foundJava = {
version: this.version,
path: restoredPath
};
}
}
if (!foundJava) {
_actions_core__WEBPACK_IMPORTED_MODULE_1__/* .info */ .pq(`Extracting Java from '${jdkFilePath}'`);
const extractedJavaPath = await (0,_util_js__WEBPACK_IMPORTED_MODULE_5__/* .extractJdkFile */ .PE)(jdkFilePath);
const archiveName = fs__WEBPACK_IMPORTED_MODULE_2___default().readdirSync(extractedJavaPath)[0];
const archivePath = path__WEBPACK_IMPORTED_MODULE_3___default().join(extractedJavaPath, archiveName);
const javaVersion = this.version;
const javaPath = await _actions_tool_cache__WEBPACK_IMPORTED_MODULE_0__/* .cacheDir */ .e8(archivePath, this.toolcacheFolderName, this.getToolcacheVersionName(javaVersion), this.architecture);
foundJava = {
version: javaVersion,
path: javaPath
};
if (jdkCache) {
const { registerJdk } = await Promise.all(/* import() */[__webpack_require__.e(824), __webpack_require__.e(971), __webpack_require__.e(779)]).then(__webpack_require__.bind(__webpack_require__, 5779));
registerJdk(jdkCache);
}
}
}
// JDK folder may contain postfix "Contents/Home" on macOS
const macOSPostfixPath = path__WEBPACK_IMPORTED_MODULE_3___default().join(foundJava.path, _constants_js__WEBPACK_IMPORTED_MODULE_6__/* .MACOS_JAVA_CONTENT_POSTFIX */ .PG);
const macOSPostfixPath = path__WEBPACK_IMPORTED_MODULE_3___default().join(foundJava.path, _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .MACOS_JAVA_CONTENT_POSTFIX */ .PG);
if (process.platform === 'darwin' && fs__WEBPACK_IMPORTED_MODULE_2___default().existsSync(macOSPostfixPath)) {
foundJava.path = macOSPostfixPath;
}
@@ -83,6 +125,13 @@ class LocalDistribution extends _base_installer_js__WEBPACK_IMPORTED_MODULE_4__/
throw new Error('This method should not be implemented in local file provider');
}
}
async function hashFile(file) {
const hash = (0,crypto__WEBPACK_IMPORTED_MODULE_6__.createHash)('sha256');
for await (const chunk of (0,fs__WEBPACK_IMPORTED_MODULE_2__.createReadStream)(file)) {
hash.update(chunk);
}
return hash.digest('hex');
}
/***/ })
+70 -3
View File
@@ -217,6 +217,7 @@ class JavaBase {
latest;
checkLatest;
forceDownload;
cacheJdk;
setDefault;
verifySignature;
verifySignaturePublicKey;
@@ -232,6 +233,7 @@ class JavaBase {
this.packageType = installerOptions.packageType;
this.checkLatest = installerOptions.checkLatest;
this.forceDownload = installerOptions.forceDownload ?? false;
this.cacheJdk = installerOptions.cacheJdk ?? false;
this.setDefault =
installerOptions.setDefault !== undefined
? installerOptions.setDefault
@@ -326,9 +328,44 @@ class JavaBase {
core/* info */.pq(`Resolved Java ${foundJava.version} from tool-cache`);
}
else {
core/* info */.pq('Trying to download...');
foundJava = await this.downloadTool(javaRelease);
core/* info */.pq(`Java ${foundJava.version} was downloaded`);
let jdkCache;
if (this.cacheJdk) {
const { getJdkVerificationIdentity } = await Promise.all(/* import() */[__webpack_require__.e(824), __webpack_require__.e(971), __webpack_require__.e(779)]).then(__webpack_require__.bind(__webpack_require__, 5779));
jdkCache = {
distribution: this.distribution,
packageType: this.packageType,
architecture: this.architecture,
version: javaRelease.version,
source: this.getJdkReleaseIdentity(javaRelease),
verification: getJdkVerificationIdentity(this.verifySignature, this.verifySignaturePublicKey),
path: this.getJdkCachePath(javaRelease.version)
};
}
if (!this.forceDownload && jdkCache) {
const { restoreJdk } = await Promise.all(/* import() */[__webpack_require__.e(824), __webpack_require__.e(971), __webpack_require__.e(779)]).then(__webpack_require__.bind(__webpack_require__, 5779));
const restored = await restoreJdk(jdkCache);
if (restored) {
const restoredPath = this.getRestoredJdkPath(javaRelease.version);
if (restoredPath) {
foundJava = {
version: javaRelease.version,
path: restoredPath
};
}
}
}
if (!foundJava || foundJava.version !== javaRelease.version) {
core/* info */.pq('Trying to download...');
foundJava = await this.downloadTool(javaRelease);
core/* info */.pq(`Java ${foundJava.version} was downloaded`);
if (jdkCache) {
// Register after the installation exists so its identity is
// captured; the post-job save refuses to upload a path whose
// installation was replaced afterwards.
const { registerJdk } = await Promise.all(/* import() */[__webpack_require__.e(824), __webpack_require__.e(971), __webpack_require__.e(779)]).then(__webpack_require__.bind(__webpack_require__, 5779));
registerJdk(jdkCache);
}
}
}
}
catch (error) {
@@ -435,6 +472,36 @@ class JavaBase {
// related issue: https://github.com/actions/virtual-environments/issues/3014
return version.replace('+', '-');
}
getJdkCachePath(version) {
const toolCache = process.env['RUNNER_TOOL_CACHE'];
if (!toolCache) {
return '';
}
return external_path_default().join(toolCache, this.toolcacheFolderName, this.getToolcacheVersionName(version));
}
getRestoredJdkPath(version) {
const basePath = this.getJdkCachePath(version);
if (!basePath) {
return null;
}
const architecturePath = external_path_default().join(basePath, this.architecture);
return external_fs_.existsSync(architecturePath) &&
external_fs_.existsSync(`${architecturePath}.complete`)
? architecturePath
: null;
}
getJdkReleaseIdentity(javaRelease) {
if (javaRelease.checksum) {
return `${javaRelease.checksum.algorithm}:${javaRelease.checksum.value}`;
}
try {
const url = new URL(javaRelease.url);
return `${url.origin}${url.pathname}`;
}
catch {
return javaRelease.url;
}
}
findInToolcache() {
// we can't use tc.find directly because firstly, we need to filter versions by stability flag
// if *-ea is provided, take only ea versions from toolcache, otherwise - only stable versions
+229
View File
@@ -0,0 +1,229 @@
export const id = 779;
export const ids = [779,394];
export const modules = {
/***/ 1394:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ isCacheFeatureAvailable: () => (/* binding */ isCacheFeatureAvailable)
/* harmony export */ });
/* harmony import */ var _actions_cache__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6971);
/* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3838);
/* harmony import */ var _util_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4527);
function isCacheFeatureAvailable() {
if (_actions_cache__WEBPACK_IMPORTED_MODULE_0__/* .isFeatureAvailable */ .w3()) {
return true;
}
if ((0,_util_js__WEBPACK_IMPORTED_MODULE_2__/* .isGhes */ .aT)()) {
_actions_core__WEBPACK_IMPORTED_MODULE_1__/* .warning */ .$e('Caching is only supported on GHES version >= 3.5. If you are on a version >= 3.5, please check with your GHES admin if the Actions cache service is enabled or not.');
return false;
}
_actions_core__WEBPACK_IMPORTED_MODULE_1__/* .warning */ .$e('The runner was not able to contact the cache service. Caching will be skipped');
return false;
}
/***/ }),
/***/ 5779:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ buildJdkCacheKey: () => (/* binding */ buildJdkCacheKey),
/* harmony export */ getJdkVerificationIdentity: () => (/* binding */ getJdkVerificationIdentity),
/* harmony export */ registerJdk: () => (/* binding */ registerJdk),
/* harmony export */ restoreJdk: () => (/* binding */ restoreJdk),
/* harmony export */ saveJdkCaches: () => (/* binding */ saveJdkCaches)
/* harmony export */ });
/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6982);
/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(crypto__WEBPACK_IMPORTED_MODULE_0__);
/* 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 _actions_cache__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(6971);
/* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(3838);
/* harmony import */ var _cache_feature_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(1394);
const STATE_JDK_CACHES = 'jdk-caches';
const JDK_CACHE_KEY_VERSION = 1;
const restoredCaches = [];
async function restoreJdk(jdk) {
if (!jdk.path || !(0,_cache_feature_js__WEBPACK_IMPORTED_MODULE_5__.isCacheFeatureAvailable)()) {
return false;
}
const key = buildJdkCacheKey(jdk);
let matchedKey;
try {
matchedKey = await _actions_cache__WEBPACK_IMPORTED_MODULE_3__/* .restoreCache */ .P3([jdk.path], key);
}
catch (error) {
_actions_core__WEBPACK_IMPORTED_MODULE_4__/* .warning */ .$e(`Failed to restore JDK cache: ${error.message}`);
}
const architecturePath = path__WEBPACK_IMPORTED_MODULE_2___default().join(jdk.path, jdk.architecture);
if (matchedKey &&
(!fs__WEBPACK_IMPORTED_MODULE_1___default().existsSync(architecturePath) ||
!fs__WEBPACK_IMPORTED_MODULE_1___default().existsSync(`${architecturePath}.complete`))) {
_actions_core__WEBPACK_IMPORTED_MODULE_4__/* .warning */ .$e(`JDK cache key ${matchedKey} was restored without the expected tool-cache path; downloading the JDK instead.`);
matchedKey = undefined;
}
recordJdkCache({
key,
path: jdk.path,
architecture: jdk.architecture,
matchedKey
});
if (matchedKey) {
_actions_core__WEBPACK_IMPORTED_MODULE_4__/* .info */ .pq(`JDK cache restored from key: ${matchedKey}`);
return true;
}
_actions_core__WEBPACK_IMPORTED_MODULE_4__/* .info */ .pq(`JDK cache is not found for ${jdk.distribution} ${jdk.version}`);
return false;
}
function registerJdk(jdk) {
if (!jdk.path) {
return;
}
recordJdkCache({
key: buildJdkCacheKey(jdk),
path: jdk.path,
architecture: jdk.architecture,
installation: getInstallationIdentity(jdk.path, jdk.architecture)
});
}
/**
* Cheap fingerprint of the installation stored at a tool-cache path. The
* `<architecture>.complete` marker is (re)created by `tc.cacheDir` every time an
* installation is written, so its inode and timestamps change whenever the
* installation is replaced. This avoids rehashing a multi-hundred-megabyte JDK
* directory while still detecting that the bytes behind a key were swapped.
*/
function getInstallationIdentity(jdkPath, architecture) {
const architecturePath = path__WEBPACK_IMPORTED_MODULE_2___default().join(jdkPath, architecture);
try {
const marker = fs__WEBPACK_IMPORTED_MODULE_1___default().statSync(`${architecturePath}.complete`);
const installation = fs__WEBPACK_IMPORTED_MODULE_1___default().statSync(architecturePath);
return [
marker.ino,
marker.mtimeMs,
marker.ctimeMs,
marker.size,
installation.ino,
installation.mtimeMs,
installation.ctimeMs
].join(':');
}
catch {
return undefined;
}
}
function getJdkVerificationIdentity(verifySignature, publicKey) {
if (!verifySignature) {
return 'unverified';
}
if (!publicKey) {
return 'verified:bundled';
}
const normalizedKey = publicKey.replace(/\r\n?/g, '\n').trim();
const fingerprint = (0,crypto__WEBPACK_IMPORTED_MODULE_0__.createHash)('sha256').update(normalizedKey).digest('hex');
return `verified:custom:sha256:${fingerprint}`;
}
async function saveJdkCaches() {
const state = _actions_core__WEBPACK_IMPORTED_MODULE_4__/* .getState */ .Gu(STATE_JDK_CACHES);
if (!state) {
return;
}
const caches = parseJdkCacheState(state);
for (const jdk of caches) {
if (jdk.matchedKey === jdk.key) {
_actions_core__WEBPACK_IMPORTED_MODULE_4__/* .info */ .pq(`Cache hit occurred on the JDK primary key ${jdk.key}, not saving cache.`);
continue;
}
if (!fs__WEBPACK_IMPORTED_MODULE_1___default().existsSync(jdk.path)) {
_actions_core__WEBPACK_IMPORTED_MODULE_4__/* .debug */ .Yz(`JDK cache path does not exist, not saving: ${jdk.path}`);
continue;
}
if (!jdk.installation) {
_actions_core__WEBPACK_IMPORTED_MODULE_4__/* .debug */ .Yz(`No JDK installation was registered for the key ${jdk.key}, not saving cache.`);
continue;
}
if (getInstallationIdentity(jdk.path, jdk.architecture) !== jdk.installation) {
_actions_core__WEBPACK_IMPORTED_MODULE_4__/* .warning */ .$e(`The JDK installation in ${jdk.path} was replaced after it was registered for the key ${jdk.key}; not saving cache.`);
continue;
}
try {
const cacheId = await _actions_cache__WEBPACK_IMPORTED_MODULE_3__/* .saveCache */ .Io([jdk.path], jdk.key);
if (cacheId !== -1) {
_actions_core__WEBPACK_IMPORTED_MODULE_4__/* .info */ .pq(`JDK cache saved with the key: ${jdk.key}`);
}
}
catch (error) {
const err = error;
if (err.name === _actions_cache__WEBPACK_IMPORTED_MODULE_3__/* .ReserveCacheError */ .Zh.name) {
_actions_core__WEBPACK_IMPORTED_MODULE_4__/* .info */ .pq(err.message);
}
else {
// Saving is best-effort and per entry: one failure must not suppress
// the remaining JDK caches.
_actions_core__WEBPACK_IMPORTED_MODULE_4__/* .warning */ .$e(`Failed to save the JDK cache with the key ${jdk.key}: ${err.message}`);
}
}
}
}
function buildJdkCacheKey(jdk) {
const runnerOs = process.env['RUNNER_OS'] ?? process.platform;
const normalizedArchitecture = jdk.architecture.toLowerCase();
const identity = JSON.stringify({
keyVersion: JDK_CACHE_KEY_VERSION,
runnerOs,
distribution: jdk.distribution.toLowerCase(),
packageType: jdk.packageType.toLowerCase(),
architecture: normalizedArchitecture,
version: jdk.version,
source: jdk.source,
verification: jdk.verification
});
const digest = (0,crypto__WEBPACK_IMPORTED_MODULE_0__.createHash)('sha256').update(identity).digest('hex');
return `setup-java-jdk-v${JDK_CACHE_KEY_VERSION}-${runnerOs}-${normalizedArchitecture}-${digest}`;
}
function recordJdkCache(jdk) {
const existing = restoredCaches.findIndex(item => item.key === jdk.key && item.path === jdk.path);
if (existing === -1) {
restoredCaches.push(jdk);
}
else {
restoredCaches[existing] = { ...restoredCaches[existing], ...jdk };
}
_actions_core__WEBPACK_IMPORTED_MODULE_4__/* .saveState */ .LZ(STATE_JDK_CACHES, JSON.stringify(restoredCaches));
}
function parseJdkCacheState(state) {
const value = JSON.parse(state);
if (!Array.isArray(value) ||
!value.every(item => typeof item === 'object' &&
item !== null &&
typeof item.key === 'string' &&
typeof item.path === 'string' &&
typeof item.architecture === 'string' &&
(item.matchedKey === undefined ||
typeof item.matchedKey === 'string') &&
(item.installation === undefined ||
typeof item.installation === 'string'))) {
throw new Error('Invalid JDK cache information retrieved from state.');
}
return value;
}
/***/ })
};
+180 -181
View File
File diff suppressed because it is too large Load Diff
+15 -2
View File
@@ -30770,6 +30770,7 @@ module.exports = {
/* harmony export */ __nccwpck_require__.d(__webpack_exports__, {
/* harmony export */ At: () => (/* binding */ INPUT_CACHE_DEPENDENCY_PATH),
/* harmony export */ E8: () => (/* binding */ INPUT_SET_DEFAULT),
/* harmony export */ GL: () => (/* binding */ INPUT_CACHE_JDK),
/* harmony export */ I9: () => (/* binding */ INPUT_FORCE_DOWNLOAD),
/* harmony export */ K$: () => (/* binding */ GPG_PASSPHRASE_PROFILE_ID),
/* harmony export */ LS: () => (/* binding */ INPUT_ARCHITECTURE),
@@ -30849,6 +30850,7 @@ const MAVEN_GPG_PASSPHRASE_DEFAULT_ENV = 'MAVEN_GPG_PASSPHRASE';
// Id of the settings.xml profile used to set `gpg.passphraseEnvName`.
const GPG_PASSPHRASE_PROFILE_ID = 'setup-java-gpg';
const INPUT_CACHE = 'cache';
const INPUT_CACHE_JDK = 'cache-jdk';
const INPUT_CACHE_DEPENDENCY_PATH = 'cache-dependency-path';
const INPUT_CACHE_PATH = 'cache-path';
const INPUT_CACHE_READ_ONLY = 'cache-read-only';
@@ -31243,6 +31245,7 @@ function validateToolchainIds(versions, versionFile, toolchainIds) {
/* harmony export */ ZY: () => (/* binding */ convertVersionToSemver),
/* harmony export */ aT: () => (/* binding */ isGhes),
/* harmony export */ ag: () => (/* binding */ getDownloadArchiveExtension),
/* harmony export */ lN: () => (/* binding */ isJdkCacheEnabled),
/* harmony export */ n2: () => (/* binding */ renameWinArchive),
/* harmony export */ rC: () => (/* binding */ getNextPageUrlFromLinkHeader),
/* harmony export */ ri: () => (/* binding */ getLatestMajorVersion),
@@ -31286,6 +31289,11 @@ function getBooleanInput(inputName, defaultValue = false) {
}
throw new Error(`Invalid value '${inputValue}' for boolean input '${inputName}'. Expected 'true' or 'false'.`);
}
function isJdkCacheEnabled(cache) {
return _actions_core__WEBPACK_IMPORTED_MODULE_4__/* .getInput */ .V4(_constants_js__WEBPACK_IMPORTED_MODULE_6__/* .INPUT_CACHE_JDK */ .GL).trim()
? getBooleanInput(_constants_js__WEBPACK_IMPORTED_MODULE_6__/* .INPUT_CACHE_JDK */ .GL)
: Boolean(cache.trim());
}
function getVersionFromToolcachePath(toolPath) {
if (toolPath) {
return path.basename(path.dirname(toolPath));
@@ -31890,6 +31898,7 @@ __nccwpck_require__.d(__webpack_exports__, {
dN: () => (/* binding */ exportVariable),
V4: () => (/* binding */ getInput),
q3: () => (/* binding */ getMultilineInput),
Gu: () => (/* binding */ getState),
pq: () => (/* binding */ info),
_o: () => (/* binding */ isDebug),
LZ: () => (/* binding */ saveState),
@@ -31900,7 +31909,7 @@ __nccwpck_require__.d(__webpack_exports__, {
$e: () => (/* binding */ warning)
});
// UNUSED EXPORTS: ExitCode, getBooleanInput, getIDToken, getState, group, markdownSummary, notice, platform, setCommandEcho, summary, toPlatformPath, toPosixPath, toWin32Path
// UNUSED EXPORTS: ExitCode, getBooleanInput, getIDToken, group, markdownSummary, notice, platform, setCommandEcho, summary, toPlatformPath, toPosixPath, toWin32Path
// EXTERNAL MODULE: external "os"
var external_os_ = __nccwpck_require__(857);
@@ -36119,6 +36128,7 @@ async function run() {
const packageType = setup_java_core/* getInput */.V4(constants/* INPUT_JAVA_PACKAGE */.p1);
const jdkFile = getJdkFileInput();
const cache = setup_java_core/* getInput */.V4(constants/* INPUT_CACHE */.gk);
const cacheJdk = (0,util/* isJdkCacheEnabled */.lN)(cache);
const cacheDependencyPath = setup_java_core/* getInput */.V4(constants/* INPUT_CACHE_DEPENDENCY_PATH */.At);
const cachePath = setup_java_core/* getMultilineInput */.q3(constants/* INPUT_CACHE_PATH */.uW);
const checkLatest = (0,util/* getBooleanInput */.Vt)(constants/* INPUT_CHECK_LATEST */.YM, false);
@@ -36157,6 +36167,7 @@ async function run() {
packageType,
checkLatest,
forceDownload,
cacheJdk,
setDefault,
verifySignature,
verifySignaturePublicKey,
@@ -36179,6 +36190,7 @@ async function run() {
packageType,
checkLatest,
forceDownload,
cacheJdk,
setDefault,
verifySignature,
verifySignaturePublicKey,
@@ -36231,12 +36243,13 @@ function getJdkFileInput() {
return jdkFile || deprecatedJdkFile;
}
async function installVersion(version, options, toolchainId = 0) {
const { distributionName, jdkFile, architecture, packageType, checkLatest, forceDownload, setDefault, verifySignature, verifySignaturePublicKey, toolchainIds } = options;
const { distributionName, jdkFile, architecture, packageType, checkLatest, forceDownload, cacheJdk, setDefault, verifySignature, verifySignaturePublicKey, toolchainIds } = options;
const installerOptions = {
architecture,
packageType,
checkLatest,
forceDownload,
cacheJdk,
setDefault,
verifySignature,
verifySignaturePublicKey,
+88
View File
@@ -17,6 +17,7 @@
- [Package compatibility](#Package-compatibility)
- [JavaFX Maven project](#JavaFX-Maven-project)
- [Ensuring the Maven cache is complete (plugin dependencies)](#ensuring-the-maven-cache-is-complete-plugin-dependencies)
- [Caching JDK installations](#caching-jdk-installations)
- [Installing custom Java architecture](#Installing-custom-Java-architecture)
- [Installing JDK without setting as default](#Installing-JDK-without-setting-as-default)
- [Installing custom Java distribution from local file](#Installing-Java-from-local-file)
@@ -468,6 +469,93 @@ jobs:
> which provides purpose-built caching (see the
> [setup-gradle documentation](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md)).
## Caching JDK installations
`cache-jdk` controls caching for downloaded JDK installations. The JDK cache is
stored and restored as its own cache entry, separate from the dependency and
build-tool wrapper caches selected by `cache`. Whether it is *enabled*, however,
is coupled to `cache`: setting `cache` turns JDK caching on as well, unless
`cache-jdk` is set explicitly.
| `cache` | `cache-jdk` | Dependency and wrapper caches | JDK cache |
| --- | --- | --- | --- |
| Omitted | Omitted | Disabled | Disabled |
| Omitted | `true` | Disabled | Enabled |
| Omitted | `false` | Disabled | Disabled |
| Set | Omitted | Enabled | Enabled |
| Set | `true` | Enabled | Enabled |
| Set | `false` | Enabled | Disabled |
JDK entries are specific to the runner operating system and normalized
architecture. They are additionally separated by distribution, package type,
exact resolved Java version, release identity, and signature-verification
identity. The release identity is the authoritative checksum when available and
otherwise the download URL without its query string. These dimensions prevent
incompatible JDKs from sharing an entry. They also mean that a matrix or workflow
using multiple JDK versions, distributions, package types, architectures, or
operating systems stores a separate JDK entry for each identity and consumes
cache storage for each one.
For `distribution: jdkfile`, the release source is a SHA-256 hash of the local
`jdk-file` contents, streamed so the archive is not held in memory. Changing the
archive therefore creates a different JDK cache entry, even when its path and
requested version are unchanged. The archive is only read when the runner tool
cache holds no installation satisfying the requested version: a matching
tool-cache installation short-circuits setup, so a changed `jdk-file` is not
re-extracted for a version that is already installed. Use
`force-download: true` when the archive contents change but the version does not.
The verification identity separates unverified downloads from packages verified
with the distribution's bundled signing key and from packages verified with each
custom key. Custom public keys are represented by a SHA-256 fingerprint of
normalized key material; the key itself is not placed in the cache key, the logs,
or action state. A verified exact-key hit reuses content that was
signature-verified when it was downloaded by the run that saved the entry,
instead of downloading and verifying it again.
> [!IMPORTANT]
> The JDK cache **key** is what isolates verification modes and release
> identity: a JDK cache entry created by an unverified download can never be
> restored for a request that sets `verify-signature: true`, and vice versa.
> `cache-jdk` does not change how the runner tool cache is used. setup-java
> first looks for an installation in the runner tool cache — a preinstalled
> JDK, or one installed by an earlier step of the same job — and uses it as-is. Such an installation is not downloaded again, and its checksum
> and signature are not reverified, even when `verify-signature: true` is set,
> because its verification history is not recorded in the tool cache. Use
> `force-download: true` for a request that must download and verify the archive
> itself.
`check-latest: true` and `java-version: latest` resolve remote metadata before
looking up the exact resolved JDK entry. `force-download: true` bypasses both the
runner tool cache and JDK cache restore, but an enabled JDK cache still records
the downloaded installation for a post-job save. `cache-read-only: true` allows
restores but suppresses post-job saves for JDK, dependency, and wrapper caches.
If the cache service fails to restore an entry, or the restored entry lacks the
expected completed tool-cache path, setup continues by downloading the JDK.
Post-job saves are best-effort and do not fail the job: cache keys are immutable,
so an existing key or a concurrent job winning the save race is left unchanged,
and a failure to save one JDK entry is reported as a warning without preventing
the remaining entries from being saved.
A key is only ever populated with the installation it was computed for. Because
tool-cache paths are shared per version and architecture, a later step — for
example one using `force-download: true` — can replace the installation an
earlier step registered. setup-java detects that replacement in the post-job
step and skips the save with a warning, so a key is never saved with content
other than the installation it identifies. This guarantee holds without
rehashing hundreds of megabytes of JDK content on every job.
JDK caching trades cache storage and cold-run save work for faster warm setup.
In a five-run Ubuntu benchmark using Microsoft OpenJDK 17.0.19, the median warm
`setup-java` time fell from 7 seconds to 3 seconds and median warm job time fell
from 24 seconds to 18 seconds. The JDK entry added 175.3 MiB for that single
identity. Results vary by runner, distribution, JDK size, network, and cache
eviction pressure; short jobs may improve latency without changing billed
minutes. The benchmark harness and methodology, along with results from later
runs, live in
[actions/setup-java-benchmarks](https://github.com/actions/setup-java-benchmarks).
## Platform and architecture compatibility
The `architecture` input is normalized before setup-java checks the tool cache
+1 -1
View File
@@ -18,7 +18,7 @@
"fix": "npm run format && npm run lint:fix && npm run build",
"prepare": "husky install",
"prerelease": "npm run-script build",
"release": "git add -f dist/setup/*.js dist/setup/package.json dist/cleanup/index.js",
"release": "git add -f dist/setup/*.js dist/setup/package.json dist/cleanup/*.js",
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --runInBand --coverage"
},
"lint-staged": {
+19 -6
View File
@@ -1,7 +1,11 @@
import * as core from '@actions/core';
import * as gpg from './gpg.js';
import * as constants from './constants.js';
import {getBooleanInput, isJobStatusSuccess} from './util.js';
import {
getBooleanInput,
isJdkCacheEnabled,
isJobStatusSuccess
} from './util.js';
import {fileURLToPath} from 'url';
async function removePrivateKeyFromKeychain() {
@@ -24,10 +28,11 @@ async function removePrivateKeyFromKeychain() {
* Check given input and run a save process for the specified package manager
* @returns Promise that will be resolved when the save process finishes
*/
async function saveCache() {
async function saveCaches() {
const jobStatus = isJobStatusSuccess();
const cache = core.getInput(constants.INPUT_CACHE);
if (!jobStatus || !cache) {
const cacheJdk = isJdkCacheEnabled(cache);
if (!jobStatus || (!cache && !cacheJdk)) {
return;
}
@@ -36,8 +41,16 @@ async function saveCache() {
return;
}
const {save} = await import('./cache.js');
await save(cache);
const saves: Promise<void>[] = [];
if (cache) {
const {save} = await import('./cache.js');
saves.push(save(cache));
}
if (cacheJdk) {
const {saveJdkCaches} = await import('./jdk-cache.js');
saves.push(saveJdkCaches());
}
await Promise.all(saves);
}
/**
@@ -59,7 +72,7 @@ async function ignoreError(promise: Promise<void>) {
export async function run() {
await removePrivateKeyFromKeychain();
await ignoreError(saveCache());
await ignoreError(saveCaches());
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
+1
View File
@@ -37,6 +37,7 @@ export const MAVEN_GPG_PASSPHRASE_DEFAULT_ENV = 'MAVEN_GPG_PASSPHRASE';
export const GPG_PASSPHRASE_PROFILE_ID = 'setup-java-gpg';
export const INPUT_CACHE = 'cache';
export const INPUT_CACHE_JDK = 'cache-jdk';
export const INPUT_CACHE_DEPENDENCY_PATH = 'cache-dependency-path';
export const INPUT_CACHE_PATH = 'cache-path';
export const INPUT_CACHE_READ_ONLY = 'cache-read-only';
+81 -3
View File
@@ -21,6 +21,7 @@ import {RetryingHttpClient} from '../retrying-http-client.js';
import os from 'os';
import {expectedDigestLength, verifyChecksum} from '../checksum.js';
import {normalizeArchitecture} from './platform-types.js';
import type {JdkCache} from '../jdk-cache.js';
export abstract class JavaBase {
protected http: httpm.HttpClient;
@@ -31,6 +32,7 @@ export abstract class JavaBase {
protected latest: boolean;
protected checkLatest: boolean;
protected forceDownload: boolean;
protected cacheJdk: boolean;
protected setDefault: boolean;
protected verifySignature: boolean;
protected verifySignaturePublicKey: string | undefined;
@@ -52,6 +54,7 @@ export abstract class JavaBase {
this.packageType = installerOptions.packageType;
this.checkLatest = installerOptions.checkLatest;
this.forceDownload = installerOptions.forceDownload ?? false;
this.cacheJdk = installerOptions.cacheJdk ?? false;
this.setDefault =
installerOptions.setDefault !== undefined
? installerOptions.setDefault
@@ -180,9 +183,48 @@ export abstract class JavaBase {
if (!this.forceDownload && foundJava?.version === javaRelease.version) {
core.info(`Resolved Java ${foundJava.version} from tool-cache`);
} else {
core.info('Trying to download...');
foundJava = await this.downloadTool(javaRelease);
core.info(`Java ${foundJava.version} was downloaded`);
let jdkCache: JdkCache | undefined;
if (this.cacheJdk) {
const {getJdkVerificationIdentity} =
await import('../jdk-cache.js');
jdkCache = {
distribution: this.distribution,
packageType: this.packageType,
architecture: this.architecture,
version: javaRelease.version,
source: this.getJdkReleaseIdentity(javaRelease),
verification: getJdkVerificationIdentity(
this.verifySignature,
this.verifySignaturePublicKey
),
path: this.getJdkCachePath(javaRelease.version)
};
}
if (!this.forceDownload && jdkCache) {
const {restoreJdk} = await import('../jdk-cache.js');
const restored = await restoreJdk(jdkCache);
if (restored) {
const restoredPath = this.getRestoredJdkPath(javaRelease.version);
if (restoredPath) {
foundJava = {
version: javaRelease.version,
path: restoredPath
};
}
}
}
if (!foundJava || foundJava.version !== javaRelease.version) {
core.info('Trying to download...');
foundJava = await this.downloadTool(javaRelease);
core.info(`Java ${foundJava.version} was downloaded`);
if (jdkCache) {
// Register after the installation exists so its identity is
// captured; the post-job save refuses to upload a path whose
// installation was replaced afterwards.
const {registerJdk} = await import('../jdk-cache.js');
registerJdk(jdkCache);
}
}
}
} catch (error: any) {
this.logSetupError(error);
@@ -299,6 +341,42 @@ export abstract class JavaBase {
return version.replace('+', '-');
}
protected getJdkCachePath(version: string): string {
const toolCache = process.env['RUNNER_TOOL_CACHE'];
if (!toolCache) {
return '';
}
return path.join(
toolCache,
this.toolcacheFolderName,
this.getToolcacheVersionName(version)
);
}
protected getRestoredJdkPath(version: string): string | null {
const basePath = this.getJdkCachePath(version);
if (!basePath) {
return null;
}
const architecturePath = path.join(basePath, this.architecture);
return fs.existsSync(architecturePath) &&
fs.existsSync(`${architecturePath}.complete`)
? architecturePath
: null;
}
private getJdkReleaseIdentity(javaRelease: JavaDownloadRelease): string {
if (javaRelease.checksum) {
return `${javaRelease.checksum.algorithm}:${javaRelease.checksum.value}`;
}
try {
const url = new URL(javaRelease.url);
return `${url.origin}${url.pathname}`;
} catch {
return javaRelease.url;
}
}
protected findInToolcache(): JavaInstallerResults | null {
// we can't use tc.find directly because firstly, we need to filter versions by stability flag
// if *-ea is provided, take only ea versions from toolcache, otherwise - only stable versions
+1
View File
@@ -4,6 +4,7 @@ export interface JavaInstallerOptions {
packageType: string;
checkLatest: boolean;
forceDownload?: boolean;
cacheJdk?: boolean;
setDefault?: boolean;
verifySignature?: boolean;
verifySignaturePublicKey?: string;
+67 -15
View File
@@ -12,6 +12,9 @@ import {
} from '../base-models.js';
import {extractJdkFile} from '../../util.js';
import {MACOS_JAVA_CONTENT_POSTFIX} from '../../constants.js';
import {createReadStream} from 'fs';
import {createHash} from 'crypto';
import type {JdkCache} from '../../jdk-cache.js';
export class LocalDistribution extends JavaBase {
constructor(
@@ -27,6 +30,11 @@ export class LocalDistribution extends JavaBase {
"The 'latest' version alias is not supported for the 'jdkfile' distribution. Please specify a concrete version."
);
}
if (this.verifySignature) {
throw new Error(
`Input 'verify-signature' is not supported for distribution '${this.distribution}'.`
);
}
let foundJava = this.forceDownload ? null : this.findInToolcache();
@@ -46,24 +54,60 @@ export class LocalDistribution extends JavaBase {
throw new Error(`JDK file was not found in path '${jdkFilePath}'`);
}
core.info(`Extracting Java from '${jdkFilePath}'`);
let jdkCache: JdkCache | undefined;
if (this.cacheJdk) {
const [{getJdkVerificationIdentity}, source] = await Promise.all([
import('../../jdk-cache.js'),
hashFile(jdkFilePath)
]);
jdkCache = {
distribution: this.distribution,
packageType: this.packageType,
architecture: this.architecture,
version: this.version,
source,
verification: getJdkVerificationIdentity(false),
path: this.getJdkCachePath(this.version)
};
}
if (!this.forceDownload && jdkCache) {
const {restoreJdk} = await import('../../jdk-cache.js');
const restored = await restoreJdk(jdkCache);
const restoredPath = restored
? this.getRestoredJdkPath(this.version)
: undefined;
if (restoredPath) {
foundJava = {
version: this.version,
path: restoredPath
};
}
}
const extractedJavaPath = await extractJdkFile(jdkFilePath);
const archiveName = fs.readdirSync(extractedJavaPath)[0];
const archivePath = path.join(extractedJavaPath, archiveName);
const javaVersion = this.version;
if (!foundJava) {
core.info(`Extracting Java from '${jdkFilePath}'`);
const javaPath = await tc.cacheDir(
archivePath,
this.toolcacheFolderName,
this.getToolcacheVersionName(javaVersion),
this.architecture
);
const extractedJavaPath = await extractJdkFile(jdkFilePath);
const archiveName = fs.readdirSync(extractedJavaPath)[0];
const archivePath = path.join(extractedJavaPath, archiveName);
const javaVersion = this.version;
foundJava = {
version: javaVersion,
path: javaPath
};
const javaPath = await tc.cacheDir(
archivePath,
this.toolcacheFolderName,
this.getToolcacheVersionName(javaVersion),
this.architecture
);
foundJava = {
version: javaVersion,
path: javaPath
};
if (jdkCache) {
const {registerJdk} = await import('../../jdk-cache.js');
registerJdk(jdkCache);
}
}
}
// JDK folder may contain postfix "Contents/Home" on macOS
@@ -103,3 +147,11 @@ export class LocalDistribution extends JavaBase {
);
}
}
async function hashFile(file: string): Promise<string> {
const hash = createHash('sha256');
for await (const chunk of createReadStream(file)) {
hash.update(chunk);
}
return hash.digest('hex');
}
+239
View File
@@ -0,0 +1,239 @@
import {createHash} from 'crypto';
import fs from 'fs';
import path from 'path';
import * as cache from '@actions/cache';
import * as core from '@actions/core';
import {isCacheFeatureAvailable} from './cache-feature.js';
const STATE_JDK_CACHES = 'jdk-caches';
const JDK_CACHE_KEY_VERSION = 1;
export interface JdkCache {
distribution: string;
packageType: string;
architecture: string;
version: string;
source: string;
verification: string;
path: string;
}
interface JdkCacheState {
key: string;
path: string;
architecture: string;
matchedKey?: string;
// Cheap identity of the installation that occupied `path` when the entry was
// registered. The tool-cache path is shared per version/architecture, so a
// later step (e.g. one using `force-download`) can replace those bytes; the
// post-job save must not upload content that does not match the identity the
// key was computed for.
installation?: string;
}
const restoredCaches: JdkCacheState[] = [];
export async function restoreJdk(jdk: JdkCache): Promise<boolean> {
if (!jdk.path || !isCacheFeatureAvailable()) {
return false;
}
const key = buildJdkCacheKey(jdk);
let matchedKey: string | undefined;
try {
matchedKey = await cache.restoreCache([jdk.path], key);
} catch (error) {
core.warning(`Failed to restore JDK cache: ${(error as Error).message}`);
}
const architecturePath = path.join(jdk.path, jdk.architecture);
if (
matchedKey &&
(!fs.existsSync(architecturePath) ||
!fs.existsSync(`${architecturePath}.complete`))
) {
core.warning(
`JDK cache key ${matchedKey} was restored without the expected tool-cache path; downloading the JDK instead.`
);
matchedKey = undefined;
}
recordJdkCache({
key,
path: jdk.path,
architecture: jdk.architecture,
matchedKey
});
if (matchedKey) {
core.info(`JDK cache restored from key: ${matchedKey}`);
return true;
}
core.info(`JDK cache is not found for ${jdk.distribution} ${jdk.version}`);
return false;
}
export function registerJdk(jdk: JdkCache): void {
if (!jdk.path) {
return;
}
recordJdkCache({
key: buildJdkCacheKey(jdk),
path: jdk.path,
architecture: jdk.architecture,
installation: getInstallationIdentity(jdk.path, jdk.architecture)
});
}
/**
* Cheap fingerprint of the installation stored at a tool-cache path. The
* `<architecture>.complete` marker is (re)created by `tc.cacheDir` every time an
* installation is written, so its inode and timestamps change whenever the
* installation is replaced. This avoids rehashing a multi-hundred-megabyte JDK
* directory while still detecting that the bytes behind a key were swapped.
*/
function getInstallationIdentity(
jdkPath: string,
architecture: string
): string | undefined {
const architecturePath = path.join(jdkPath, architecture);
try {
const marker = fs.statSync(`${architecturePath}.complete`);
const installation = fs.statSync(architecturePath);
return [
marker.ino,
marker.mtimeMs,
marker.ctimeMs,
marker.size,
installation.ino,
installation.mtimeMs,
installation.ctimeMs
].join(':');
} catch {
return undefined;
}
}
export function getJdkVerificationIdentity(
verifySignature: boolean,
publicKey?: string
): string {
if (!verifySignature) {
return 'unverified';
}
if (!publicKey) {
return 'verified:bundled';
}
const normalizedKey = publicKey.replace(/\r\n?/g, '\n').trim();
const fingerprint = createHash('sha256').update(normalizedKey).digest('hex');
return `verified:custom:sha256:${fingerprint}`;
}
export async function saveJdkCaches(): Promise<void> {
const state = core.getState(STATE_JDK_CACHES);
if (!state) {
return;
}
const caches = parseJdkCacheState(state);
for (const jdk of caches) {
if (jdk.matchedKey === jdk.key) {
core.info(
`Cache hit occurred on the JDK primary key ${jdk.key}, not saving cache.`
);
continue;
}
if (!fs.existsSync(jdk.path)) {
core.debug(`JDK cache path does not exist, not saving: ${jdk.path}`);
continue;
}
if (!jdk.installation) {
core.debug(
`No JDK installation was registered for the key ${jdk.key}, not saving cache.`
);
continue;
}
if (
getInstallationIdentity(jdk.path, jdk.architecture) !== jdk.installation
) {
core.warning(
`The JDK installation in ${jdk.path} was replaced after it was registered for the key ${jdk.key}; not saving cache.`
);
continue;
}
try {
const cacheId = await cache.saveCache([jdk.path], jdk.key);
if (cacheId !== -1) {
core.info(`JDK cache saved with the key: ${jdk.key}`);
}
} catch (error) {
const err = error as Error;
if (err.name === cache.ReserveCacheError.name) {
core.info(err.message);
} else {
// Saving is best-effort and per entry: one failure must not suppress
// the remaining JDK caches.
core.warning(
`Failed to save the JDK cache with the key ${jdk.key}: ${err.message}`
);
}
}
}
}
export function buildJdkCacheKey(jdk: JdkCache): string {
const runnerOs = process.env['RUNNER_OS'] ?? process.platform;
const normalizedArchitecture = jdk.architecture.toLowerCase();
const identity = JSON.stringify({
keyVersion: JDK_CACHE_KEY_VERSION,
runnerOs,
distribution: jdk.distribution.toLowerCase(),
packageType: jdk.packageType.toLowerCase(),
architecture: normalizedArchitecture,
version: jdk.version,
source: jdk.source,
verification: jdk.verification
});
const digest = createHash('sha256').update(identity).digest('hex');
return `setup-java-jdk-v${JDK_CACHE_KEY_VERSION}-${runnerOs}-${normalizedArchitecture}-${digest}`;
}
function recordJdkCache(jdk: JdkCacheState): void {
const existing = restoredCaches.findIndex(
item => item.key === jdk.key && item.path === jdk.path
);
if (existing === -1) {
restoredCaches.push(jdk);
} else {
restoredCaches[existing] = {...restoredCaches[existing], ...jdk};
}
core.saveState(STATE_JDK_CACHES, JSON.stringify(restoredCaches));
}
function parseJdkCacheState(state: string): JdkCacheState[] {
const value: unknown = JSON.parse(state);
if (
!Array.isArray(value) ||
!value.every(
item =>
typeof item === 'object' &&
item !== null &&
typeof (item as JdkCacheState).key === 'string' &&
typeof (item as JdkCacheState).path === 'string' &&
typeof (item as JdkCacheState).architecture === 'string' &&
((item as JdkCacheState).matchedKey === undefined ||
typeof (item as JdkCacheState).matchedKey === 'string') &&
((item as JdkCacheState).installation === undefined ||
typeof (item as JdkCacheState).installation === 'string')
)
) {
throw new Error('Invalid JDK cache information retrieved from state.');
}
return value as JdkCacheState[];
}
+11 -1
View File
@@ -1,6 +1,10 @@
import fs from 'fs';
import * as core from '@actions/core';
import {getBooleanInput, getVersionFromFileContent} from './util.js';
import {
getBooleanInput,
getVersionFromFileContent,
isJdkCacheEnabled
} from './util.js';
import * as constants from './constants.js';
import * as path from 'path';
import {fileURLToPath} from 'url';
@@ -17,6 +21,7 @@ export async function run() {
const packageType = core.getInput(constants.INPUT_JAVA_PACKAGE);
const jdkFile = getJdkFileInput();
const cache = core.getInput(constants.INPUT_CACHE);
const cacheJdk = isJdkCacheEnabled(cache);
const cacheDependencyPath = core.getInput(
constants.INPUT_CACHE_DEPENDENCY_PATH
);
@@ -80,6 +85,7 @@ export async function run() {
packageType,
checkLatest,
forceDownload,
cacheJdk,
setDefault,
verifySignature,
verifySignaturePublicKey,
@@ -105,6 +111,7 @@ export async function run() {
packageType,
checkLatest,
forceDownload,
cacheJdk,
setDefault,
verifySignature,
verifySignaturePublicKey,
@@ -183,6 +190,7 @@ async function installVersion(
packageType,
checkLatest,
forceDownload,
cacheJdk,
setDefault,
verifySignature,
verifySignaturePublicKey,
@@ -194,6 +202,7 @@ async function installVersion(
packageType,
checkLatest,
forceDownload,
cacheJdk,
setDefault,
verifySignature,
verifySignaturePublicKey,
@@ -238,6 +247,7 @@ interface installerInputsOptions {
packageType: string;
checkLatest: boolean;
forceDownload: boolean;
cacheJdk: boolean;
setDefault: boolean;
verifySignature: boolean;
verifySignaturePublicKey: string | undefined;
+8 -1
View File
@@ -8,7 +8,8 @@ import * as tc from '@actions/tool-cache';
import * as httpm from '@actions/http-client';
import {
INPUT_JOB_STATUS,
DISTRIBUTIONS_ONLY_MAJOR_VERSION
DISTRIBUTIONS_ONLY_MAJOR_VERSION,
INPUT_CACHE_JDK
} from './constants.js';
import {OutgoingHttpHeaders} from 'http';
@@ -37,6 +38,12 @@ export function getBooleanInput(inputName: string, defaultValue = false) {
);
}
export function isJdkCacheEnabled(cache: string): boolean {
return core.getInput(INPUT_CACHE_JDK).trim()
? getBooleanInput(INPUT_CACHE_JDK)
: Boolean(cache.trim());
}
export function getVersionFromToolcachePath(toolPath: string) {
if (toolPath) {
return path.basename(path.dirname(toolPath));