Compare commits

..

3 Commits

Author SHA1 Message Date
Bruno Borges
86ce8e6cc8 docs: clarify Maven cache paths and key hash inputs (#1096)
Address post-merge review feedback on #1094: the intro paragraph
described the cache too narrowly. setup-java caches both
~/.m2/repository and ~/.m2/wrapper/dists, and the default key hashes
**/pom.xml plus .mvn/wrapper/maven-wrapper.properties and
.mvn/extensions.xml, so changing wrapper/extensions files also
invalidates the cache.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-09 17:14:05 -04:00
Bruno Borges
90be2728bc docs: document seeding the Maven cache for plugin dependencies (#1094)
* docs: document Maven cache seeding for plugin dependencies (#990)

Explain that setup-java's maven cache only stores what a run downloads and
is not re-saved on a hit, so plugin dependencies resolved lazily (e.g. by
maven-shade-plugin) can be missing from the cache. Document a dependency
resolution 'seed' step (dependency:go-offline + dependency:resolve-plugins)
in README and a fuller advanced-usage section with a goal-comparison table,
single-job and separate-seed-job examples, and caveats.

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

* docs: note gradle cache has the same limitation, point to setup-gradle (#990)

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

* docs: shorten README maven cache note, link to advanced docs (#990)

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

* docs: cover the parallel multi-job cache race in the seed-job example (#705)

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

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-09 16:03:22 -04:00
Bruno Borges
3157986b3f Support multi-field Java versions like 18.0.1.1 (#1092)
Java's version scheme (JEP 322) can contain more than the three numeric
fields SemVer allows, e.g. 18.0.1.1 or 11.0.9.1. normalizeVersion()
rejected these inputs. Convert exact multi-field versions to SemVer build
notation (18.0.1.1 -> 18.0.1+1) before validation, reusing the existing
convertVersionToSemver() helper. Ranges, EA tags, and inputs that already
carry build metadata are left untouched.

Fixes: #326

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-09 15:24:02 -04:00
5 changed files with 152 additions and 2 deletions

View File

@@ -104,6 +104,7 @@ steps:
The `java-version` input supports an exact version or a version range using [SemVer](https://semver.org/) notation. The values below are examples, not an exhaustive list: The `java-version` input supports an exact version or a version range using [SemVer](https://semver.org/) notation. The values below are examples, not an exhaustive list:
- major versions, such as: `8`, `11`, `16`, `17`, `21`, `25` - major versions, such as: `8`, `11`, `16`, `17`, `21`, `25`
- more specific versions: `8.0.282+8`, `8.0.232`, `11.0`, `11.0.4`, `17.0` - more specific versions: `8.0.282+8`, `8.0.232`, `11.0`, `11.0.4`, `17.0`
- multi-field Java versions (JEP 322), such as: `11.0.9.1`, `18.0.1.1`
- early access (EA) versions: `15-ea`, `15.0.0-ea` - early access (EA) versions: `15-ea`, `15.0.0-ea`
#### Supported distributions #### Supported distributions
@@ -189,6 +190,13 @@ steps:
run: mvn -B package --file pom.xml run: mvn -B package --file pom.xml
``` ```
> [!NOTE]
> Maven resolves plugin dependencies lazily, so a cache created by a "thin" goal
> (e.g. `mvn compile`) can be missing plugin dependencies that later
> `test`/`verify`/`package` jobs then re-download on every run. See
> [Ensuring the Maven cache is complete](docs/advanced-usage.md#ensuring-the-maven-cache-is-complete-plugin-dependencies)
> for how to seed a complete cache.
#### Caching sbt dependencies #### Caching sbt dependencies
```yaml ```yaml
steps: steps:

View File

@@ -748,7 +748,11 @@ describe('normalizeVersion', () => {
['11.0', {version: '11.0', stable: true}], ['11.0', {version: '11.0', stable: true}],
['11.0.10', {version: '11.0.10', stable: true}], ['11.0.10', {version: '11.0.10', stable: true}],
['11-ea', {version: '11', stable: false}], ['11-ea', {version: '11', stable: false}],
['11.0.2-ea', {version: '11.0.2', stable: false}] ['11.0.2-ea', {version: '11.0.2', stable: false}],
['18.0.1.1', {version: '18.0.1+1', stable: true}],
['11.0.9.1', {version: '11.0.9+1', stable: true}],
['12.0.2.1.0', {version: '12.0.2+1.0', stable: true}],
['18.0.1.1-ea', {version: '18.0.1+1', stable: false}]
])('normalizeVersion from %s to %s', (input, expected) => { ])('normalizeVersion from %s to %s', (input, expected) => {
expect(DummyJavaBase.prototype.normalizeVersion.call(null, input)).toEqual( expect(DummyJavaBase.prototype.normalizeVersion.call(null, input)).toEqual(
expected expected

8
dist/setup/index.js vendored
View File

@@ -131258,6 +131258,14 @@ class JavaBase {
version = version.replace('-ea.', '+'); version = version.replace('-ea.', '+');
stable = false; stable = false;
} }
// Java uses a versioning scheme (JEP 322) that can contain more numeric
// fields than SemVer allows, e.g. '18.0.1.1' or '11.0.9.1'. Convert such
// exact versions to SemVer build notation ('18.0.1+1') so they are
// accepted. Ranges and versions that already carry build metadata are
// left untouched.
if (/^\d+(\.\d+){3,}$/.test(version)) {
version = convertVersionToSemver(version);
}
if (!semver_default().validRange(version)) { if (!semver_default().validRange(version)) {
throw new Error(`The string '${version}' is not valid SemVer notation for a Java version. Please check README file for code snippets and more detailed information`); throw new Error(`The string '${version}' is not valid SemVer notation for a Java version. Please check README file for code snippets and more detailed information`);
} }

View File

@@ -15,6 +15,7 @@
- [Tencent Kona](#Tencent-Kona) - [Tencent Kona](#Tencent-Kona)
- [Installing custom Java package type](#Installing-custom-Java-package-type) - [Installing custom Java package type](#Installing-custom-Java-package-type)
- [JavaFX Maven project](#JavaFX-Maven-project) - [JavaFX Maven project](#JavaFX-Maven-project)
- [Ensuring the Maven cache is complete (plugin dependencies)](#ensuring-the-maven-cache-is-complete-plugin-dependencies)
- [Installing custom Java architecture](#Installing-custom-Java-architecture) - [Installing custom Java architecture](#Installing-custom-Java-architecture)
- [Installing JDK without setting as default](#Installing-JDK-without-setting-as-default) - [Installing JDK without setting as default](#Installing-JDK-without-setting-as-default)
- [Installing custom Java distribution from local file](#Installing-Java-from-local-file) - [Installing custom Java distribution from local file](#Installing-Java-from-local-file)
@@ -285,6 +286,122 @@ To run the JavaFX application in CI:
run: mvn --no-transfer-progress javafx:run run: mvn --no-transfer-progress javafx:run
``` ```
## Ensuring the Maven cache is complete (plugin dependencies)
When you enable `cache: maven`, the action caches your local Maven repository
(`~/.m2/repository`) and downloaded Maven Wrapper distributions
(`~/.m2/wrapper/dists`). The cache key is a hash of your Maven inputs — every
`**/pom.xml`, plus `**/.mvn/wrapper/maven-wrapper.properties` and
`**/.mvn/extensions.xml` — so changing any of those files (for example bumping
the wrapper version or editing core extensions) produces a new key and
invalidates the cache. At the end of the job the action saves whatever was
downloaded during that run. It does **not** re-save the cache when the key
already matches (a cache *hit*).
Maven resolves **plugin** dependencies lazily: it only downloads the plugins and
plugin dependencies required by the goals that actually execute. As a result, the
run that first creates the cache determines what is stored. If that run executed a
"thin" goal such as `mvn compile`, plugins bound to later phases are never
resolved. For example, `maven-shade-plugin` (bound to `package`) pulls in
`plexus-archiver`, `commons-compress`, `io.airlift:aircompressor` and
`org.tukaani:xz` — none of which a `compile` run downloads. Those artifacts are
therefore absent from the cache, and because the action does not re-save on a
hit, every later `test`/`verify`/`package` job re-downloads them on every run.
### Seed the cache with a resolution step
To populate `~/.m2` as comprehensively as possible on the run that creates the
cache, run a dependency-resolution "seed" command before your build. Choose a
command based on how thorough you need it to be:
| Seed command | Resolves plugin dependencies? | Notes |
|--------------|:-----------------------------:|-------|
| `mvn dependency:resolve` | No | Resolves project dependencies only — misses plugin dependencies (e.g. `aircompressor`). |
| `mvn dependency:resolve-plugins` | Yes | Resolves plugins **and their dependencies**. |
| `mvn dependency:go-offline` | Yes | Resolves project and plugin dependencies (a superset). |
| `mvn dependency:go-offline dependency:resolve-plugins` | Yes (most thorough) | Recommended default. Use `dependency:resolve dependency:resolve-plugins` if `go-offline` is flaky or insufficient for your project. |
Single job — seed, then build (the cache saved at the end of this run contains
the full set):
```yaml
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '25'
cache: 'maven'
- name: Seed the Maven cache
run: mvn -B dependency:go-offline dependency:resolve-plugins
- name: Build with Maven
run: mvn -B verify --file pom.xml
```
Separate seed job — useful for a matrix where different legs run different goals
(`test`, `check`, `verify`, `-Pprofile1`, ...) but all share the same `~/.m2`
cache. Without a seed, whichever job finishes first creates the cache from its
own partial `.m2`, and parallel jobs race to save an equally partial cache; the
seed job instead creates one comprehensive cache that every other job reuses:
```yaml
jobs:
seed-cache:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '25'
cache: 'maven'
- name: Seed the Maven cache
run: mvn -B dependency:go-offline dependency:resolve-plugins
build:
needs: seed-cache
runs-on: ubuntu-latest
strategy:
matrix:
goal: ['test', 'verify', 'test -Pprofile1']
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '25'
cache: 'maven'
- name: Build
run: mvn -B ${{ matrix.goal }} --file pom.xml
```
### Caveats
- **The seed only helps on the run that creates the cache.** Once a cache exists
for the current `pom.xml` hash, later runs get a hit and any additional
downloads are not saved. On an existing repository whose cache is already
incomplete, invalidate it once (for example by changing `cache-dependency-path`
or deleting the repository's caches) so a complete cache is created from the
seed.
- **Static resolution is not exhaustive.** `go-offline`/`resolve-plugins` resolve
the statically declared plugin set for the *active* profiles and modules.
Profile-gated plugins, conditionally-active modules, and artifacts a plugin
fetches at execution time may still be missed. For the most complete cache,
seed with the fullest goal set your CI actually uses (for example
`mvn -B verify` with every profile enabled).
- **Multi-module projects:** run the seed at the reactor root so every module's
plugins are resolved.
> [!NOTE]
> The same "the cache stores only what the creating run downloaded, and is not
> re-saved on a hit" behavior applies to `cache: gradle`, since Gradle also
> resolves dependencies and plugin/buildscript classpaths lazily. Gradle has no
> direct equivalent of `dependency:go-offline`, so for complete and fine-grained
> dependency caching on Gradle projects we recommend
> [`gradle/actions/setup-gradle`](https://github.com/gradle/actions/tree/main/setup-gradle),
> which provides purpose-built caching (see the
> [setup-gradle documentation](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md)).
## Installing custom Java architecture ## Installing custom Java architecture
```yaml ```yaml

View File

@@ -4,7 +4,11 @@ import * as fs from 'fs';
import semver from 'semver'; import semver from 'semver';
import path from 'path'; import path from 'path';
import * as httpm from '@actions/http-client'; import * as httpm from '@actions/http-client';
import {getToolcachePath, isVersionSatisfies} from '../util.js'; import {
convertVersionToSemver,
getToolcachePath,
isVersionSatisfies
} from '../util.js';
import { import {
JavaDownloadRelease, JavaDownloadRelease,
JavaInstallerOptions, JavaInstallerOptions,
@@ -273,6 +277,15 @@ export abstract class JavaBase {
stable = false; stable = false;
} }
// Java uses a versioning scheme (JEP 322) that can contain more numeric
// fields than SemVer allows, e.g. '18.0.1.1' or '11.0.9.1'. Convert such
// exact versions to SemVer build notation ('18.0.1+1') so they are
// accepted. Ranges and versions that already carry build metadata are
// left untouched.
if (/^\d+(\.\d+){3,}$/.test(version)) {
version = convertVersionToSemver(version);
}
if (!semver.validRange(version)) { if (!semver.validRange(version)) {
throw new Error( throw new Error(
`The string '${version}' is not valid SemVer notation for a Java version. Please check README file for code snippets and more detailed information` `The string '${version}' is not valid SemVer notation for a Java version. Please check README file for code snippets and more detailed information`