diff --git a/README.md b/README.md index daa1b7b2..be8c8c59 100644 --- a/README.md +++ b/README.md @@ -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: - 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` +- 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` - the `latest` alias, which floats to the newest available stable (GA) release @@ -197,6 +198,13 @@ steps: 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 ```yaml steps: diff --git a/__tests__/distributors/base-installer.test.ts b/__tests__/distributors/base-installer.test.ts index b73f7d73..a3cef10c 100644 --- a/__tests__/distributors/base-installer.test.ts +++ b/__tests__/distributors/base-installer.test.ts @@ -772,6 +772,10 @@ describe('normalizeVersion', () => { ['11.0.10', {version: '11.0.10', stable: true, latest: false}], ['11-ea', {version: '11', stable: false, latest: false}], ['11.0.2-ea', {version: '11.0.2', stable: false, latest: false}], + ['18.0.1.1', {version: '18.0.1+1', stable: true, latest: false}], + ['11.0.9.1', {version: '11.0.9+1', stable: true, latest: false}], + ['12.0.2.1.0', {version: '12.0.2+1.0', stable: true, latest: false}], + ['18.0.1.1-ea', {version: '18.0.1+1', stable: false, latest: false}], ['latest', {version: 'x', stable: true, latest: true}], ['LATEST', {version: 'x', stable: true, latest: true}], [' Latest ', {version: 'x', stable: true, latest: true}] diff --git a/dist/setup/index.js b/dist/setup/index.js index b907944d..f6e21597 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -131290,6 +131290,14 @@ class JavaBase { version = version.replace('-ea.', '+'); 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)) { 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`); } diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index b2b87eb3..1e4db7c0 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -15,6 +15,7 @@ - [Tencent Kona](#Tencent-Kona) - [Installing custom Java package type](#Installing-custom-Java-package-type) - [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 JDK without setting as default](#Installing-JDK-without-setting-as-default) - [Installing custom Java distribution from local file](#Installing-Java-from-local-file) @@ -285,6 +286,117 @@ To run the JavaFX application in CI: 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`) keyed on a hash of your `pom.xml` files, and — at the end of +the job — 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 ```yaml diff --git a/src/distributions/base-installer.ts b/src/distributions/base-installer.ts index e80bfffe..9ed68600 100644 --- a/src/distributions/base-installer.ts +++ b/src/distributions/base-installer.ts @@ -4,7 +4,11 @@ import * as fs from 'fs'; import semver from 'semver'; import path from 'path'; import * as httpm from '@actions/http-client'; -import {getToolcachePath, isVersionSatisfies} from '../util.js'; +import { + convertVersionToSemver, + getToolcachePath, + isVersionSatisfies +} from '../util.js'; import { JavaDownloadRelease, JavaInstallerOptions, @@ -288,6 +292,15 @@ export abstract class JavaBase { 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)) { 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`