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
3 changes: 3 additions & 0 deletions .github/workflows/e2e-cache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ jobs:
distribution: 'adopt'
java-version: '11'
cache: gradle
cache-read-only: true
- name: Confirm that ~/.gradle/caches directory has been made
run: bash __tests__/check-dir.sh "$HOME/.gradle/caches"
maven-save:
Expand Down Expand Up @@ -105,6 +106,7 @@ jobs:
distribution: 'adopt'
java-version: '11'
cache: maven
cache-read-only: true
- name: Confirm that ~/.m2/repository directory has been made
run: bash __tests__/check-dir.sh "$HOME/.m2/repository"
sbt-save:
Expand Down Expand Up @@ -169,6 +171,7 @@ jobs:
distribution: 'adopt'
java-version: '11'
cache: sbt
cache-read-only: true

- name: Confirm that ~/Library/Caches/Coursier directory has been made
if: matrix.os == 'macos-15-intel'
Expand Down
50 changes: 50 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-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
The action has a bunch of inputs to generate maven's [settings.xml](https://maven.apache.org/settings.html) on the fly and pass the values to Apache Maven GPG Plugin as well as Apache Maven Toolchains. See [advanced usage](docs/advanced-usage.md) for more.

Expand Down Expand Up @@ -192,6 +194,54 @@ The workflow output `cache-primary-key` exposes the primary cache key computed b

The cache input is optional, and caching is turned off by default.

Set `cache-read-only: true` to restore the main dependency cache and any Maven
or Gradle wrapper cache without archiving or uploading changes after the job.
For example, a workflow can allow only the default branch to write caches while
pull requests, merge queues, and short-lived branches remain read-only:

```yaml
- uses: actions/setup-java@v6
with:
distribution: 'temurin'
java-version: '25'
cache: 'maven'
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
```

For a fan-out matrix, use one seed job to populate a complete cache and make
every matrix job a read-only consumer. The seed and consumers must use the same
runner OS and cache dependency inputs so they compute the same key:

```yaml
jobs:
seed-cache:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
distribution: 'temurin'
java-version: '25'
cache: 'maven'
- run: mvn dependency:go-offline dependency:resolve-plugins

build:
needs: seed-cache
runs-on: ubuntu-latest
strategy:
matrix:
goal: [test, verify, package]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
distribution: 'temurin'
java-version: '25'
cache: 'maven'
cache-read-only: true
- run: mvn ${{ matrix.goal }}
```

**Maven Wrapper:** when `cache: 'maven'` is enabled, the action also caches and restores the Maven Wrapper distribution downloaded to `~/.m2/wrapper/dists` (in addition to the local repository), so wrapper-based (`./mvnw`) builds don't re-download the Maven distribution. The wrapper distribution is stored in a **separate** cache entry keyed only on `**/.mvn/wrapper/maven-wrapper.properties`, so it stays cached across the frequent `pom.xml` changes that rotate the main dependency cache key.

#### Caching gradle dependencies
Expand Down
58 changes: 58 additions & 0 deletions __tests__/cleanup-java.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,49 @@ describe('cleanup', () => {
await cleanup();
expect(spyCacheSave).toHaveBeenCalled();
});

it.each(['maven', 'gradle', 'sbt'])(
'does not save the %s cache in read-only mode',
async packageManager => {
createStateForSuccessfulRestoreWithWrapper(packageManager);
(core.getInput as jest.Mock<any>).mockImplementation((name: string) => {
switch (name) {
case 'cache':
return packageManager;
case 'cache-read-only':
return 'true';
default:
return '';
}
});

await cleanup();

expect(spyCacheSave).not.toHaveBeenCalled();
expect(core.getState).not.toHaveBeenCalled();
expect(spyInfo).toHaveBeenCalledWith(
'Cache saving is skipped because cache-read-only is enabled.'
);
}
);

