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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion .github/workflows/e2e-cache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,9 @@ jobs:
distribution: 'adopt'
java-version: '11'
cache: maven
cache-dependency-path: __tests__/cache/maven2/pom.xml
cache-dependency-path: |
__tests__/cache/maven2/pom.xml
README.md
- name: Confirm that ~/.m2/repository directory has not been made
run: bash __tests__/check-dir.sh "$HOME/.m2/repository" absent
sbt1-save:
Expand Down Expand Up @@ -446,3 +448,50 @@ jobs:
- name: Confirm that ~/.cache/coursier directory has not been made
if: matrix.os == 'ubuntu-22.04'
run: bash "$GITHUB_WORKSPACE/__tests__/check-dir.sh" "$HOME/.cache/coursier" absent
custom-maven-path-save:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Run setup-java with a custom Maven cache path
uses: ./
with:
distribution: 'adopt'
java-version: '11'
cache: maven
cache-dependency-path: |
__tests__/cache/maven2/pom.xml
.github/workflows/e2e-cache.yml
cache-path: |
${{ runner.temp }}/setup-java-custom-maven-repository
!${{ runner.temp }}/setup-java-custom-maven-repository/**/*.lastUpdated
- name: Populate the custom Maven repository
run: |
mvn -Dmaven.repo.local="$RUNNER_TEMP/setup-java-custom-maven-repository" verify -f __tests__/cache/maven2/pom.xml
touch "$RUNNER_TEMP/setup-java-custom-maven-repository/setup-java-cache-path-marker"
bash __tests__/check-dir.sh "$RUNNER_TEMP/setup-java-custom-maven-repository"
custom-maven-path-restore:
runs-on: ubuntu-latest
needs: custom-maven-path-save
steps:
- name: Checkout
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Run setup-java with a custom Maven cache path
uses: ./
with:
distribution: 'adopt'
java-version: '11'
cache: maven
cache-dependency-path: |
__tests__/cache/maven2/pom.xml
.github/workflows/e2e-cache.yml
cache-path: |
${{ runner.temp }}/setup-java-custom-maven-repository
!${{ runner.temp }}/setup-java-custom-maven-repository/**/*.lastUpdated
cache-read-only: true
- name: Confirm that the custom Maven repository has been restored
run: test -f "$RUNNER_TEMP/setup-java-custom-maven-repository/setup-java-cache-path-marker"
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ For more details, see the full release notes on the [releases page](https://git

- `cache-dependency-path`: 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.

- `cache-path`: The dependency cache path to use instead of the default path for the package manager selected by `cache`. This option supports a list of paths and exclusion patterns. The build tool must be configured to use the same location.

- `cache-read-only`: Restore dependency caches without saving changes in the post action. Defaults to `false`. Use this for pull requests, merge queues, short-lived branches, and fan-out jobs that should consume caches populated by a default-branch or seed job.

#### Maven options
Expand Down Expand Up @@ -188,6 +190,34 @@ The action has a built-in functionality for caching and restoring dependencies.

When the option `cache-dependency-path` is specified, the hash is based on the matching file. This option supports wildcards and a list of file names, and is especially useful for monorepos.

Use `cache-path` to replace the selected package manager's default dependency
cache paths. Each non-empty line is passed to `actions/cache`, including
supported exclusion patterns. `setup-java` does not configure the build tool,
so the build must use the same paths:

```yaml
- uses: actions/setup-java@v6
Comment thread
brunoborges marked this conversation as resolved.
with:
distribution: 'temurin'
java-version: '25'
cache: 'maven'
cache-path: |
/custom/maven/repository
!/custom/maven/repository/**/*.lastUpdated
- run: mvn -Dmaven.repo.local=/custom/maven/repository verify
```

`cache-path` does not change the cache key. The key continues to use the runner
OS, architecture, selected package manager, and dependency-file hash described
above. Jobs intended to share a cache key must therefore use the same
`cache-path` values so that they restore and save the same filesystem
locations.

The Maven and Gradle wrapper caches remain at their documented default paths
and are managed independently of `cache-path`. For advanced keying, fallback
keys, or cache topologies that do not map to one package manager's dependency
paths, use [`actions/cache`](https://github.com/actions/cache) directly.

The workflow output `cache-hit` is set to indicate if an exact match was found for the key [as actions/cache does](https://github.com/actions/cache/tree/main#outputs).

The workflow output `cache-primary-key` exposes the primary cache key computed by the action for the configured build tool. It is useful for composing with [`actions/cache`](https://github.com/actions/cache) or [`actions/cache/restore`](https://github.com/actions/cache/tree/main/restore) in later steps or dependent jobs that need to reuse the exact same key. It is empty when caching is not enabled or when caching is skipped (for example, when the cache service is unavailable).
Expand Down
72 changes: 68 additions & 4 deletions __tests__/cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,10 @@ describe('dependency cache', () => {
join(workspace, '.mvn', 'wrapper', 'maven-wrapper.properties')
);

await restore('maven', '');
await restore('maven', '', ['/custom/maven/repository']);
// Main dependency cache no longer carries the wrapper dists path.
expect(spyCacheRestore).toHaveBeenCalledWith(
[join(os.homedir(), '.m2', 'repository')],
['/custom/maven/repository'],
expect.any(String)
);
expect(spyCacheRestore).toHaveBeenCalledWith(
Expand Down Expand Up @@ -394,10 +394,10 @@ describe('dependency cache', () => {
it('restores the gradle wrapper distribution cache independently of the main cache', async () => {
createFile(join(workspace, 'build.gradle'));

await restore('gradle', '');
await restore('gradle', '', ['/custom/gradle/caches']);
// Main dependency cache no longer carries the wrapper path.
expect(spyCacheRestore).toHaveBeenCalledWith(
[join(os.homedir(), '.gradle', 'caches')],
['/custom/gradle/caches'],
expect.any(String)
);
// Wrapper distribution is restored on its own, keyed only on the
Expand Down Expand Up @@ -576,6 +576,34 @@ describe('dependency cache', () => {
expect(spyInfo).toHaveBeenCalledWith('gradle cache is not found');
});
});
describe('cache-path', () => {
it.each([
['maven', ['/custom/maven/repository']],
['gradle', ['/custom/gradle/caches']],
[
'sbt',
[
'/custom/ivy/cache',
'/custom/coursier/cache',
'!/custom/ivy/cache/*.lock'
]
]
])(
'restores and persists custom paths for %s',
async (packageManager, cachePaths) => {
await restore(packageManager, '', cachePaths);

expect(spyCacheRestore).toHaveBeenCalledWith(
cachePaths,
expect.any(String)
);
expect(spySaveState).toHaveBeenCalledWith(
'cache-paths',
JSON.stringify(cachePaths)
);
}
);
});
});
describe('save', () => {
let spyCacheSave: any;
Expand Down Expand Up @@ -625,6 +653,42 @@ describe('dependency cache', () => {
);
});

it.each([
['maven', ['/custom/maven/repository']],
['gradle', ['/custom/gradle/caches']],
[
'sbt',
[
'/custom/ivy/cache',
'/custom/coursier/cache',
'!/custom/ivy/cache/*.lock'
]
]
])(
'saves the persisted custom paths for %s',
async (packageManager, cachePaths) => {
(core.getState as jest.Mock<any>).mockImplementation((name: any) => {
switch (name) {
case 'cache-primary-key':
return 'setup-java-cache-primary-key';
case 'cache-matched-key':
return 'setup-java-cache-matched-key';
case 'cache-paths':
return JSON.stringify(cachePaths);
default:
return '';
}
});

await save(packageManager);

expect(spyCacheSave).toHaveBeenCalledWith(
cachePaths,
'setup-java-cache-primary-key'
);
}
);

describe('for maven', () => {
it('uploads cache even if no pom.xml found', async () => {
createStateForMissingBuildFile();
Expand Down
9 changes: 8 additions & 1 deletion __tests__/setup-java.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,10 @@ describe('setup action orchestration', () => {
inputs.set('cache', 'maven');
inputs.set('cache-dependency-path', '**/pom.xml');
multilineInputs.set('java-version', ['21']);
multilineInputs.set('cache-path', [
'/custom/maven/repository',
'!/custom/maven/repository/excluded'
]);
const setupJava = jest.fn(async () => ({
version: '21.0.4+7',
path: '/opt/java/21'
Expand All @@ -312,7 +316,10 @@ describe('setup action orchestration', () => {
expect(problemMatcher.configureProblemMatcher).toHaveBeenCalledWith(
expect.stringMatching(/\.github[/\\]java\.json$/)
);
expect(cache.restore).toHaveBeenCalledWith('maven', '**/pom.xml');
expect(cache.restore).toHaveBeenCalledWith('maven', '**/pom.xml', [
'/custom/maven/repository',
'!/custom/maven/repository/excluded'
]);
expect(
(toolchains.configureToolchains as jest.Mock).mock.invocationCallOrder[0]
).toBeLessThan(
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ inputs:
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
cache-path:
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.'
required: false
Expand Down
33 changes: 27 additions & 6 deletions dist/cleanup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -97338,6 +97338,7 @@ const MAVEN_GPG_PASSPHRASE_DEFAULT_ENV = 'MAVEN_GPG_PASSPHRASE';
const GPG_PASSPHRASE_PROFILE_ID = 'setup-java-gpg';
const INPUT_CACHE = 'cache';
const INPUT_CACHE_DEPENDENCY_PATH = 'cache-dependency-path';
const INPUT_CACHE_PATH = 'cache-path';
const INPUT_CACHE_READ_ONLY = 'cache-read-only';
const INPUT_JOB_STATUS = 'job-status';
const STATE_GPG_PRIVATE_KEY_FINGERPRINT = 'gpg-private-key-fingerprint';
Expand Down Expand Up @@ -97771,6 +97772,7 @@ async function verifyPackageSignature(archivePath, signatureUrl, publicKeyConten


const STATE_CACHE_PRIMARY_KEY = 'cache-primary-key';
const STATE_CACHE_PATHS = 'cache-paths';
const CACHE_MATCHED_KEY = 'cache-matched-key';
const CACHE_KEY_PREFIX = 'setup-java';
const supportedPackageManager = [
Expand Down Expand Up @@ -97852,6 +97854,21 @@ function findPackageManager(id) {
}
return packageManager;
}
function resolveCachePaths(packageManager, cachePaths) {
return cachePaths.length > 0 ? cachePaths : packageManager.path;
}
function getCachePathsFromState(packageManager) {
const cachePathsState = getState(STATE_CACHE_PATHS);
if (!cachePathsState) {
return packageManager.path;
}
const cachePaths = JSON.parse(cachePathsState);
if (!Array.isArray(cachePaths) ||
!cachePaths.every(cachePath => typeof cachePath === 'string')) {
throw new Error('Invalid cache paths retrieved from state.');
}
return cachePaths;
}
/**
* State keys used to carry an additional cache's restore-time information over
* to the post (save) action, scoped by the additional cache name.
Expand Down Expand Up @@ -97894,30 +97911,33 @@ async function computeAdditionalCacheKey(additionalCache) {
}
/**
* Restore the dependency cache
* @param id ID of the package manager, should be "maven" or "gradle"
* @param id ID of the package manager, should be "maven", "gradle", or "sbt"
* @param cacheDependencyPath The path to a dependency file
* @param cachePaths Paths to cache instead of the package manager defaults
*/
async function restore(id, cacheDependencyPath) {
async function restore(id, cacheDependencyPath, cachePaths = []) {
const packageManager = findPackageManager(id);
const resolvedCachePaths = resolveCachePaths(packageManager, cachePaths);
const [primaryKey, preparedAdditionalCaches] = await Promise.all([
computeCacheKey(packageManager, cacheDependencyPath),
prepareAdditionalCaches(packageManager.additionalCaches ?? [])
]);
core.debug(`primary key is ${primaryKey}`);
core.saveState(STATE_CACHE_PRIMARY_KEY, primaryKey);
core.saveState(STATE_CACHE_PATHS, JSON.stringify(resolvedCachePaths));
core.setOutput(STATE_CACHE_PRIMARY_KEY, primaryKey);
for (const preparedCache of preparedAdditionalCaches) {
core.debug(`${preparedCache.cache.name} primary key is ${preparedCache.primaryKey}`);
core.saveState(additionalCachePrimaryKeyState(preparedCache.cache.name), preparedCache.primaryKey);
}
await Promise.all([
restorePrimaryCache(packageManager, primaryKey),
restorePrimaryCache(packageManager, resolvedCachePaths, primaryKey),
...preparedAdditionalCaches.map(preparedCache => restoreAdditionalCache(preparedCache))
]);
}
async function restorePrimaryCache(packageManager, primaryKey) {
async function restorePrimaryCache(packageManager, cachePaths, primaryKey) {
// No "restoreKeys" is set, to start with a clear cache after dependency update (see https://github.com/actions/setup-java/issues/269)
const matchedKey = await cache.restoreCache(packageManager.path, primaryKey);
const matchedKey = await cache.restoreCache(cachePaths, primaryKey);
if (matchedKey) {
core.saveState(CACHE_MATCHED_KEY, matchedKey);
core.setOutput('cache-hit', matchedKey === primaryKey);
Expand Down Expand Up @@ -97963,6 +97983,7 @@ async function restoreAdditionalCache(preparedCache) {
*/
async function save(id) {
const packageManager = findPackageManager(id);
const cachePaths = getCachePathsFromState(packageManager);
const matchedKey = getState(CACHE_MATCHED_KEY);
// Inputs are re-evaluated before the post action, so we want the original key used for restore
const primaryKey = getState(STATE_CACHE_PRIMARY_KEY);
Expand All @@ -97985,7 +98006,7 @@ async function save(id) {
return;
}
try {
const cacheId = await cache_saveCache(packageManager.path, primaryKey);
const cacheId = await cache_saveCache(cachePaths, primaryKey);
if (cacheId === -1) {
// saveCache returns -1 without throwing when the cache was not saved,
// e.g. a reserve collision or a read-only token (fork PR). @actions/cache
Expand Down
Loading
Loading