it('saves the cache when read-only mode is explicitly disabled', async () => {
spyCacheSave.mockResolvedValue(0);
(core.getInput as jest.Mock<any>).mockImplementation((name: string) => {
switch (name) {
case 'cache':
return 'maven';
case 'cache-read-only':
return 'false';
default:
return '';
}
});

await cleanup();

expect(spyCacheSave).toHaveBeenCalled();
});
});

function resetState() {
Expand All @@ -141,3 +184,18 @@ function createStateForSuccessfulRestore() {
}
});
}

function createStateForSuccessfulRestoreWithWrapper(packageManager: string) {
(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-primary-key-${packageManager}-wrapper`:
return `setup-java-${packageManager}-wrapper-primary-key`;
default:
return '';
}
});
}
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ 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-read-only:
description: 'Restore dependency caches without saving cache changes in the post action.'
required: false
default: false
job-status:
description: 'Workaround to pass job status to post job step. This variable is not intended for manual setting'
required: false
Expand Down
12 changes: 10 additions & 2 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_READ_ONLY = 'cache-read-only';
const INPUT_JOB_STATUS = 'job-status';
const STATE_GPG_PRIVATE_KEY_FINGERPRINT = 'gpg-private-key-fingerprint';
const M2_DIR = '.m2';
Expand Down Expand Up @@ -97365,7 +97366,7 @@ function getTempDir() {
return tempDirectory;
}
function util_getBooleanInput(inputName, defaultValue = false) {
const inputValue = core.getInput(inputName);
const inputValue = getInput(inputName);
const normalizedValue = inputValue.trim().toLowerCase();
if (!normalizedValue) {
return defaultValue;
Expand Down Expand Up @@ -98076,7 +98077,14 @@ async function removePrivateKeyFromKeychain() {
async function cleanup_java_saveCache() {
const jobStatus = isJobStatusSuccess();
const cache = getInput(INPUT_CACHE);
return jobStatus && cache ? save(cache) : Promise.resolve();
if (!jobStatus || !cache) {
return;
}
if (util_getBooleanInput(INPUT_CACHE_READ_ONLY, false)) {
info('Cache saving is skipped because cache-read-only is enabled.');
return;
}
await save(cache);
}
/**
* The save process is best-effort, and it should not make the workflow fail
Expand Down
1 change: 1 addition & 0 deletions dist/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -72120,6 +72120,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_READ_ONLY = 'cache-read-only';
const constants_INPUT_JOB_STATUS = 'job-status';
const STATE_GPG_PRIVATE_KEY_FINGERPRINT = 'gpg-private-key-fingerprint';
const M2_DIR = '.m2';
Expand Down
13 changes: 11 additions & 2 deletions src/cleanup-java.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as core from '@actions/core';
import * as gpg from './gpg.js';
import * as constants from './constants.js';
import {isJobStatusSuccess} from './util.js';
import {getBooleanInput, isJobStatusSuccess} from './util.js';
import {save} from './cache.js';
import {fileURLToPath} from 'url';

Expand All @@ -28,7 +28,16 @@ async function removePrivateKeyFromKeychain() {
async function saveCache() {
const jobStatus = isJobStatusSuccess();
const cache = core.getInput(constants.INPUT_CACHE);
return jobStatus && cache ? save(cache) : Promise.resolve();
if (!jobStatus || !cache) {
return;
}

if (getBooleanInput(constants.INPUT_CACHE_READ_ONLY, false)) {
core.info('Cache saving is skipped because cache-read-only is enabled.');
return;
}

await save(cache);
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const GPG_PASSPHRASE_PROFILE_ID = 'setup-java-gpg';

export const INPUT_CACHE = 'cache';
export const INPUT_CACHE_DEPENDENCY_PATH = 'cache-dependency-path';
export const INPUT_CACHE_READ_ONLY = 'cache-read-only';
export const INPUT_JOB_STATUS = 'job-status';

export const STATE_GPG_PRIVATE_KEY_FINGERPRINT = 'gpg-private-key-fingerprint';
Expand Down
Loading