Compare commits

..

17 Commits

Author SHA1 Message Date
Bruno Borges
6cd4d8602a Document Gradle Wrapper distribution caching in README
Add a parallel "Gradle Wrapper" note alongside the Maven Wrapper note, so the
new behavior for cache: 'gradle' (caching ~/.gradle/wrapper in a separate entry
keyed on **/gradle-wrapper.properties) is documented.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-09 21:29:01 -04:00
Bruno Borges
ec868c0274 Address cache.ts review feedback
- saveAdditionalCache: handle @actions/cache ValidationError (thrown when the
  cache paths do not resolve, e.g. the wrapper distribution was never
  downloaded) by skipping instead of failing the post step. Add a test.
- Point the Gradle wrapper cache comment at issue #269 (Gradle wrapper cache
  churn) instead of the Maven-specific #1095.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-09 21:21:56 -04:00
Bruno Borges
90f45856e1 Fix broken try/catch in saveAdditionalCache and rebuild dist
The autofix commits dropped the `} catch (error) {` line in
saveAdditionalCache, leaving a `try` block without a catch and a dangling
`error` reference, which broke compilation and Prettier. Restore the catch
clause, reformat, and regenerate the dist bundles.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-09 21:16:37 -04:00
Bruno Borges
7b72cee2af Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-09 21:11:19 -04:00
Bruno Borges
49af9112b2 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-09 21:10:44 -04:00
Bruno Borges
66cc98638d Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-09 21:09:47 -04:00
Bruno Borges
ac3a4a8d4b Cache Gradle wrapper distribution separately from the dependency cache (#1098)
The Gradle wrapper distribution (~/.gradle/wrapper) only depends on
gradle-wrapper.properties, which changes rarely, but it was previously
cached in the same entry as ~/.gradle/caches, keyed on volatile
**/*.gradle* files with no restoreKeys (issue #269). Every dependency
change therefore re-downloaded the wrapper.

Move ~/.gradle/wrapper into a dedicated `gradle-wrapper` additional
cache keyed only on **/gradle-wrapper.properties, reusing the
additionalCaches infrastructure introduced for the Maven wrapper fix.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-09 21:00:52 -04:00
Bruno Borges
bc6a9b1b01 Cache Maven wrapper distribution separately from the local repository
The Maven wrapper distribution (~/.m2/wrapper/dists) was cached in the same
entry as the local Maven repository (~/.m2/repository), keyed on a hash of
**/pom.xml (plus wrapper properties and extensions). Because pom.xml changes
frequently and no restoreKeys are used (by design, #269), almost every change
produces a full cache miss and the wrapper distribution is re-downloaded via
mvnw — which intermittently fails due to upstream rate limiting.

The wrapper distribution only depends on maven-wrapper.properties, which
changes very rarely. Give it its own cache entry keyed solely on
**/.mvn/wrapper/maven-wrapper.properties so it survives the frequent pom.xml
changes that rotate the main dependency cache key.

- Add a generic additionalCaches concept to PackageManager, restored and saved
  independently with name-scoped state keys.
- Move ~/.m2/wrapper/dists out of the main maven path into a maven-wrapper
  additional cache; skip silently when the project does not use mvnw.
- Keep cache-hit / cache-primary-key outputs driven by the main cache.
- Update tests, docs, and rebuild dist bundles.

Fixes #1095

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-09 20:27:55 -04:00
Bruno Borges
174d4b5609 Support pinning java-version as "latest" (#1093)
* Support pinning java-version as "latest"

Add a `latest` alias for the `java-version` input that floats to the newest
available stable (GA) release. It is normalized to the SemVer wildcard at the
base-installer layer and always resolves from remote (like `check-latest: true`).

List-based distributions resolve it automatically via the existing newest-first
matching. Corretto selects its newest available major; Oracle and GraalVM look up
the newest GA major via the Adoptium API and request it, failing with an actionable
error if that major isn't published yet. The jdkfile distribution rejects `latest`.

Closes #832

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

* Clarify latest note for oracle/graalvm version resolution vs download

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

* graalvm-community: float latest to its own newest GA release

GraalVM Community publishes its releases on GitHub, so the `latest`
alias now matches against that real release list using the SemVer
wildcard instead of asking the Adoptium API for the newest GA major.
This prevents `latest` from hard-failing when GraalVM lags behind a
freshly released Java major (e.g. Adoptium reports 26 before GraalVM
ships it). Oracle GraalVM has no listing endpoint, so it keeps deriving
the newest major from Adoptium and errors clearly if that major is not
yet published.

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

* Potential fix for pull request finding

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

* Give latest+qualifier inputs (e.g. latest-ea) a targeted error

Inputs like 'latest-ea' had their '-ea' suffix stripped and fell through
to the generic SemVer validation, failing with a confusing "'latest' is
not valid SemVer" message even though 'latest' is supported. Add an
explicit guard so any 'latest*' value other than exactly 'latest' throws
a targeted error explaining that 'latest' resolves GA releases only and
cannot be combined with '-ea' or other qualifiers.

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

---------

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>
2026-07-09 20:11:03 -04:00
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
Bruno Borges
548a822bee docs: clarify V6 ESM migration is not a user-facing breaking change (#1090)
* docs: clarify V6 ESM migration is not a user-facing breaking change

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

* Potential fix for pull request finding

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

---------

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>
2026-07-09 14:18:56 -04:00
Bruno Borges
f848fd1976 feat: expose cache-primary-key output (#597) (#1088)
* feat: expose cache-primary-key output (#597)

Expose the primary cache key computed by the caching logic as a new
`cache-primary-key` action output, so workflows can compose the built-in
setup-java cache key with actions/cache or actions/cache/restore across
steps and dependent jobs.

- src/cache.ts: set the `cache-primary-key` output in restore()
- action.yml: declare the new output
- README.md: document the new output
- __tests__/cache.test.ts: assert the output is set
- dist: rebuild

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

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

* chore: rebuild dist

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

---------

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>
2026-07-09 11:19:31 -04:00
Bruno Borges
fe9b749bcb Add Maven compiler problem matcher for javac diagnostics (#1086)
The javac problem matcher only recognized javac's native diagnostic
format (File.java:12: warning: msg), which works for plain javac and
Gradle but not Maven. The maven-compiler-plugin reformats diagnostics
to [WARNING] /path/File.java:[12,5] msg, so Maven builds produced zero
annotations.

Add a new maven-javac matcher owner that recognizes the Maven format,
capturing severity, file, line, column, and message. Maven builds now
annotate consistently with Gradle and plain javac.

Fixes: #1085

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-08 14:28:02 -04:00
Bruno Borges
4db08ef6bf Infer distribution from asdf .tool-versions vendor prefix (#1084)
* Infer distribution from asdf .tool-versions vendor prefix

asdf-java encodes the JDK vendor as a prefix on the version string in
.tool-versions (e.g. `java temurin-17.0.3+7`). Capture that prefix and
map it to a setup-java distribution, mirroring the existing .sdkmanrc
behavior. Unknown prefixes warn and fall back to the distribution input.

Fixes #1081

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

* Potential fix for pull request finding

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

---------

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>
2026-07-08 12:57:52 -04:00
Bruno Borges
7dbccc66ee Rename jdkFile input to jdk-file with deprecated alias (#1083)
* Rename jdkFile input to jdk-file with deprecated alias

Add a standardized `jdk-file` input to match the lowercase-dash naming
used by every other action input. The camelCase `jdkFile` input is kept
as a deprecated alias: it still works, but emits a deprecation warning and
may be removed in a future release. `jdk-file` takes precedence when both
are provided.

Updates docs and the local-file e2e workflow (one case intentionally keeps
using the deprecated alias for coverage) and regenerates the dist bundles.

Fixes #1077

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

* docs: keep jdkFile in switching-to-v2 guide

The switching-to-v2 migration guide uses actions/setup-java@v2, which only supports the camelCase jdkFile input. Keep the new jdk-file spelling in current-version docs only.

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

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-08 12:44:58 -04:00
24 changed files with 1675 additions and 83 deletions

13
.github/java.json vendored
View File

@@ -21,6 +21,19 @@
"message": 4 "message": 4
} }
] ]
},
{
"owner": "maven-javac",
"pattern": [
{
"regexp": "^\\[(WARNING|ERROR)\\]\\s+(.+?\\.java):\\[(\\d+),(\\d+)\\]\\s+(.+)$",
"severity": 1,
"file": 2,
"line": 3,
"column": 4,
"message": 5
}
]
} }
] ]
} }

View File

@@ -47,7 +47,7 @@ jobs:
id: setup-java id: setup-java
with: with:
distribution: 'jdkfile' distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/${{ env.LocalFilename }} jdk-file: ${{ runner.temp }}/${{ env.LocalFilename }}
java-version: '11.0.0-ea' java-version: '11.0.0-ea'
architecture: x64 architecture: x64
- name: Verify Java version - name: Verify Java version
@@ -88,7 +88,7 @@ jobs:
id: setup-java id: setup-java
with: with:
distribution: 'jdkfile' distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/${{ env.LocalFilename }} jdk-file: ${{ runner.temp }}/${{ env.LocalFilename }}
java-version: '11.0.0-ea' java-version: '11.0.0-ea'
architecture: x64 architecture: x64
- name: Verify Java version - name: Verify Java version
@@ -129,6 +129,7 @@ jobs:
id: setup-java id: setup-java
with: with:
distribution: 'jdkfile' distribution: 'jdkfile'
# Intentionally uses the deprecated `jdkFile` alias to keep it covered.
jdkFile: ${{ runner.temp }}/${{ env.LocalFilename }} jdkFile: ${{ runner.temp }}/${{ env.LocalFilename }}
java-version: '11.0.0-ea' java-version: '11.0.0-ea'
architecture: x64 architecture: x64

View File

@@ -18,9 +18,9 @@ The `setup-java` action provides the following functionality for GitHub Actions
This action allows you to work with Java and Scala projects. This action allows you to work with Java and Scala projects.
## Breaking changes in V6 ## What's new in V6
- **Migrated to ESM** to enable support for the latest `@actions/*` package versions. - **Migrated to ESM** to enable support for the latest `@actions/*` package versions. This is an internal implementation change only. No changes are required to your workflow configuration, and the action's behavior is unchanged. Existing workflows continue to work as before.
## Breaking changes in V5 ## Breaking changes in V5
@@ -41,7 +41,7 @@ For more details, see the full release notes on the [releases page](https://git
- `architecture`: The target architecture of the package. Possible values: `x86`, `x64`, `armv7`, `aarch64`, `ppc64le`. Default value: Derived from the runner machine. - `architecture`: The target architecture of the package. Possible values: `x86`, `x64`, `armv7`, `aarch64`, `ppc64le`. Default value: Derived from the runner machine.
- `jdkFile`: If a use-case requires a custom distribution setup-java uses the compressed JDK from the location pointed by this input and will take care of the installation and caching on the VM. Note: `distribution` must be set to 'jdkfile' (case-sensitive; all lowercase) when using this option. - `jdk-file`: If a use-case requires a custom distribution setup-java uses the compressed JDK from the location pointed by this input and will take care of the installation and caching on the VM. Note: `distribution` must be set to 'jdkfile' (case-sensitive; all lowercase) when using this option. (The camelCase `jdkFile` input is still accepted as a deprecated alias and may be removed in a future release.)
- `check-latest`: Setting this option makes the action to check for the latest available version for the version spec. - `check-latest`: Setting this option makes the action to check for the latest available version for the version spec.
@@ -104,7 +104,16 @@ 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`
- the `latest` alias, which floats to the newest available stable (GA) release
> [!NOTE]
> - `latest` always resolves the newest version from the distribution's remote metadata (it behaves like `check-latest: true`), so it ignores any older version already present in the runner tool cache. This has the same performance trade-off described in [Check latest](#check-latest).
> - `latest` is only supported through the `java-version` input, not through `java-version-file`, and it resolves stable (GA) releases only — it cannot be combined with `-ea`.
> - The `jdkfile` distribution does not support `latest`, as it installs from a local file.
> - For `oracle` and `graalvm` (Oracle GraalVM), `latest` uses the Adoptium API only to determine the newest GA **major version number** — the JDK binary itself is still downloaded from the Oracle / GraalVM servers for that major. Because these distributions have no endpoint to list their own releases, if their servers haven't published the resolved major yet, the action fails and asks you to specify a concrete version. Note the Oracle JDK license caveat below still applies to a floating `latest`.
> - For `graalvm-community`, `latest` floats to the newest GA release published on GitHub, so it never depends on the Adoptium API and always resolves to the newest major that GraalVM Community actually ships.
#### Supported distributions #### Supported distributions
Currently, the following distributions are supported: Currently, the following distributions are supported:
@@ -149,9 +158,11 @@ When the option `cache-dependency-path` is specified, the hash is based on the m
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-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).
The cache input is optional, and caching is turned off by default. The cache input is optional, and caching is turned off by default.
**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 wrapper on every run. This is keyed on `**/.mvn/wrapper/maven-wrapper.properties` as shown above. **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 #### Caching gradle dependencies
```yaml ```yaml
@@ -169,6 +180,8 @@ steps:
``` ```
Using the `cache: gradle` provides a simple and effective way to cache Gradle dependencies with minimal configuration. Using the `cache: gradle` provides a simple and effective way to cache Gradle dependencies with minimal configuration.
**Gradle Wrapper:** when `cache: 'gradle'` is enabled, the action also caches and restores the Gradle Wrapper distribution downloaded to `~/.gradle/wrapper` (in addition to the Gradle caches), so wrapper-based (`./gradlew`) builds don't re-download the Gradle distribution. The wrapper distribution is stored in a **separate** cache entry keyed only on `**/gradle-wrapper.properties`, so it stays cached across the frequent `*.gradle*` changes that rotate the main dependency cache key.
For projects that require more advanced `Gradle` caching features, such as caching build outputs, support for Gradle configuration cache, encrypted cache storage, fine-grained cache control (including options to enable or disable the cache, set it to read-only or write-only, perform automated cleanup, and define custom cache rules), or optimized performance for complex CI workflows, consider using [`gradle/actions/setup-gradle`](https://github.com/gradle/actions/tree/main/setup-gradle). For projects that require more advanced `Gradle` caching features, such as caching build outputs, support for Gradle configuration cache, encrypted cache storage, fine-grained cache control (including options to enable or disable the cache, set it to read-only or write-only, perform automated cleanup, and define custom cache rules), or optimized performance for complex CI workflows, consider using [`gradle/actions/setup-gradle`](https://github.com/gradle/actions/tree/main/setup-gradle).
For setup details and a comprehensive overview of all available features, visit the [setup-gradle documentation](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md). For setup details and a comprehensive overview of all available features, visit the [setup-gradle documentation](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md).
@@ -187,6 +200,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

@@ -133,6 +133,7 @@ describe('dependency cache', () => {
describe('restore', () => { describe('restore', () => {
let spyCacheRestore: any; let spyCacheRestore: any;
let spyGlobHashFiles: any; let spyGlobHashFiles: any;
let spySetOutput: any;
beforeEach(() => { beforeEach(() => {
spyCacheRestore = (cache.restoreCache as any).mockImplementation( spyCacheRestore = (cache.restoreCache as any).mockImplementation(
@@ -140,6 +141,8 @@ describe('dependency cache', () => {
); );
spyGlobHashFiles = glob.hashFiles as jest.Mock; spyGlobHashFiles = glob.hashFiles as jest.Mock;
spyGlobHashFiles.mockResolvedValue('hash-stub'); spyGlobHashFiles.mockResolvedValue('hash-stub');
spySetOutput = core.setOutput as jest.Mock;
spySetOutput.mockImplementation(() => null);
spyWarning.mockImplementation(() => null); spyWarning.mockImplementation(() => null);
}); });
@@ -163,10 +166,7 @@ describe('dependency cache', () => {
await restore('maven', ''); await restore('maven', '');
expect(spyCacheRestore).toHaveBeenCalledWith( expect(spyCacheRestore).toHaveBeenCalledWith(
[ [join(os.homedir(), '.m2', 'repository')],
join(os.homedir(), '.m2', 'repository'),
join(os.homedir(), '.m2', 'wrapper', 'dists')
],
expect.any(String) expect.any(String)
); );
expect(spyGlobHashFiles).toHaveBeenCalledWith( expect(spyGlobHashFiles).toHaveBeenCalledWith(
@@ -175,6 +175,15 @@ describe('dependency cache', () => {
expect(spyWarning).not.toHaveBeenCalled(); expect(spyWarning).not.toHaveBeenCalled();
expect(spyInfo).toHaveBeenCalledWith('maven cache is not found'); expect(spyInfo).toHaveBeenCalledWith('maven cache is not found');
}); });
it('sets the cache-primary-key output', async () => {
createFile(join(workspace, 'pom.xml'));
await restore('maven', '');
expect(spySetOutput).toHaveBeenCalledWith(
'cache-primary-key',
expect.stringContaining('setup-java-')
);
});
it('downloads cache based on maven-wrapper.properties', async () => { it('downloads cache based on maven-wrapper.properties', async () => {
createDirectory(join(workspace, '.mvn')); createDirectory(join(workspace, '.mvn'));
createDirectory(join(workspace, '.mvn', 'wrapper')); createDirectory(join(workspace, '.mvn', 'wrapper'));
@@ -184,10 +193,7 @@ describe('dependency cache', () => {
await restore('maven', ''); await restore('maven', '');
expect(spyCacheRestore).toHaveBeenCalledWith( expect(spyCacheRestore).toHaveBeenCalledWith(
[ [join(os.homedir(), '.m2', 'repository')],
join(os.homedir(), '.m2', 'repository'),
join(os.homedir(), '.m2', 'wrapper', 'dists')
],
expect.any(String) expect.any(String)
); );
expect(spyGlobHashFiles).toHaveBeenCalledWith( expect(spyGlobHashFiles).toHaveBeenCalledWith(
@@ -202,10 +208,7 @@ describe('dependency cache', () => {
await restore('maven', ''); await restore('maven', '');
expect(spyCacheRestore).toHaveBeenCalledWith( expect(spyCacheRestore).toHaveBeenCalledWith(
[ [join(os.homedir(), '.m2', 'repository')],
join(os.homedir(), '.m2', 'repository'),
join(os.homedir(), '.m2', 'wrapper', 'dists')
],
expect.any(String) expect.any(String)
); );
expect(spyGlobHashFiles).toHaveBeenCalledWith( expect(spyGlobHashFiles).toHaveBeenCalledWith(
@@ -214,6 +217,47 @@ describe('dependency cache', () => {
expect(spyWarning).not.toHaveBeenCalled(); expect(spyWarning).not.toHaveBeenCalled();
expect(spyInfo).toHaveBeenCalledWith('maven cache is not found'); expect(spyInfo).toHaveBeenCalledWith('maven cache is not found');
}); });
it('restores the maven wrapper distribution cache independently of the main cache', async () => {
createDirectory(join(workspace, '.mvn'));
createDirectory(join(workspace, '.mvn', 'wrapper'));
createFile(
join(workspace, '.mvn', 'wrapper', 'maven-wrapper.properties')
);
await restore('maven', '');
// Main dependency cache no longer carries the wrapper dists path.
expect(spyCacheRestore).toHaveBeenCalledWith(
[join(os.homedir(), '.m2', 'repository')],
expect.any(String)
);
expect(spyCacheRestore).toHaveBeenCalledWith(
[join(os.homedir(), '.m2', 'wrapper', 'dists')],
expect.stringContaining('maven-wrapper')
);
expect(spyGlobHashFiles).toHaveBeenCalledWith(
'**/.mvn/wrapper/maven-wrapper.properties'
);
});
it('skips the maven wrapper cache when no wrapper properties exist', async () => {
createFile(join(workspace, 'pom.xml'));
spyGlobHashFiles.mockImplementation((pattern: string) =>
Promise.resolve(
pattern === '**/.mvn/wrapper/maven-wrapper.properties'
? ''
: 'hash-stub'
)
);
await restore('maven', '');
// Only the main dependency cache is restored; the wrapper cache path is
// never touched because the project does not use mvnw.
expect(spyCacheRestore).toHaveBeenCalledTimes(1);
expect(spyCacheRestore).toHaveBeenCalledWith(
[join(os.homedir(), '.m2', 'repository')],
expect.any(String)
);
expect(spyWarning).not.toHaveBeenCalled();
});
}); });
describe('for gradle', () => { describe('for gradle', () => {
it('throws error if no build.gradle found', async () => { it('throws error if no build.gradle found', async () => {
@@ -270,6 +314,43 @@ describe('dependency cache', () => {
expect(spyWarning).not.toHaveBeenCalled(); expect(spyWarning).not.toHaveBeenCalled();
expect(spyInfo).toHaveBeenCalledWith('gradle cache is not found'); expect(spyInfo).toHaveBeenCalledWith('gradle cache is not found');
}); });
it('restores the gradle wrapper distribution cache independently of the main cache', async () => {
createFile(join(workspace, 'build.gradle'));
await restore('gradle', '');
// Main dependency cache no longer carries the wrapper path.
expect(spyCacheRestore).toHaveBeenCalledWith(
[join(os.homedir(), '.gradle', 'caches')],
expect.any(String)
);
// Wrapper distribution is restored on its own, keyed only on the
// wrapper properties file.
expect(spyCacheRestore).toHaveBeenCalledWith(
[join(os.homedir(), '.gradle', 'wrapper')],
expect.stringContaining('setup-java-')
);
expect(spyGlobHashFiles).toHaveBeenCalledWith(
'**/gradle-wrapper.properties'
);
});
it('skips the gradle wrapper cache when no wrapper properties exist', async () => {
createFile(join(workspace, 'build.gradle'));
spyGlobHashFiles.mockImplementation((pattern: string) =>
Promise.resolve(
pattern === '**/gradle-wrapper.properties' ? '' : 'hash-stub'
)
);
await restore('gradle', '');
// Only the main dependency cache is restored; the wrapper cache path is
// never touched because the project does not use the gradle wrapper.
expect(spyCacheRestore).toHaveBeenCalledTimes(1);
expect(spyCacheRestore).toHaveBeenCalledWith(
[join(os.homedir(), '.gradle', 'caches')],
expect.any(String)
);
expect(spyWarning).not.toHaveBeenCalled();
});
}); });
describe('for sbt', () => { describe('for sbt', () => {
it('throws error if no build.sbt found', async () => { it('throws error if no build.sbt found', async () => {
@@ -445,6 +526,77 @@ describe('dependency cache', () => {
expect.stringMatching(/^Cache saved with the key:.*/) expect.stringMatching(/^Cache saved with the key:.*/)
); );
}); });
it('saves the maven wrapper distribution cache under its own key', async () => {
createFile(join(workspace, 'pom.xml'));
(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-maven-wrapper':
return 'setup-java-maven-wrapper-key';
default:
return '';
}
});
await save('maven');
expect(spyCacheSave).toHaveBeenCalledWith(
[join(os.homedir(), '.m2', 'wrapper', 'dists')],
'setup-java-maven-wrapper-key'
);
expect(spyWarning).not.toHaveBeenCalled();
});
it('does not save the maven wrapper cache on an exact wrapper hit', async () => {
createFile(join(workspace, 'pom.xml'));
(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-maven-wrapper':
case 'cache-matched-key-maven-wrapper':
return 'setup-java-maven-wrapper-key';
default:
return '';
}
});
await save('maven');
expect(spyCacheSave).not.toHaveBeenCalledWith(
[join(os.homedir(), '.m2', 'wrapper', 'dists')],
expect.any(String)
);
});
it('does not fail the post step when the wrapper distribution path is missing', async () => {
createFile(join(workspace, 'pom.xml'));
(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-maven-wrapper':
return 'setup-java-maven-wrapper-key';
default:
return '';
}
});
spyCacheSave.mockImplementation((paths: string[]) =>
paths.includes(join(os.homedir(), '.m2', 'wrapper', 'dists'))
? Promise.reject(
new cache.ValidationError(
'Path Validation Error: Path(s) specified in the action for caching do(es) not exist'
)
)
: Promise.resolve(0)
);
await expect(save('maven')).resolves.toBeUndefined();
expect(spyWarning).not.toHaveBeenCalled();
});
}); });
describe('for gradle', () => { describe('for gradle', () => {
it('uploads cache even if no build.gradle found', async () => { it('uploads cache even if no build.gradle found', async () => {
@@ -497,6 +649,50 @@ describe('dependency cache', () => {
expect.stringMatching(/^Cache saved with the key:.*/) expect.stringMatching(/^Cache saved with the key:.*/)
); );
}); });
it('saves the gradle wrapper distribution cache under its own key', async () => {
createFile(join(workspace, 'build.gradle'));
(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-gradle-wrapper':
return 'setup-java-gradle-wrapper-key';
default:
return '';
}
});
await save('gradle');
expect(spyCacheSave).toHaveBeenCalledWith(
[join(os.homedir(), '.gradle', 'wrapper')],
'setup-java-gradle-wrapper-key'
);
expect(spyWarning).not.toHaveBeenCalled();
});
it('does not save the gradle wrapper cache on an exact wrapper hit', async () => {
createFile(join(workspace, 'build.gradle'));
(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-gradle-wrapper':
case 'cache-matched-key-gradle-wrapper':
return 'setup-java-gradle-wrapper-key';
default:
return '';
}
});
await save('gradle');
expect(spyCacheSave).not.toHaveBeenCalledWith(
[join(os.homedir(), '.gradle', 'wrapper')],
expect.any(String)
);
});
}); });
describe('for sbt', () => { describe('for sbt', () => {
it('uploads cache even if no build.sbt found', async () => { it('uploads cache even if no build.sbt found', async () => {

View File

@@ -420,6 +420,29 @@ describe('setupJava', () => {
expect(spyCoreInfo).not.toHaveBeenCalledWith('Trying to download...'); expect(spyCoreInfo).not.toHaveBeenCalledWith('Trying to download...');
}); });
it('should resolve the latest version from remote when java-version is "latest", even if a version is cached', async () => {
mockJavaBase = new EmptyJavaBase({
version: 'latest',
architecture: 'x86',
packageType: 'jdk',
checkLatest: false
});
await expect(mockJavaBase.setupJava()).resolves.toEqual({
version: actualJavaVersion,
path: javaPathInstalled
});
// `latest` must bypass the tool-cache short-circuit and always resolve remotely
expect(spyCoreInfo).toHaveBeenCalledWith(
'Trying to resolve the latest version from remote'
);
expect(spyCoreInfo).toHaveBeenCalledWith('Trying to download...');
expect(spyCoreInfo).not.toHaveBeenCalledWith(
`Resolved Java ${installedJavaVersion} from tool-cache`
);
});
it.each([ it.each([
[ [
{ {
@@ -744,11 +767,18 @@ describe('normalizeVersion', () => {
const DummyJavaBase = JavaBase as any; const DummyJavaBase = JavaBase as any;
it.each([ it.each([
['11', {version: '11', stable: true}], ['11', {version: '11', stable: true, latest: false}],
['11.0', {version: '11.0', stable: true}], ['11.0', {version: '11.0', stable: true, latest: false}],
['11.0.10', {version: '11.0.10', stable: true}], ['11.0.10', {version: '11.0.10', stable: true, latest: false}],
['11-ea', {version: '11', stable: false}], ['11-ea', {version: '11', stable: false, latest: false}],
['11.0.2-ea', {version: '11.0.2', stable: 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}]
])('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
@@ -763,6 +793,17 @@ describe('normalizeVersion', () => {
`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`
); );
}); });
it.each(['latest-ea', 'latest.1', 'LATEST-EA', ' latest-ea '])(
'normalizeVersion should throw a targeted error for latest combined with a qualifier (%s)',
version => {
expect(
DummyJavaBase.prototype.normalizeVersion.bind(null, version)
).toThrow(
`The 'latest' alias resolves stable (GA) releases only and cannot be combined with '-ea' or other qualifiers (received '${version}'). Use 'latest' on its own, or specify a concrete version.`
);
}
);
}); });
describe('createVersionNotFoundError', () => { describe('createVersionNotFoundError', () => {

View File

@@ -204,6 +204,24 @@ describe('getAvailableVersions', () => {
expect(availableVersion.url).toBe(expectedLink); expect(availableVersion.url).toBe(expectedLink);
}); });
it('with latest resolves to the newest available major version', async () => {
const distribution = new CorrettoDistribution({
version: 'latest',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
});
mockPlatform(distribution, 'linux');
const availableVersion =
await distribution['findPackageForDownload']('x');
expect(availableVersion).not.toBeNull();
// 18 is the newest major present in the mocked Corretto index
expect(availableVersion.url).toBe(
'https://corretto.aws/downloads/resources/18.0.0.37.1/amazon-corretto-18.0.0.37.1-linux-x64.tar.gz'
);
});
it('with unstable version expect to throw not supported error', async () => { it('with unstable version expect to throw not supported error', async () => {
const version = '18.0.1-ea'; const version = '18.0.1-ea';
const distribution = new CorrettoDistribution({ const distribution = new CorrettoDistribution({

View File

@@ -417,6 +417,61 @@ describe('GraalVMDistribution', () => {
); );
}); });
describe('latest alias', () => {
it('resolves the newest major version from the Adoptium API', async () => {
const latestDistribution = new GraalVMDistribution({
...defaultOptions,
version: 'latest'
});
(latestDistribution as any).http = mockHttpClient;
jest
.spyOn(latestDistribution, 'getPlatform')
.mockReturnValue('linux');
mockHttpClient.getJson.mockResolvedValue({
statusCode: 200,
result: {most_recent_feature_release: 25},
headers: {}
});
mockHttpClient.head.mockResolvedValue({
message: {statusCode: 200}
});
const result = await (
latestDistribution as any
).findPackageForDownload('x');
expect(result).toEqual({
url: 'https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-x64_bin.tar.gz',
version: '25'
});
});
it('throws an actionable error when the latest major is not yet available', async () => {
const latestDistribution = new GraalVMDistribution({
...defaultOptions,
version: 'latest'
});
(latestDistribution as any).http = mockHttpClient;
jest
.spyOn(latestDistribution, 'getPlatform')
.mockReturnValue('linux');
mockHttpClient.getJson.mockResolvedValue({
statusCode: 200,
result: {most_recent_feature_release: 25},
headers: {}
});
mockHttpClient.head.mockResolvedValue({
message: {statusCode: 404}
});
await expect(
(latestDistribution as any).findPackageForDownload('x')
).rejects.toThrow(
/is not yet available for the GraalVM distribution/
);
});
});
it('should throw error for JDK versions less than 17', async () => { it('should throw error for JDK versions less than 17', async () => {
await expect( await expect(
(distribution as any).findPackageForDownload('11') (distribution as any).findPackageForDownload('11')
@@ -1115,6 +1170,60 @@ describe('GraalVMDistribution', () => {
}); });
}); });
it('resolves latest to the newest GA across all Community majors without calling Adoptium', async () => {
const latestCommunity = new GraalVMCommunityDistribution({
...defaultOptions,
version: 'latest'
});
(latestCommunity as any).http = mockHttpClient;
jest.spyOn(latestCommunity, 'getPlatform').mockReturnValue('linux');
mockHttpClient.getJson.mockResolvedValue({
result: [
{
draft: false,
prerelease: false,
assets: [
{
name: 'graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz',
browser_download_url:
'https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz'
}
]
},
{
draft: false,
prerelease: false,
assets: [
{
name: 'graalvm-community-jdk-24.0.1_linux-x64_bin.tar.gz',
browser_download_url:
'https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-24.0.1/graalvm-community-jdk-24.0.1_linux-x64_bin.tar.gz'
}
]
}
],
statusCode: 200,
headers: {}
});
const result = await (latestCommunity as any).findPackageForDownload(
'x'
);
expect(result).toEqual({
url: 'https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-24.0.1/graalvm-community-jdk-24.0.1_linux-x64_bin.tar.gz',
version: '24.0.1'
});
// The Community release list is authoritative, so the Adoptium
// most_recent_feature_release endpoint must not be consulted.
expect(mockHttpClient.getJson).toHaveBeenCalledTimes(1);
expect(mockHttpClient.getJson).toHaveBeenCalledWith(
expect.stringContaining('graalvm-ce-builds/releases'),
expect.anything()
);
});
it('should reject GraalVM Community early access requests', async () => { it('should reject GraalVM Community early access requests', async () => {
(communityDistribution as any).stable = false; (communityDistribution as any).stable = false;

View File

@@ -170,6 +170,20 @@ describe('setupJava', () => {
jest.restoreAllMocks(); jest.restoreAllMocks();
}); });
it('throws for the latest alias since jdkfile has no version list', async () => {
const inputs = {
version: 'latest',
architecture: 'x86',
packageType: 'jdk',
checkLatest: false
};
mockJavaBase = new LocalDistribution(inputs, expectedJdkFile);
await expect(mockJavaBase.setupJava()).rejects.toThrow(
"The 'latest' version alias is not supported for the 'jdkfile' distribution. Please specify a concrete version."
);
});
it('java is resolved from toolcache, jdkfile is untouched', async () => { it('java is resolved from toolcache, jdkfile is untouched', async () => {
const inputs = { const inputs = {
version: actualJavaVersion, version: actualJavaVersion,

View File

@@ -174,3 +174,59 @@ describe('findPackageForDownload', () => {
); );
}); });
}); });
describe('findPackageForDownload with latest', () => {
let spyHttpClientHead: any;
let spyHttpClientGetJson: any;
beforeEach(() => {
(core.debug as jest.Mock).mockImplementation(() => {});
(core.error as jest.Mock).mockImplementation(() => {});
spyHttpClientGetJson = jest.spyOn(HttpClient.prototype, 'getJson');
spyHttpClientGetJson.mockResolvedValue({
statusCode: 200,
result: {most_recent_feature_release: 25},
headers: {}
});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('resolves the newest major version from the Adoptium API', async () => {
spyHttpClientHead = jest.spyOn(HttpClient.prototype, 'head');
spyHttpClientHead.mockResolvedValue({message: {statusCode: 200}});
const distribution = new OracleDistribution({
version: 'latest',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
});
const result = await distribution['findPackageForDownload']('x');
const osType = distribution.getPlatform();
const archiveType = getDownloadArchiveExtension();
expect(result.version).toBe('25');
expect(result.url).toBe(
`https://download.oracle.com/java/25/latest/jdk-25_${osType}-x64_bin.${archiveType}`
);
});
it('throws an actionable error when the latest major is not yet available', async () => {
spyHttpClientHead = jest.spyOn(HttpClient.prototype, 'head');
spyHttpClientHead.mockResolvedValue({message: {statusCode: 404}});
const distribution = new OracleDistribution({
version: 'latest',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
});
await expect(distribution['findPackageForDownload']('x')).rejects.toThrow(
/is not yet available for the Oracle JDK distribution/
);
});
});

View File

@@ -312,6 +312,24 @@ describe('findPackageForDownload', () => {
expect(resolvedVersion.signatureUrl).toBeDefined(); expect(resolvedVersion.signatureUrl).toBeDefined();
}); });
it('version "latest" is normalized to the newest available version', async () => {
const distribution = new TemurinDistribution(
{
version: 'latest',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
},
TemurinImplementation.Hotspot
);
distribution['getAvailableVersions'] = async () => manifestData as any;
// normalizeVersion turns `latest` into the wildcard carried on `this.version`
const resolvedVersion = await distribution['findPackageForDownload'](
distribution['version']
);
expect(resolvedVersion.version).toBe('16.0.2+7');
});
it('version is found but binaries list is empty', async () => { it('version is found but binaries list is empty', async () => {
const distribution = new TemurinDistribution( const distribution = new TemurinDistribution(
{ {

View File

@@ -56,7 +56,8 @@ const {
isVersionSatisfies, isVersionSatisfies,
isCacheFeatureAvailable, isCacheFeatureAvailable,
isGhes, isGhes,
validatePaginationUrl validatePaginationUrl,
getLatestMajorVersion
} = await import('../src/util.js'); } = await import('../src/util.js');
describe('isVersionSatisfies', () => { describe('isVersionSatisfies', () => {
@@ -296,6 +297,72 @@ describe('getVersionFromFileContent', () => {
); );
}); });
}); });
describe('.tool-versions', () => {
it.each([
['java temurin-17.0.3+7', '17.0.3+7', 'temurin'],
['java temurin-jre-17.0.3+7', '17.0.3+7', 'temurin'],
['java adoptopenjdk-11.0.16+8', '11.0.16+8', 'temurin'],
['java adoptopenjdk-openj9-11.0.16+8', '11.0.16+8', 'temurin'],
['java zulu-11.56.19', '11.56.19', 'zulu'],
['java corretto-17.0.13.11.1', '17', 'corretto'], // corretto -> major only
['java liberica-11.0.15+10', '11.0.15+10', 'liberica'],
['java microsoft-11.0.13.8.1', '11.0.13', 'microsoft'],
['java semeru-openj9-11.0.25+9', '11.0.25+9', 'semeru'],
['java ibm-openj9-11.0.25+9', '11.0.25+9', 'semeru'],
['java dragonwell-17.0.13.0.13+11', '17.0.13', 'dragonwell'],
['java graalvm-22.3.0+java17', '22.3.0+java17', 'graalvm'],
['java graalvm-community-22.3.0', '22.3.0', 'graalvm-community'],
['java oracle-graalvm-21.0.5', '21.0.5', 'graalvm'],
['java oracle-21.0.5', '21.0.5', 'oracle'],
['java sapmachine-21.0.5', '21.0.5', 'sapmachine'],
['java kona-17.0.13', '17.0.13', 'kona'],
['java jetbrains-21.0.5', '21.0.5', 'jetbrains']
])(
'parsing %s should return version %s and distribution %s',
(content: string, expectedVersion: string, expectedDist: string) => {
const actual = getVersionFromFileContent(
content,
'openjdk',
'.tool-versions'
);
expect(actual?.version).toBe(expectedVersion);
expect(actual?.distribution).toBe(expectedDist);
}
);
it.each([
['java 17.0.7', '17.0.7'],
['java 17', '17'],
['java 1.8', '8'],
['java 21-ea', '21-ea']
])(
'parsing prefix-less %s should return version %s and no distribution',
(content: string, expectedVersion: string) => {
const actual = getVersionFromFileContent(
content,
'temurin',
'.tool-versions'
);
expect(actual?.version).toBe(expectedVersion);
expect(actual?.distribution).toBeUndefined();
}
);
it('should warn and return undefined distribution for unsupported vendor', () => {
const warnSpy = jest.spyOn(core, 'warning');
const actual = getVersionFromFileContent(
'java openjdk-17.0.7',
'temurin',
'.tool-versions'
);
expect(actual?.version).toBe('17.0.7');
expect(actual?.distribution).toBeUndefined();
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Unknown asdf distribution identifier')
);
});
});
}); });
describe('isGhes', () => { describe('isGhes', () => {
@@ -334,3 +401,33 @@ describe('isGhes', () => {
expect(isGhes()).toBeTruthy(); expect(isGhes()).toBeTruthy();
}); });
}); });
describe('getLatestMajorVersion', () => {
const makeHttp = (getJson: jest.Mock) =>
({getJson}) as unknown as import('@actions/http-client').HttpClient;
it('returns most_recent_feature_release from the Adoptium API', async () => {
const getJson = jest.fn(async () => ({
statusCode: 200,
result: {most_recent_feature_release: 25},
headers: {}
}));
await expect(getLatestMajorVersion(makeHttp(getJson))).resolves.toBe(25);
expect(getJson).toHaveBeenCalledWith(
'https://api.adoptium.net/v3/info/available_releases'
);
});
it('throws when the response does not contain a usable value', async () => {
const getJson = jest.fn(async () => ({
statusCode: 200,
result: {},
headers: {}
}));
await expect(getLatestMajorVersion(makeHttp(getJson))).rejects.toThrow(
'Could not determine the latest available Java major version'
);
});
});

View File

@@ -4,7 +4,7 @@ description: 'Set up a specific version of the Java JDK and add the
author: 'GitHub' author: 'GitHub'
inputs: inputs:
java-version: java-version:
description: 'The Java version to set up. Takes a whole or semver Java version. See examples of supported syntax in README file' description: 'The Java version to set up. Takes a whole or semver Java version, or the "latest" alias to use the newest available stable release. See examples of supported syntax in README file'
required: false required: false
java-version-file: java-version-file:
description: 'The path to a file containing the Java version to set up (.java-version, .tool-versions, .sdkmanrc). Used when java-version is not set. See examples of supported syntax in README file' description: 'The path to a file containing the Java version to set up (.java-version, .tool-versions, .sdkmanrc). Used when java-version is not set. See examples of supported syntax in README file'
@@ -19,9 +19,13 @@ inputs:
architecture: architecture:
description: "The architecture of the package (defaults to the action runner's architecture)" description: "The architecture of the package (defaults to the action runner's architecture)"
required: false required: false
jdkFile: jdk-file:
description: 'Path to where the compressed JDK is located' description: 'Path to where the compressed JDK is located'
required: false required: false
jdkFile:
description: 'Deprecated alias for `jdk-file`. Path to where the compressed JDK is located. Use `jdk-file` instead; this alias may be removed in a future release.'
required: false
deprecationMessage: 'The `jdkFile` input is deprecated. Use `jdk-file` instead.'
check-latest: check-latest:
description: 'Set this option if you want the action to check for the latest available version that satisfies the version spec' description: 'Set this option if you want the action to check for the latest available version that satisfies the version spec'
required: false required: false
@@ -99,6 +103,8 @@ outputs:
description: 'Path to where the java environment has been installed (same as $JAVA_HOME)' description: 'Path to where the java environment has been installed (same as $JAVA_HOME)'
cache-hit: cache-hit:
description: 'A boolean value to indicate an exact match was found for the primary key' description: 'A boolean value to indicate an exact match was found for the primary key'
cache-primary-key:
description: 'The primary cache key computed by the action for the configured build tool. Empty when caching is not enabled or when caching is skipped (e.g. cache service unavailable). Useful for composing with actions/cache or actions/cache/restore across jobs.'
runs: runs:
using: 'node24' using: 'node24'
main: 'dist/setup/index.js' main: 'dist/setup/index.js'

198
dist/cleanup/index.js vendored
View File

@@ -95834,7 +95834,8 @@ const INPUT_JAVA_VERSION_FILE = 'java-version-file';
const INPUT_ARCHITECTURE = 'architecture'; const INPUT_ARCHITECTURE = 'architecture';
const INPUT_JAVA_PACKAGE = 'java-package'; const INPUT_JAVA_PACKAGE = 'java-package';
const INPUT_DISTRIBUTION = 'distribution'; const INPUT_DISTRIBUTION = 'distribution';
const INPUT_JDK_FILE = 'jdkFile'; const INPUT_JDK_FILE = 'jdk-file';
const INPUT_JDK_FILE_DEPRECATED = 'jdkFile';
const INPUT_CHECK_LATEST = 'check-latest'; const INPUT_CHECK_LATEST = 'check-latest';
const INPUT_SET_DEFAULT = 'set-default'; const INPUT_SET_DEFAULT = 'set-default';
const INPUT_VERIFY_SIGNATURE = 'verify-signature'; const INPUT_VERIFY_SIGNATURE = 'verify-signature';
@@ -95965,8 +95966,10 @@ function getVersionFromFileContent(content, distributionName, versionFile) {
} }
const versionFileName = getFileName(versionFile); const versionFileName = getFileName(versionFile);
if (versionFileName == '.tool-versions') { if (versionFileName == '.tool-versions') {
// Capture an optional asdf-java vendor prefix (e.g. `temurin-`, `corretto-`)
// in the `distribution` group so it can be mapped to a setup-java distribution.
javaVersionRegExp = javaVersionRegExp =
/^java\s+(?:\S*-)?(?<version>\d+(?:\.\d+)*([+_.-](?:openj9[-._]?\d[\w.-]*|java\d+|jre[-_\w]*|OpenJDK\d+[\w_.-]*|[a-z0-9]+))*)/im; /^java\s+(?:(?<distribution>\S*)-)?(?<version>\d+(?:\.\d+)*([+_.-](?:openj9[-._]?\d[\w.-]*|java\d+|jre[-_\w]*|OpenJDK\d+[\w_.-]*|[a-z0-9]+))*)/im;
} }
else if (versionFileName == '.sdkmanrc') { else if (versionFileName == '.sdkmanrc') {
// Match both version and optional distribution identifier // Match both version and optional distribution identifier
@@ -95986,6 +95989,14 @@ function getVersionFromFileContent(content, distributionName, versionFile) {
extractedDistribution = mapSdkmanDistribution(sdkmanDist); extractedDistribution = mapSdkmanDistribution(sdkmanDist);
core.debug(`Parsed distribution '${extractedDistribution}' from SDKMAN identifier '${sdkmanDist}'`); core.debug(`Parsed distribution '${extractedDistribution}' from SDKMAN identifier '${sdkmanDist}'`);
} }
// Extract distribution from asdf .tool-versions file
if (versionFileName == '.tool-versions' && match?.groups?.distribution) {
const asdfDist = match.groups.distribution;
extractedDistribution = mapAsdfDistribution(asdfDist);
if (extractedDistribution) {
core.debug(`Parsed distribution '${extractedDistribution}' from asdf identifier '${asdfDist}'`);
}
}
core.debug(`Parsed version '${capturedVersion}' from file '${versionFileName}'`); core.debug(`Parsed version '${capturedVersion}' from file '${versionFileName}'`);
if (!capturedVersion) { if (!capturedVersion) {
return null; return null;
@@ -96034,6 +96045,43 @@ function mapSdkmanDistribution(sdkmanDist) {
} }
return mapped; return mapped;
} }
// Map asdf-java (.tool-versions) vendor identifiers to setup-java distribution names.
// asdf-java encodes the vendor as a prefix on the version string, e.g.
// `java temurin-17.0.3+7` or `java semeru-openj9-11.0.25+9`. Packaging variants
// (`-jre`, `-musl`, `-openj9`, `-crac`, `-javafx`, ...) are collapsed onto the
// base vendor since setup-java does not distinguish them here.
function mapAsdfDistribution(asdfDist) {
const normalized = asdfDist.toLowerCase();
// Multi-segment vendors that map to a distinct setup-java distribution.
if (normalized.startsWith('graalvm-community')) {
return 'graalvm-community';
}
if (normalized.startsWith('oracle-graalvm')) {
return 'graalvm';
}
const baseVendor = normalized.split('-')[0];
const distributionMap = {
temurin: 'temurin',
adoptopenjdk: 'temurin',
zulu: 'zulu',
corretto: 'corretto',
liberica: 'liberica',
microsoft: 'microsoft',
semeru: 'semeru',
ibm: 'semeru',
dragonwell: 'dragonwell',
graalvm: 'graalvm',
oracle: 'oracle',
sapmachine: 'sapmachine',
kona: 'kona',
jetbrains: 'jetbrains'
};
const mapped = distributionMap[baseVendor];
if (!mapped) {
core.warning(`Unknown asdf distribution identifier '${asdfDist}'. Please specify the distribution explicitly.`);
}
return mapped;
}
// By convention, action expects version 8 in the format `8.*` instead of `1.8` // By convention, action expects version 8 in the format `8.*` instead of `1.8`
function avoidOldNotation(content) { function avoidOldNotation(content) {
return content.startsWith('1.') ? content.substring(2) : content; return content.startsWith('1.') ? content.substring(2) : content;
@@ -96106,6 +96154,22 @@ function renameWinArchive(javaArchivePath) {
fs.renameSync(javaArchivePath, javaArchivePathRenamed); fs.renameSync(javaArchivePath, javaArchivePathRenamed);
return javaArchivePathRenamed; return javaArchivePathRenamed;
} }
// Resolve the newest available stable/GA feature (major) release.
//
// Some distributions (e.g. Oracle, GraalVM) construct their download URLs from a
// concrete major version and don't expose an endpoint to list every available
// release, so a bare `latest` alias can't be resolved from their own metadata.
// The Adoptium (Temurin) API is used as a proxy for "what is the newest GA major
// version out there", which those distributions typically publish at the same time.
async function getLatestMajorVersion(http) {
const availableReleasesUrl = 'https://api.adoptium.net/v3/info/available_releases';
const response = await http.getJson(availableReleasesUrl);
const mostRecent = response.result?.most_recent_feature_release;
if (!mostRecent || Number.isNaN(Number(mostRecent))) {
throw new Error(`Could not determine the latest available Java major version from ${availableReleasesUrl}`);
}
return Number(mostRecent);
}
;// CONCATENATED MODULE: ./src/gpg.ts ;// CONCATENATED MODULE: ./src/gpg.ts
@@ -99602,23 +99666,28 @@ const CACHE_KEY_PREFIX = 'setup-java';
const supportedPackageManager = [ const supportedPackageManager = [
{ {
id: 'maven', id: 'maven',
path: [ path: [(0,external_path_.join)(external_os_default().homedir(), '.m2', 'repository')],
(0,external_path_.join)(external_os_default().homedir(), '.m2', 'repository'),
(0,external_path_.join)(external_os_default().homedir(), '.m2', 'wrapper', 'dists')
],
// https://github.com/actions/cache/blob/0638051e9af2c23d10bb70fa9beffcad6cff9ce3/examples.md#java---maven // https://github.com/actions/cache/blob/0638051e9af2c23d10bb70fa9beffcad6cff9ce3/examples.md#java---maven
pattern: [ pattern: [
'**/pom.xml', '**/pom.xml',
'**/.mvn/wrapper/maven-wrapper.properties', '**/.mvn/wrapper/maven-wrapper.properties',
'**/.mvn/extensions.xml' '**/.mvn/extensions.xml'
],
// The Maven wrapper distribution only depends on the wrapper properties,
// which change very rarely, so it is cached separately from the local
// repository. This keeps it available across the frequent pom.xml changes
// that rotate the main cache key. See issue #1095.
additionalCaches: [
{
name: 'maven-wrapper',
path: [(0,external_path_.join)(external_os_default().homedir(), '.m2', 'wrapper', 'dists')],
pattern: ['**/.mvn/wrapper/maven-wrapper.properties']
}
] ]
}, },
{ {
id: 'gradle', id: 'gradle',
path: [ path: [(0,external_path_.join)(external_os_default().homedir(), '.gradle', 'caches')],
(0,external_path_.join)(external_os_default().homedir(), '.gradle', 'caches'),
(0,external_path_.join)(external_os_default().homedir(), '.gradle', 'wrapper')
],
// https://github.com/actions/cache/blob/0638051e9af2c23d10bb70fa9beffcad6cff9ce3/examples.md#java---gradle // https://github.com/actions/cache/blob/0638051e9af2c23d10bb70fa9beffcad6cff9ce3/examples.md#java---gradle
pattern: [ pattern: [
'**/*.gradle*', '**/*.gradle*',
@@ -99627,6 +99696,17 @@ const supportedPackageManager = [
'buildSrc/**/Dependencies.kt', 'buildSrc/**/Dependencies.kt',
'gradle/*.versions.toml', 'gradle/*.versions.toml',
'**/versions.properties' '**/versions.properties'
],
// The Gradle wrapper distribution only depends on the wrapper properties,
// which change very rarely, so it is cached separately from the Gradle
// caches. This keeps it available across the frequent *.gradle* changes
// that rotate the main cache key. See issue #269.
additionalCaches: [
{
name: 'gradle-wrapper',
path: [(0,external_path_.join)(external_os_default().homedir(), '.gradle', 'wrapper')],
pattern: ['**/gradle-wrapper.properties']
}
] ]
}, },
{ {
@@ -99662,6 +99742,19 @@ function findPackageManager(id) {
} }
return packageManager; return packageManager;
} }
/**
* State keys used to carry an additional cache's restore-time information over
* to the post (save) action, scoped by the additional cache name.
*/
function additionalCachePrimaryKeyState(name) {
return `${STATE_CACHE_PRIMARY_KEY}-${name}`;
}
function additionalCacheMatchedKeyState(name) {
return `${CACHE_MATCHED_KEY}-${name}`;
}
function buildCacheKey(id, fileHash) {
return `${CACHE_KEY_PREFIX}-${process.env['RUNNER_OS']}-${process.arch}-${id}-${fileHash}`;
}
/** /**
* A function that generates a cache key to use. * A function that generates a cache key to use.
* Format of the generated key will be "${{ platform }}-${{ id }}-${{ fileHash }}"". * Format of the generated key will be "${{ platform }}-${{ id }}-${{ fileHash }}"".
@@ -99675,7 +99768,19 @@ async function computeCacheKey(packageManager, cacheDependencyPath) {
if (!fileHash) { if (!fileHash) {
throw new Error(`No file in ${process.cwd()} matched to [${pattern}], make sure you have checked out the target repository`); throw new Error(`No file in ${process.cwd()} matched to [${pattern}], make sure you have checked out the target repository`);
} }
return `${CACHE_KEY_PREFIX}-${process.env['RUNNER_OS']}-${process.arch}-${packageManager.id}-${fileHash}`; return buildCacheKey(packageManager.id, fileHash);
}
/**
* Computes the cache key for an additional cache. Unlike {@link computeCacheKey}
* this returns undefined (instead of throwing) when no file matches the pattern,
* because additional caches are optional features that many projects do not use.
*/
async function computeAdditionalCacheKey(additionalCache) {
const fileHash = await glob.hashFiles(additionalCache.pattern.join('\n'));
if (!fileHash) {
return undefined;
}
return buildCacheKey(additionalCache.name, fileHash);
} }
/** /**
* Restore the dependency cache * Restore the dependency cache
@@ -99687,6 +99792,7 @@ async function restore(id, cacheDependencyPath) {
const primaryKey = await computeCacheKey(packageManager, cacheDependencyPath); const primaryKey = await computeCacheKey(packageManager, cacheDependencyPath);
core.debug(`primary key is ${primaryKey}`); core.debug(`primary key is ${primaryKey}`);
core.saveState(STATE_CACHE_PRIMARY_KEY, primaryKey); core.saveState(STATE_CACHE_PRIMARY_KEY, primaryKey);
core.setOutput(STATE_CACHE_PRIMARY_KEY, primaryKey);
// No "restoreKeys" is set, to start with a clear cache after dependency update (see https://github.com/actions/setup-java/issues/269) // 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(packageManager.path, primaryKey);
if (matchedKey) { if (matchedKey) {
@@ -99698,6 +99804,29 @@ async function restore(id, cacheDependencyPath) {
core.setOutput('cache-hit', false); core.setOutput('cache-hit', false);
core.info(`${packageManager.id} cache is not found`); core.info(`${packageManager.id} cache is not found`);
} }
for (const additionalCache of packageManager.additionalCaches ?? []) {
await restoreAdditionalCache(additionalCache);
}
}
/**
* Restore an additional cache (e.g. a build-tool wrapper distribution) that is
* keyed independently of the main dependency cache so that it survives changes
* to volatile dependency files. Skips silently when the project does not use
* the corresponding feature.
*/
async function restoreAdditionalCache(additionalCache) {
const primaryKey = await computeAdditionalCacheKey(additionalCache);
if (!primaryKey) {
core.debug(`No file matched [${additionalCache.pattern}] for the ${additionalCache.name} cache, skipping.`);
return;
}
core.debug(`${additionalCache.name} primary key is ${primaryKey}`);
core.saveState(additionalCachePrimaryKeyState(additionalCache.name), primaryKey);
const matchedKey = await cache.restoreCache(additionalCache.path, primaryKey);
if (matchedKey) {
core.saveState(additionalCacheMatchedKeyState(additionalCache.name), matchedKey);
core.info(`${additionalCache.name} cache restored from key: ${matchedKey}`);
}
} }
/** /**
* Save the dependency cache * Save the dependency cache
@@ -99708,6 +99837,9 @@ async function save(id) {
const matchedKey = getState(CACHE_MATCHED_KEY); const matchedKey = getState(CACHE_MATCHED_KEY);
// Inputs are re-evaluated before the post action, so we want the original key used for restore // Inputs are re-evaluated before the post action, so we want the original key used for restore
const primaryKey = getState(STATE_CACHE_PRIMARY_KEY); const primaryKey = getState(STATE_CACHE_PRIMARY_KEY);
for (const additionalCache of packageManager.additionalCaches ?? []) {
await saveAdditionalCache(packageManager, additionalCache);
}
if (!primaryKey) { if (!primaryKey) {
warning('Error retrieving key from state.'); warning('Error retrieving key from state.');
return; return;
@@ -99742,6 +99874,50 @@ async function save(id) {
} }
} }
} }
/**
* Save an additional cache under its own key. Skips when no key was recorded at
* restore time (feature unused) or when the exact key was already restored.
*/
async function saveAdditionalCache(packageManager, additionalCache) {
const primaryKey = getState(additionalCachePrimaryKeyState(additionalCache.name));
const matchedKey = getState(additionalCacheMatchedKeyState(additionalCache.name));
if (!primaryKey) {
// The feature is not used by this project, nothing to save.
core_debug(`No primary key for the ${additionalCache.name} cache, not saving cache.`);
return;
}
else if (matchedKey === primaryKey) {
info(`Cache hit occurred on the ${additionalCache.name} primary key ${primaryKey}, not saving cache.`);
return;
}
try {
const cacheId = await cache_saveCache(additionalCache.path, primaryKey);
if (cacheId === -1) {
core_debug(`${additionalCache.name} cache was not saved for the key: ${primaryKey}`);
return;
}
info(`${additionalCache.name} cache saved with the key: ${primaryKey}`);
}
catch (error) {
const err = error;
if (err.name === ValidationError.name) {
// The cache paths did not resolve, e.g. the wrapper distribution was
// never downloaded because a system build tool was used or the download
// failed. Optional wrapper caches must not fail the post step, so skip.
core_debug(`${additionalCache.name} cache paths do not exist, not saving cache: ${err.message}`);
return;
}
if (err.name === ReserveCacheError.name) {
info(err.message);
}
else {
if (isProbablyGradleDaemonProblem(packageManager, err)) {
warning('Failed to save Gradle cache on Windows. If tar.exe reported "Permission denied", try to run Gradle with `--no-daemon` option. Refer to https://github.com/actions/cache/issues/454 for details.');
}
throw error;
}
}
}
/** /**
* @param packageManager the specified package manager by user * @param packageManager the specified package manager by user
* @param error the error thrown by the saveCache * @param error the error thrown by the saveCache

311
dist/setup/index.js vendored
View File

@@ -73279,7 +73279,8 @@ const INPUT_JAVA_VERSION_FILE = 'java-version-file';
const INPUT_ARCHITECTURE = 'architecture'; const INPUT_ARCHITECTURE = 'architecture';
const INPUT_JAVA_PACKAGE = 'java-package'; const INPUT_JAVA_PACKAGE = 'java-package';
const INPUT_DISTRIBUTION = 'distribution'; const INPUT_DISTRIBUTION = 'distribution';
const INPUT_JDK_FILE = 'jdkFile'; const INPUT_JDK_FILE = 'jdk-file';
const INPUT_JDK_FILE_DEPRECATED = 'jdkFile';
const INPUT_CHECK_LATEST = 'check-latest'; const INPUT_CHECK_LATEST = 'check-latest';
const INPUT_SET_DEFAULT = 'set-default'; const INPUT_SET_DEFAULT = 'set-default';
const INPUT_VERIFY_SIGNATURE = 'verify-signature'; const INPUT_VERIFY_SIGNATURE = 'verify-signature';
@@ -126957,8 +126958,10 @@ function getVersionFromFileContent(content, distributionName, versionFile) {
} }
const versionFileName = getFileName(versionFile); const versionFileName = getFileName(versionFile);
if (versionFileName == '.tool-versions') { if (versionFileName == '.tool-versions') {
// Capture an optional asdf-java vendor prefix (e.g. `temurin-`, `corretto-`)
// in the `distribution` group so it can be mapped to a setup-java distribution.
javaVersionRegExp = javaVersionRegExp =
/^java\s+(?:\S*-)?(?<version>\d+(?:\.\d+)*([+_.-](?:openj9[-._]?\d[\w.-]*|java\d+|jre[-_\w]*|OpenJDK\d+[\w_.-]*|[a-z0-9]+))*)/im; /^java\s+(?:(?<distribution>\S*)-)?(?<version>\d+(?:\.\d+)*([+_.-](?:openj9[-._]?\d[\w.-]*|java\d+|jre[-_\w]*|OpenJDK\d+[\w_.-]*|[a-z0-9]+))*)/im;
} }
else if (versionFileName == '.sdkmanrc') { else if (versionFileName == '.sdkmanrc') {
// Match both version and optional distribution identifier // Match both version and optional distribution identifier
@@ -126978,6 +126981,14 @@ function getVersionFromFileContent(content, distributionName, versionFile) {
extractedDistribution = mapSdkmanDistribution(sdkmanDist); extractedDistribution = mapSdkmanDistribution(sdkmanDist);
core_debug(`Parsed distribution '${extractedDistribution}' from SDKMAN identifier '${sdkmanDist}'`); core_debug(`Parsed distribution '${extractedDistribution}' from SDKMAN identifier '${sdkmanDist}'`);
} }
// Extract distribution from asdf .tool-versions file
if (versionFileName == '.tool-versions' && match?.groups?.distribution) {
const asdfDist = match.groups.distribution;
extractedDistribution = mapAsdfDistribution(asdfDist);
if (extractedDistribution) {
core_debug(`Parsed distribution '${extractedDistribution}' from asdf identifier '${asdfDist}'`);
}
}
core_debug(`Parsed version '${capturedVersion}' from file '${versionFileName}'`); core_debug(`Parsed version '${capturedVersion}' from file '${versionFileName}'`);
if (!capturedVersion) { if (!capturedVersion) {
return null; return null;
@@ -127026,6 +127037,43 @@ function mapSdkmanDistribution(sdkmanDist) {
} }
return mapped; return mapped;
} }
// Map asdf-java (.tool-versions) vendor identifiers to setup-java distribution names.
// asdf-java encodes the vendor as a prefix on the version string, e.g.
// `java temurin-17.0.3+7` or `java semeru-openj9-11.0.25+9`. Packaging variants
// (`-jre`, `-musl`, `-openj9`, `-crac`, `-javafx`, ...) are collapsed onto the
// base vendor since setup-java does not distinguish them here.
function mapAsdfDistribution(asdfDist) {
const normalized = asdfDist.toLowerCase();
// Multi-segment vendors that map to a distinct setup-java distribution.
if (normalized.startsWith('graalvm-community')) {
return 'graalvm-community';
}
if (normalized.startsWith('oracle-graalvm')) {
return 'graalvm';
}
const baseVendor = normalized.split('-')[0];
const distributionMap = {
temurin: 'temurin',
adoptopenjdk: 'temurin',
zulu: 'zulu',
corretto: 'corretto',
liberica: 'liberica',
microsoft: 'microsoft',
semeru: 'semeru',
ibm: 'semeru',
dragonwell: 'dragonwell',
graalvm: 'graalvm',
oracle: 'oracle',
sapmachine: 'sapmachine',
kona: 'kona',
jetbrains: 'jetbrains'
};
const mapped = distributionMap[baseVendor];
if (!mapped) {
warning(`Unknown asdf distribution identifier '${asdfDist}'. Please specify the distribution explicitly.`);
}
return mapped;
}
// By convention, action expects version 8 in the format `8.*` instead of `1.8` // By convention, action expects version 8 in the format `8.*` instead of `1.8`
function avoidOldNotation(content) { function avoidOldNotation(content) {
return content.startsWith('1.') ? content.substring(2) : content; return content.startsWith('1.') ? content.substring(2) : content;
@@ -127098,6 +127146,22 @@ function renameWinArchive(javaArchivePath) {
external_fs_namespaceObject.renameSync(javaArchivePath, javaArchivePathRenamed); external_fs_namespaceObject.renameSync(javaArchivePath, javaArchivePathRenamed);
return javaArchivePathRenamed; return javaArchivePathRenamed;
} }
// Resolve the newest available stable/GA feature (major) release.
//
// Some distributions (e.g. Oracle, GraalVM) construct their download URLs from a
// concrete major version and don't expose an endpoint to list every available
// release, so a bare `latest` alias can't be resolved from their own metadata.
// The Adoptium (Temurin) API is used as a proxy for "what is the newest GA major
// version out there", which those distributions typically publish at the same time.
async function getLatestMajorVersion(http) {
const availableReleasesUrl = 'https://api.adoptium.net/v3/info/available_releases';
const response = await http.getJson(availableReleasesUrl);
const mostRecent = response.result?.most_recent_feature_release;
if (!mostRecent || Number.isNaN(Number(mostRecent))) {
throw new Error(`Could not determine the latest available Java major version from ${availableReleasesUrl}`);
}
return Number(mostRecent);
}
;// CONCATENATED MODULE: ./src/gpg.ts ;// CONCATENATED MODULE: ./src/gpg.ts
@@ -130824,23 +130888,28 @@ const CACHE_KEY_PREFIX = 'setup-java';
const supportedPackageManager = [ const supportedPackageManager = [
{ {
id: 'maven', id: 'maven',
path: [ path: [(0,external_path_.join)(external_os_default().homedir(), '.m2', 'repository')],
(0,external_path_.join)(external_os_default().homedir(), '.m2', 'repository'),
(0,external_path_.join)(external_os_default().homedir(), '.m2', 'wrapper', 'dists')
],
// https://github.com/actions/cache/blob/0638051e9af2c23d10bb70fa9beffcad6cff9ce3/examples.md#java---maven // https://github.com/actions/cache/blob/0638051e9af2c23d10bb70fa9beffcad6cff9ce3/examples.md#java---maven
pattern: [ pattern: [
'**/pom.xml', '**/pom.xml',
'**/.mvn/wrapper/maven-wrapper.properties', '**/.mvn/wrapper/maven-wrapper.properties',
'**/.mvn/extensions.xml' '**/.mvn/extensions.xml'
],
// The Maven wrapper distribution only depends on the wrapper properties,
// which change very rarely, so it is cached separately from the local
// repository. This keeps it available across the frequent pom.xml changes
// that rotate the main cache key. See issue #1095.
additionalCaches: [
{
name: 'maven-wrapper',
path: [(0,external_path_.join)(external_os_default().homedir(), '.m2', 'wrapper', 'dists')],
pattern: ['**/.mvn/wrapper/maven-wrapper.properties']
}
] ]
}, },
{ {
id: 'gradle', id: 'gradle',
path: [ path: [(0,external_path_.join)(external_os_default().homedir(), '.gradle', 'caches')],
(0,external_path_.join)(external_os_default().homedir(), '.gradle', 'caches'),
(0,external_path_.join)(external_os_default().homedir(), '.gradle', 'wrapper')
],
// https://github.com/actions/cache/blob/0638051e9af2c23d10bb70fa9beffcad6cff9ce3/examples.md#java---gradle // https://github.com/actions/cache/blob/0638051e9af2c23d10bb70fa9beffcad6cff9ce3/examples.md#java---gradle
pattern: [ pattern: [
'**/*.gradle*', '**/*.gradle*',
@@ -130849,6 +130918,17 @@ const supportedPackageManager = [
'buildSrc/**/Dependencies.kt', 'buildSrc/**/Dependencies.kt',
'gradle/*.versions.toml', 'gradle/*.versions.toml',
'**/versions.properties' '**/versions.properties'
],
// The Gradle wrapper distribution only depends on the wrapper properties,
// which change very rarely, so it is cached separately from the Gradle
// caches. This keeps it available across the frequent *.gradle* changes
// that rotate the main cache key. See issue #269.
additionalCaches: [
{
name: 'gradle-wrapper',
path: [(0,external_path_.join)(external_os_default().homedir(), '.gradle', 'wrapper')],
pattern: ['**/gradle-wrapper.properties']
}
] ]
}, },
{ {
@@ -130884,6 +130964,19 @@ function findPackageManager(id) {
} }
return packageManager; return packageManager;
} }
/**
* State keys used to carry an additional cache's restore-time information over
* to the post (save) action, scoped by the additional cache name.
*/
function additionalCachePrimaryKeyState(name) {
return `${STATE_CACHE_PRIMARY_KEY}-${name}`;
}
function additionalCacheMatchedKeyState(name) {
return `${CACHE_MATCHED_KEY}-${name}`;
}
function buildCacheKey(id, fileHash) {
return `${CACHE_KEY_PREFIX}-${process.env['RUNNER_OS']}-${process.arch}-${id}-${fileHash}`;
}
/** /**
* A function that generates a cache key to use. * A function that generates a cache key to use.
* Format of the generated key will be "${{ platform }}-${{ id }}-${{ fileHash }}"". * Format of the generated key will be "${{ platform }}-${{ id }}-${{ fileHash }}"".
@@ -130897,7 +130990,19 @@ async function computeCacheKey(packageManager, cacheDependencyPath) {
if (!fileHash) { if (!fileHash) {
throw new Error(`No file in ${process.cwd()} matched to [${pattern}], make sure you have checked out the target repository`); throw new Error(`No file in ${process.cwd()} matched to [${pattern}], make sure you have checked out the target repository`);
} }
return `${CACHE_KEY_PREFIX}-${process.env['RUNNER_OS']}-${process.arch}-${packageManager.id}-${fileHash}`; return buildCacheKey(packageManager.id, fileHash);
}
/**
* Computes the cache key for an additional cache. Unlike {@link computeCacheKey}
* this returns undefined (instead of throwing) when no file matches the pattern,
* because additional caches are optional features that many projects do not use.
*/
async function computeAdditionalCacheKey(additionalCache) {
const fileHash = await lib_glob_hashFiles(additionalCache.pattern.join('\n'));
if (!fileHash) {
return undefined;
}
return buildCacheKey(additionalCache.name, fileHash);
} }
/** /**
* Restore the dependency cache * Restore the dependency cache
@@ -130909,6 +131014,7 @@ async function restore(id, cacheDependencyPath) {
const primaryKey = await computeCacheKey(packageManager, cacheDependencyPath); const primaryKey = await computeCacheKey(packageManager, cacheDependencyPath);
core_debug(`primary key is ${primaryKey}`); core_debug(`primary key is ${primaryKey}`);
saveState(STATE_CACHE_PRIMARY_KEY, primaryKey); saveState(STATE_CACHE_PRIMARY_KEY, primaryKey);
setOutput(STATE_CACHE_PRIMARY_KEY, primaryKey);
// No "restoreKeys" is set, to start with a clear cache after dependency update (see https://github.com/actions/setup-java/issues/269) // 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 restoreCache(packageManager.path, primaryKey); const matchedKey = await restoreCache(packageManager.path, primaryKey);
if (matchedKey) { if (matchedKey) {
@@ -130920,6 +131026,29 @@ async function restore(id, cacheDependencyPath) {
setOutput('cache-hit', false); setOutput('cache-hit', false);
info(`${packageManager.id} cache is not found`); info(`${packageManager.id} cache is not found`);
} }
for (const additionalCache of packageManager.additionalCaches ?? []) {
await restoreAdditionalCache(additionalCache);
}
}
/**
* Restore an additional cache (e.g. a build-tool wrapper distribution) that is
* keyed independently of the main dependency cache so that it survives changes
* to volatile dependency files. Skips silently when the project does not use
* the corresponding feature.
*/
async function restoreAdditionalCache(additionalCache) {
const primaryKey = await computeAdditionalCacheKey(additionalCache);
if (!primaryKey) {
core_debug(`No file matched [${additionalCache.pattern}] for the ${additionalCache.name} cache, skipping.`);
return;
}
core_debug(`${additionalCache.name} primary key is ${primaryKey}`);
saveState(additionalCachePrimaryKeyState(additionalCache.name), primaryKey);
const matchedKey = await restoreCache(additionalCache.path, primaryKey);
if (matchedKey) {
saveState(additionalCacheMatchedKeyState(additionalCache.name), matchedKey);
info(`${additionalCache.name} cache restored from key: ${matchedKey}`);
}
} }
/** /**
* Save the dependency cache * Save the dependency cache
@@ -130930,6 +131059,9 @@ async function save(id) {
const matchedKey = core.getState(CACHE_MATCHED_KEY); const matchedKey = core.getState(CACHE_MATCHED_KEY);
// Inputs are re-evaluated before the post action, so we want the original key used for restore // Inputs are re-evaluated before the post action, so we want the original key used for restore
const primaryKey = core.getState(STATE_CACHE_PRIMARY_KEY); const primaryKey = core.getState(STATE_CACHE_PRIMARY_KEY);
for (const additionalCache of packageManager.additionalCaches ?? []) {
await saveAdditionalCache(packageManager, additionalCache);
}
if (!primaryKey) { if (!primaryKey) {
core.warning('Error retrieving key from state.'); core.warning('Error retrieving key from state.');
return; return;
@@ -130964,6 +131096,50 @@ async function save(id) {
} }
} }
} }
/**
* Save an additional cache under its own key. Skips when no key was recorded at
* restore time (feature unused) or when the exact key was already restored.
*/
async function saveAdditionalCache(packageManager, additionalCache) {
const primaryKey = core.getState(additionalCachePrimaryKeyState(additionalCache.name));
const matchedKey = core.getState(additionalCacheMatchedKeyState(additionalCache.name));
if (!primaryKey) {
// The feature is not used by this project, nothing to save.
core.debug(`No primary key for the ${additionalCache.name} cache, not saving cache.`);
return;
}
else if (matchedKey === primaryKey) {
core.info(`Cache hit occurred on the ${additionalCache.name} primary key ${primaryKey}, not saving cache.`);
return;
}
try {
const cacheId = await cache.saveCache(additionalCache.path, primaryKey);
if (cacheId === -1) {
core.debug(`${additionalCache.name} cache was not saved for the key: ${primaryKey}`);
return;
}
core.info(`${additionalCache.name} cache saved with the key: ${primaryKey}`);
}
catch (error) {
const err = error;
if (err.name === cache.ValidationError.name) {
// The cache paths did not resolve, e.g. the wrapper distribution was
// never downloaded because a system build tool was used or the download
// failed. Optional wrapper caches must not fail the post step, so skip.
core.debug(`${additionalCache.name} cache paths do not exist, not saving cache: ${err.message}`);
return;
}
if (err.name === cache.ReserveCacheError.name) {
core.info(err.message);
}
else {
if (isProbablyGradleDaemonProblem(packageManager, err)) {
core.warning('Failed to save Gradle cache on Windows. If tar.exe reported "Permission denied", try to run Gradle with `--no-daemon` option. Refer to https://github.com/actions/cache/issues/454 for details.');
}
throw error;
}
}
}
/** /**
* @param packageManager the specified package manager by user * @param packageManager the specified package manager by user
* @param error the error thrown by the saveCache * @param error the error thrown by the saveCache
@@ -130996,6 +131172,7 @@ class JavaBase {
architecture; architecture;
packageType; packageType;
stable; stable;
latest;
checkLatest; checkLatest;
setDefault; setDefault;
verifySignature; verifySignature;
@@ -131006,7 +131183,11 @@ class JavaBase {
allowRetries: true, allowRetries: true,
maxRetries: 3 maxRetries: 3
}); });
({ version: this.version, stable: this.stable } = this.normalizeVersion(installerOptions.version)); ({
version: this.version,
stable: this.stable,
latest: this.latest
} = this.normalizeVersion(installerOptions.version));
this.architecture = installerOptions.architecture || external_os_default().arch(); this.architecture = installerOptions.architecture || external_os_default().arch();
this.packageType = installerOptions.packageType; this.packageType = installerOptions.packageType;
this.checkLatest = installerOptions.checkLatest; this.checkLatest = installerOptions.checkLatest;
@@ -131022,7 +131203,7 @@ class JavaBase {
throw new Error(`Input 'verify-signature' is not supported for distribution '${this.distribution}'.`); throw new Error(`Input 'verify-signature' is not supported for distribution '${this.distribution}'.`);
} }
let foundJava = this.findInToolcache(); let foundJava = this.findInToolcache();
if (foundJava && !this.checkLatest) { if (foundJava && !this.checkLatest && !this.latest) {
info(`Resolved Java ${foundJava.version} from tool-cache`); info(`Resolved Java ${foundJava.version} from tool-cache`);
} }
else { else {
@@ -131200,6 +131381,26 @@ class JavaBase {
} }
normalizeVersion(version) { normalizeVersion(version) {
let stable = true; let stable = true;
const latest = false;
// Support the `latest` alias (case-insensitive), which floats to the newest
// available stable/GA release. It is translated to the SemVer wildcard `x`
// so the existing "newest satisfying version wins" resolution applies.
const normalized = version.trim().toLowerCase();
if (normalized === 'latest') {
return {
version: 'x',
stable: true,
latest: true
};
}
// Reject `latest` combined with any qualifier (e.g. `latest-ea`). Such inputs
// would otherwise have their `-ea` suffix stripped and fall through to the
// generic SemVer check, which fails with a confusing "'latest' is not valid
// SemVer" message even though `latest` is a supported value. Fail early with a
// targeted explanation instead.
if (normalized.startsWith('latest')) {
throw new Error(`The 'latest' alias resolves stable (GA) releases only and cannot be combined with '-ea' or other qualifiers (received '${version}'). Use 'latest' on its own, or specify a concrete version.`);
}
if (version.endsWith('-ea')) { if (version.endsWith('-ea')) {
version = version.replace(/-ea$/, ''); version = version.replace(/-ea$/, '');
stable = false; stable = false;
@@ -131209,12 +131410,21 @@ 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`);
} }
return { return {
version, version,
stable stable,
latest
}; };
} }
createVersionNotFoundError(versionOrRange, availableVersions, additionalContext) { createVersionNotFoundError(versionOrRange, availableVersions, additionalContext) {
@@ -131288,6 +131498,9 @@ class LocalDistribution extends JavaBase {
this.jdkFile = jdkFile; this.jdkFile = jdkFile;
} }
async setupJava() { async setupJava() {
if (this.latest) {
throw new Error("The 'latest' version alias is not supported for the 'jdkfile' distribution. Please specify a concrete version.");
}
let foundJava = this.findInToolcache(); let foundJava = this.findInToolcache();
if (foundJava) { if (foundJava) {
info(`Resolved Java ${foundJava.version} from tool-cache`); info(`Resolved Java ${foundJava.version} from tool-cache`);
@@ -132303,10 +132516,22 @@ class CorrettoDistribution extends JavaBase {
if (!this.stable) { if (!this.stable) {
throw new Error('Early access versions are not supported'); throw new Error('Early access versions are not supported');
} }
const availableVersions = await this.getAvailableVersions();
// The `latest` alias is normalized to the SemVer wildcard, but Corretto
// matches on an exact major version, so resolve it to the newest available
// major from Corretto's own list.
if (this.latest) {
const majors = availableVersions
.map(item => parseInt(item.version, 10))
.filter(major => Number.isFinite(major) && major > 0);
if (majors.length === 0) {
throw new Error('Could not determine the latest available Corretto major version from remote metadata');
}
version = Math.max(...majors).toString();
}
if (version.includes('.')) { if (version.includes('.')) {
throw new Error('Only major versions are supported'); throw new Error('Only major versions are supported');
} }
const availableVersions = await this.getAvailableVersions();
const matchingVersions = availableVersions const matchingVersions = availableVersions
.filter(item => item.version == version) .filter(item => item.version == version)
.map(item => { .map(item => {
@@ -132434,6 +132659,13 @@ class OracleDistribution extends JavaBase {
} }
const platform = this.getPlatform(); const platform = this.getPlatform();
const extension = getDownloadArchiveExtension(); const extension = getDownloadArchiveExtension();
// The `latest` alias is normalized to the SemVer wildcard. Oracle builds its
// download URLs from a concrete major and has no endpoint to list releases,
// so resolve the newest available GA major from the Adoptium API and use it.
if (this.latest) {
const latestMajor = await getLatestMajorVersion(this.http);
range = latestMajor.toString();
}
const isOnlyMajorProvided = !range.includes('.'); const isOnlyMajorProvided = !range.includes('.');
const major = isOnlyMajorProvided ? range : range.split('.')[0]; const major = isOnlyMajorProvided ? range : range.split('.')[0];
const possibleUrls = []; const possibleUrls = [];
@@ -132460,6 +132692,11 @@ class OracleDistribution extends JavaBase {
throw new Error(`Http request for Oracle JDK failed with status code: ${response.message.statusCode}`); throw new Error(`Http request for Oracle JDK failed with status code: ${response.message.statusCode}`);
} }
} }
if (this.latest) {
const error = this.createVersionNotFoundError(range);
error.message += `\nThe latest Java major version (${range}) is not yet available for the Oracle JDK distribution. Please specify a concrete version instead of 'latest'.`;
throw error;
}
throw this.createVersionNotFoundError(range); throw this.createVersionNotFoundError(range);
} }
getPlatform(platform = process.platform) { getPlatform(platform = process.platform) {
@@ -132858,6 +133095,12 @@ class GraalVMDistribution extends JavaBase {
if (!this.stable) { if (!this.stable) {
return this.findEABuildDownloadUrl(`${range}-ea`); return this.findEABuildDownloadUrl(`${range}-ea`);
} }
// The `latest` alias is normalized to the SemVer wildcard. Oracle GraalVM
// builds its download URLs from a concrete major and has no endpoint to list
// releases, so resolve the newest available GA major from the Adoptium API.
if (this.latest) {
range = (await getLatestMajorVersion(this.http)).toString();
}
const { platform, extension, major } = this.validateStableBuildRequest(range); const { platform, extension, major } = this.validateStableBuildRequest(range);
const fileUrl = this.constructFileUrl(range, major, platform, arch, extension); const fileUrl = this.constructFileUrl(range, major, platform, arch, extension);
const response = await this.http.head(fileUrl); const response = await this.http.head(fileUrl);
@@ -132906,6 +133149,9 @@ class GraalVMDistribution extends JavaBase {
if (statusCode === HttpCodes.NotFound) { if (statusCode === HttpCodes.NotFound) {
// Create the standard error with additional hint about checking the download URL // Create the standard error with additional hint about checking the download URL
const error = this.createVersionNotFoundError(range); const error = this.createVersionNotFoundError(range);
if (this.latest) {
error.message += `\nThe latest Java major version (${range}) is not yet available for the ${this.distribution} distribution. Please specify a concrete version instead of 'latest'.`;
}
error.message += `\nPlease check if this version is available at ${GRAALVM_DOWNLOAD_URL} . Pick a version from the list.`; error.message += `\nPlease check if this version is available at ${GRAALVM_DOWNLOAD_URL} . Pick a version from the list.`;
throw error; throw error;
} }
@@ -132993,7 +133239,24 @@ class GraalVMCommunityDistribution extends GraalVMDistribution {
throw new Error('GraalVM Community does not provide early access builds'); throw new Error('GraalVM Community does not provide early access builds');
} }
const arch = this.getSupportedArchitecture(); const arch = this.getSupportedArchitecture();
const { platform, extension } = this.validateStableBuildRequest(range); // GraalVM Community publishes its releases on GitHub, so the `latest` alias
// (normalized to the SemVer wildcard `x`) can float to the newest GA it
// actually ships. Unlike Oracle GraalVM (which has no listing endpoint and
// must derive the newest major from the Adoptium API), we match against the
// real release list here, so `latest` never fails when GraalVM lags behind a
// brand-new Java major.
let platform;
let extension;
if (this.latest) {
if (this.packageType !== 'jdk') {
throw new Error(`${this.distribution} provides only the \`jdk\` package type`);
}
platform = this.getPlatform();
extension = getDownloadArchiveExtension();
}
else {
({ platform, extension } = this.validateStableBuildRequest(range));
}
// GraalVM Community asset names embed the platform, architecture and // GraalVM Community asset names embed the platform, architecture and
// archive type, e.g. `graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz`. // archive type, e.g. `graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz`.
const assetSuffix = `_${platform}-${arch}_bin.${extension}`; const assetSuffix = `_${platform}-${arch}_bin.${extension}`;
@@ -133513,7 +133776,7 @@ async function run() {
const versionFile = getInput(INPUT_JAVA_VERSION_FILE); const versionFile = getInput(INPUT_JAVA_VERSION_FILE);
const architecture = getInput(INPUT_ARCHITECTURE); const architecture = getInput(INPUT_ARCHITECTURE);
const packageType = getInput(INPUT_JAVA_PACKAGE); const packageType = getInput(INPUT_JAVA_PACKAGE);
const jdkFile = getInput(INPUT_JDK_FILE); const jdkFile = getJdkFileInput();
const cache = getInput(INPUT_CACHE); const cache = getInput(INPUT_CACHE);
const cacheDependencyPath = getInput(INPUT_CACHE_DEPENDENCY_PATH); const cacheDependencyPath = getInput(INPUT_CACHE_DEPENDENCY_PATH);
const checkLatest = util_getBooleanInput(INPUT_CHECK_LATEST, false); const checkLatest = util_getBooleanInput(INPUT_CHECK_LATEST, false);
@@ -133591,6 +133854,14 @@ async function run() {
} }
} }
run(); run();
function getJdkFileInput() {
const jdkFile = getInput(INPUT_JDK_FILE);
const deprecatedJdkFile = getInput(INPUT_JDK_FILE_DEPRECATED);
if (deprecatedJdkFile) {
warning(`The '${INPUT_JDK_FILE_DEPRECATED}' input is deprecated and may be removed in a future release. Please use '${INPUT_JDK_FILE}' instead.`);
}
return jdkFile || deprecatedJdkFile;
}
async function installVersion(version, options, toolchainId = 0) { async function installVersion(version, options, toolchainId = 0) {
const { distributionName, jdkFile, architecture, packageType, checkLatest, setDefault, verifySignature, verifySignaturePublicKey, toolchainIds } = options; const { distributionName, jdkFile, architecture, packageType, checkLatest, setDefault, verifySignature, verifySignaturePublicKey, toolchainIds } = options;
const installerOptions = { const installerOptions = {
@@ -133607,7 +133878,11 @@ async function installVersion(version, options, toolchainId = 0) {
throw new Error(`No supported distribution was found for input ${distributionName}`); throw new Error(`No supported distribution was found for input ${distributionName}`);
} }
const result = await distribution.setupJava(); const result = await distribution.setupJava();
await configureToolchains(version, distributionName, result.path, toolchainIds[toolchainId]); // When the `latest` alias is used, the literal input isn't a real version, so
// pass the resolved version to the toolchains configuration instead.
const isLatest = version.trim().toLowerCase() === 'latest';
const toolchainVersion = isLatest ? result.version : version;
await configureToolchains(toolchainVersion, distributionName, result.path, toolchainIds[toolchainId]);
info(''); info('');
info('Java configuration:'); info('Java configuration:');
info(` Distribution: ${distributionName}`); info(` Distribution: ${distributionName}`);

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,129 @@ 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`). 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*).
Downloaded Maven Wrapper distributions (`~/.m2/wrapper/dists`) are cached in a
**separate** cache entry keyed only on `**/.mvn/wrapper/maven-wrapper.properties`.
Because the wrapper distribution changes far less often than your `pom.xml`
files, this keeps it available across the frequent dependency changes that
rotate the main cache key, so wrapper-based (`./mvnw`) builds don't re-download
the Maven distribution on every dependency change. See
[issue #1095](https://github.com/actions/setup-java/issues/1095).
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
@@ -330,7 +454,7 @@ In this example, `JAVA_HOME` and `java` on `PATH` point to Java 17, while Java 2
If your use-case requires a custom distribution or a version that is not provided by setup-java, you can download it manually and setup-java will take care of the installation and caching on the VM: If your use-case requires a custom distribution or a version that is not provided by setup-java, you can download it manually and setup-java will take care of the installation and caching on the VM:
> [!NOTE] > [!NOTE]
> This approach also lets you use builds that setup-java does not provide directly, such as **Early Access (EA)** or other unreleased JDK builds (for example, an upcoming feature release or a Loom/Valhalla preview build). Download the desired archive in a prior step and point `jdkFile` at it; setup-java will extract, install, and cache it just like a supported distribution. When targeting multiple architectures, select the correct binary per architecture in your workflow (for example, with a build matrix). > This approach also lets you use builds that setup-java does not provide directly, such as **Early Access (EA)** or other unreleased JDK builds (for example, an upcoming feature release or a Loom/Valhalla preview build). Download the desired archive in a prior step and point `jdk-file` at it; setup-java will extract, install, and cache it just like a supported distribution. When targeting multiple architectures, select the correct binary per architecture in your workflow (for example, with a build matrix).
```yaml ```yaml
steps: steps:
@@ -340,7 +464,7 @@ steps:
- uses: actions/setup-java@v5 - uses: actions/setup-java@v5
with: with:
distribution: 'jdkfile' distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz jdk-file: ${{ runner.temp }}/java_package.tar.gz
java-version: '11.0.0' java-version: '11.0.0'
architecture: x64 architecture: x64
@@ -357,7 +481,7 @@ steps:
- uses: actions/setup-java@v5 - uses: actions/setup-java@v5
with: with:
distribution: 'jdkfile' distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz jdk-file: ${{ runner.temp }}/java_package.tar.gz
java-version: '25.0.0-ea.36' java-version: '25.0.0-ea.36'
architecture: x64 architecture: x64
@@ -383,7 +507,7 @@ If your use-case requires a custom distribution (in the example, alpine-linux is
- uses: actions/setup-java@v5 - uses: actions/setup-java@v5
with: with:
distribution: 'jdkfile' distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz jdk-file: ${{ runner.temp }}/java_package.tar.gz
java-version: {{ steps.fetch_latest_jdk.outputs.java_version }} java-version: {{ steps.fetch_latest_jdk.outputs.java_version }}
architecture: x64 architecture: x64
- run: java --version - run: java --version
@@ -708,7 +832,7 @@ The result is a Toolchain with entries for JDKs 8, 11 and 15. You can even combi
- uses: actions/setup-java@v5 - uses: actions/setup-java@v5
with: with:
distribution: 'jdkfile' distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz jdk-file: ${{ runner.temp }}/java_package.tar.gz
java-version: '1.6' java-version: '1.6'
architecture: x64 architecture: x64
``` ```
@@ -725,7 +849,7 @@ Each JDK provider will receive a default `vendor` using the `distribution` input
- uses: actions/setup-java@v5 - uses: actions/setup-java@v5
with: with:
distribution: 'jdkfile' distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz jdk-file: ${{ runner.temp }}/java_package.tar.gz
java-version: '1.6' java-version: '1.6'
architecture: x64 architecture: x64
mvn-toolchain-vendor: 'Oracle' mvn-toolchain-vendor: 'Oracle'
@@ -780,7 +904,27 @@ steps:
Supported files are `.java-version`, `.tool-versions` and `.sdkmanrc`. Supported files are `.java-version`, `.tool-versions` and `.sdkmanrc`.
* In `.java-version` file, only the version should be specified (e.g., 17.0.7). The `.java-version` file recognizes all variants of the version description according to [jenv](https://github.com/jenv/jenv). * In `.java-version` file, only the version should be specified (e.g., 17.0.7). The `.java-version` file recognizes all variants of the version description according to [jenv](https://github.com/jenv/jenv).
* In `.tool-versions` file, java version should be preceded by the java keyword (e.g., java 17.0.7). The `.tool-versions` file supports version specifications in accordance with [asdf](https://github.com/asdf-vm/asdf) standards, adhering to Semantic Versioning ([semver](https://semver.org/)). * In `.tool-versions` file, java version should be preceded by the java keyword (e.g., java 17.0.7). The `.tool-versions` file supports version specifications in accordance with [asdf](https://github.com/asdf-vm/asdf) standards, adhering to Semantic Versioning ([semver](https://semver.org/)). When the entry includes an [asdf-java](https://github.com/halcyon/asdf-java) vendor prefix (e.g. `java temurin-17.0.3+7`), setup-java can infer the `distribution` input automatically. Unrecognized vendor prefixes require setting `distribution` explicitly.
Supported asdf-java vendor prefix mappings (packaging variants such as `-jre`, `-musl`, `-openj9`, `-crac`, `-javafx` are collapsed onto the base vendor):
| asdf-java vendor prefix | setup-java distribution |
| ----------------------- | ----------------------- |
| `temurin` | `temurin` |
| `adoptopenjdk` | `temurin` |
| `zulu` | `zulu` |
| `corretto` | `corretto` |
| `liberica` | `liberica` |
| `microsoft` | `microsoft` |
| `semeru`, `ibm` | `semeru` |
| `dragonwell` | `dragonwell` |
| `graalvm`, `oracle-graalvm` | `graalvm` |
| `graalvm-community` | `graalvm-community` |
| `oracle` | `oracle` |
| `sapmachine` | `sapmachine` |
| `kona` | `kona` |
| `jetbrains` | `jetbrains` |
* In `.sdkmanrc` file, java version should be preceded by the `java=` prefix (e.g., `java=17.0.7-tem`). When a recognized SDKMAN distribution suffix is present, setup-java can infer the `distribution` input automatically. Unrecognized suffixes require setting `distribution` explicitly. The `.sdkmanrc` file supports version specifications in accordance with [file format](https://sdkman.io/usage#env-command), see [Sdkman! documentation](https://sdkman.io/jdks) for more information. * In `.sdkmanrc` file, java version should be preceded by the `java=` prefix (e.g., `java=17.0.7-tem`). When a recognized SDKMAN distribution suffix is present, setup-java can infer the `distribution` input automatically. Unrecognized suffixes require setting `distribution` explicitly. The `.sdkmanrc` file supports version specifications in accordance with [file format](https://sdkman.io/usage#env-command), see [Sdkman! documentation](https://sdkman.io/jdks) for more information.
Supported SDKMAN suffix mappings: Supported SDKMAN suffix mappings:
@@ -816,6 +960,19 @@ steps:
java=17.0.7-tem java=17.0.7-tem
``` ```
**Example step using `asdf`** (distribution inferred from `.tool-versions`):
```yml
- name: Setup java
uses: actions/setup-java@v5
with:
java-version-file: '.tool-versions'
```
**Example `.tool-versions`**:
```
java temurin-17.0.7+7
```
Valid entry options (does not apply to `.sdkmanrc`): Valid entry options (does not apply to `.sdkmanrc`):
``` ```
major versions: 8, 11, 16, 17, 21 major versions: 8, 11, 16, 17, 21

View File

@@ -12,6 +12,30 @@ const STATE_CACHE_PRIMARY_KEY = 'cache-primary-key';
const CACHE_MATCHED_KEY = 'cache-matched-key'; const CACHE_MATCHED_KEY = 'cache-matched-key';
const CACHE_KEY_PREFIX = 'setup-java'; const CACHE_KEY_PREFIX = 'setup-java';
/**
* An additional cache entry that is restored and saved independently of the
* main dependency cache. Used for build-tool wrapper distributions that rarely
* change (e.g. the Maven wrapper distribution) so that they are not evicted
* every time a volatile dependency file such as pom.xml changes. See
* https://github.com/actions/setup-java/issues/1095.
*/
interface AdditionalCache {
/**
* Short identifier for the cache, used to build its cache key and to scope
* the state keys that carry information from restore to save.
*/
name: string;
/**
* Paths that make up this cache entry.
*/
path: string[];
/**
* Glob patterns whose hash forms the cache key. If no file matches, the
* cache is skipped silently (the project simply does not use this feature).
*/
pattern: string[];
}
interface PackageManager { interface PackageManager {
id: 'maven' | 'gradle' | 'sbt'; id: 'maven' | 'gradle' | 'sbt';
/** /**
@@ -19,27 +43,36 @@ interface PackageManager {
*/ */
path: string[]; path: string[];
pattern: string[]; pattern: string[];
/**
* Additional caches keyed independently of the main dependency cache.
*/
additionalCaches?: AdditionalCache[];
} }
const supportedPackageManager: PackageManager[] = [ const supportedPackageManager: PackageManager[] = [
{ {
id: 'maven', id: 'maven',
path: [ path: [join(os.homedir(), '.m2', 'repository')],
join(os.homedir(), '.m2', 'repository'),
join(os.homedir(), '.m2', 'wrapper', 'dists')
],
// https://github.com/actions/cache/blob/0638051e9af2c23d10bb70fa9beffcad6cff9ce3/examples.md#java---maven // https://github.com/actions/cache/blob/0638051e9af2c23d10bb70fa9beffcad6cff9ce3/examples.md#java---maven
pattern: [ pattern: [
'**/pom.xml', '**/pom.xml',
'**/.mvn/wrapper/maven-wrapper.properties', '**/.mvn/wrapper/maven-wrapper.properties',
'**/.mvn/extensions.xml' '**/.mvn/extensions.xml'
],
// The Maven wrapper distribution only depends on the wrapper properties,
// which change very rarely, so it is cached separately from the local
// repository. This keeps it available across the frequent pom.xml changes
// that rotate the main cache key. See issue #1095.
additionalCaches: [
{
name: 'maven-wrapper',
path: [join(os.homedir(), '.m2', 'wrapper', 'dists')],
pattern: ['**/.mvn/wrapper/maven-wrapper.properties']
}
] ]
}, },
{ {
id: 'gradle', id: 'gradle',
path: [ path: [join(os.homedir(), '.gradle', 'caches')],
join(os.homedir(), '.gradle', 'caches'),
join(os.homedir(), '.gradle', 'wrapper')
],
// https://github.com/actions/cache/blob/0638051e9af2c23d10bb70fa9beffcad6cff9ce3/examples.md#java---gradle // https://github.com/actions/cache/blob/0638051e9af2c23d10bb70fa9beffcad6cff9ce3/examples.md#java---gradle
pattern: [ pattern: [
'**/*.gradle*', '**/*.gradle*',
@@ -48,6 +81,17 @@ const supportedPackageManager: PackageManager[] = [
'buildSrc/**/Dependencies.kt', 'buildSrc/**/Dependencies.kt',
'gradle/*.versions.toml', 'gradle/*.versions.toml',
'**/versions.properties' '**/versions.properties'
],
// The Gradle wrapper distribution only depends on the wrapper properties,
// which change very rarely, so it is cached separately from the Gradle
// caches. This keeps it available across the frequent *.gradle* changes
// that rotate the main cache key. See issue #269.
additionalCaches: [
{
name: 'gradle-wrapper',
path: [join(os.homedir(), '.gradle', 'wrapper')],
pattern: ['**/gradle-wrapper.properties']
}
] ]
}, },
{ {
@@ -87,6 +131,21 @@ function findPackageManager(id: string): PackageManager {
return packageManager; return packageManager;
} }
/**
* State keys used to carry an additional cache's restore-time information over
* to the post (save) action, scoped by the additional cache name.
*/
function additionalCachePrimaryKeyState(name: string): string {
return `${STATE_CACHE_PRIMARY_KEY}-${name}`;
}
function additionalCacheMatchedKeyState(name: string): string {
return `${CACHE_MATCHED_KEY}-${name}`;
}
function buildCacheKey(id: string, fileHash: string): string {
return `${CACHE_KEY_PREFIX}-${process.env['RUNNER_OS']}-${process.arch}-${id}-${fileHash}`;
}
/** /**
* A function that generates a cache key to use. * A function that generates a cache key to use.
* Format of the generated key will be "${{ platform }}-${{ id }}-${{ fileHash }}"". * Format of the generated key will be "${{ platform }}-${{ id }}-${{ fileHash }}"".
@@ -105,7 +164,22 @@ async function computeCacheKey(
`No file in ${process.cwd()} matched to [${pattern}], make sure you have checked out the target repository` `No file in ${process.cwd()} matched to [${pattern}], make sure you have checked out the target repository`
); );
} }
return `${CACHE_KEY_PREFIX}-${process.env['RUNNER_OS']}-${process.arch}-${packageManager.id}-${fileHash}`; return buildCacheKey(packageManager.id, fileHash);
}
/**
* Computes the cache key for an additional cache. Unlike {@link computeCacheKey}
* this returns undefined (instead of throwing) when no file matches the pattern,
* because additional caches are optional features that many projects do not use.
*/
async function computeAdditionalCacheKey(
additionalCache: AdditionalCache
): Promise<string | undefined> {
const fileHash = await glob.hashFiles(additionalCache.pattern.join('\n'));
if (!fileHash) {
return undefined;
}
return buildCacheKey(additionalCache.name, fileHash);
} }
/** /**
@@ -118,6 +192,7 @@ export async function restore(id: string, cacheDependencyPath: string) {
const primaryKey = await computeCacheKey(packageManager, cacheDependencyPath); const primaryKey = await computeCacheKey(packageManager, cacheDependencyPath);
core.debug(`primary key is ${primaryKey}`); core.debug(`primary key is ${primaryKey}`);
core.saveState(STATE_CACHE_PRIMARY_KEY, primaryKey); core.saveState(STATE_CACHE_PRIMARY_KEY, primaryKey);
core.setOutput(STATE_CACHE_PRIMARY_KEY, primaryKey);
// No "restoreKeys" is set, to start with a clear cache after dependency update (see https://github.com/actions/setup-java/issues/269) // 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(packageManager.path, primaryKey);
@@ -129,6 +204,40 @@ export async function restore(id: string, cacheDependencyPath: string) {
core.setOutput('cache-hit', false); core.setOutput('cache-hit', false);
core.info(`${packageManager.id} cache is not found`); core.info(`${packageManager.id} cache is not found`);
} }
for (const additionalCache of packageManager.additionalCaches ?? []) {
await restoreAdditionalCache(additionalCache);
}
}
/**
* Restore an additional cache (e.g. a build-tool wrapper distribution) that is
* keyed independently of the main dependency cache so that it survives changes
* to volatile dependency files. Skips silently when the project does not use
* the corresponding feature.
*/
async function restoreAdditionalCache(additionalCache: AdditionalCache) {
const primaryKey = await computeAdditionalCacheKey(additionalCache);
if (!primaryKey) {
core.debug(
`No file matched [${additionalCache.pattern}] for the ${additionalCache.name} cache, skipping.`
);
return;
}
core.debug(`${additionalCache.name} primary key is ${primaryKey}`);
core.saveState(
additionalCachePrimaryKeyState(additionalCache.name),
primaryKey
);
const matchedKey = await cache.restoreCache(additionalCache.path, primaryKey);
if (matchedKey) {
core.saveState(
additionalCacheMatchedKeyState(additionalCache.name),
matchedKey
);
core.info(`${additionalCache.name} cache restored from key: ${matchedKey}`);
}
} }
/** /**
@@ -142,6 +251,10 @@ export async function save(id: string) {
// Inputs are re-evaluated before the post action, so we want the original key used for restore // Inputs are re-evaluated before the post action, so we want the original key used for restore
const primaryKey = core.getState(STATE_CACHE_PRIMARY_KEY); const primaryKey = core.getState(STATE_CACHE_PRIMARY_KEY);
for (const additionalCache of packageManager.additionalCaches ?? []) {
await saveAdditionalCache(packageManager, additionalCache);
}
if (!primaryKey) { if (!primaryKey) {
core.warning('Error retrieving key from state.'); core.warning('Error retrieving key from state.');
return; return;
@@ -179,6 +292,70 @@ export async function save(id: string) {
} }
} }
/**
* Save an additional cache under its own key. Skips when no key was recorded at
* restore time (feature unused) or when the exact key was already restored.
*/
async function saveAdditionalCache(
packageManager: PackageManager,
additionalCache: AdditionalCache
) {
const primaryKey = core.getState(
additionalCachePrimaryKeyState(additionalCache.name)
);
const matchedKey = core.getState(
additionalCacheMatchedKeyState(additionalCache.name)
);
if (!primaryKey) {
// The feature is not used by this project, nothing to save.
core.debug(
`No primary key for the ${additionalCache.name} cache, not saving cache.`
);
return;
} else if (matchedKey === primaryKey) {
core.info(
`Cache hit occurred on the ${additionalCache.name} primary key ${primaryKey}, not saving cache.`
);
return;
}
try {
const cacheId = await cache.saveCache(additionalCache.path, primaryKey);
if (cacheId === -1) {
core.debug(
`${additionalCache.name} cache was not saved for the key: ${primaryKey}`
);
return;
}
core.info(
`${additionalCache.name} cache saved with the key: ${primaryKey}`
);
} catch (error) {
const err = error as Error;
if (err.name === cache.ValidationError.name) {
// The cache paths did not resolve, e.g. the wrapper distribution was
// never downloaded because a system build tool was used or the download
// failed. Optional wrapper caches must not fail the post step, so skip.
core.debug(
`${additionalCache.name} cache paths do not exist, not saving cache: ${err.message}`
);
return;
}
if (err.name === cache.ReserveCacheError.name) {
core.info(err.message);
} else {
if (isProbablyGradleDaemonProblem(packageManager, err)) {
core.warning(
'Failed to save Gradle cache on Windows. If tar.exe reported "Permission denied", try to run Gradle with `--no-daemon` option. Refer to https://github.com/actions/cache/issues/454 for details.'
);
}
throw error;
}
}
}
/** /**
* @param packageManager the specified package manager by user * @param packageManager the specified package manager by user
* @param error the error thrown by the saveCache * @param error the error thrown by the saveCache

View File

@@ -4,7 +4,8 @@ export const INPUT_JAVA_VERSION_FILE = 'java-version-file';
export const INPUT_ARCHITECTURE = 'architecture'; export const INPUT_ARCHITECTURE = 'architecture';
export const INPUT_JAVA_PACKAGE = 'java-package'; export const INPUT_JAVA_PACKAGE = 'java-package';
export const INPUT_DISTRIBUTION = 'distribution'; export const INPUT_DISTRIBUTION = 'distribution';
export const INPUT_JDK_FILE = 'jdkFile'; export const INPUT_JDK_FILE = 'jdk-file';
export const INPUT_JDK_FILE_DEPRECATED = 'jdkFile';
export const INPUT_CHECK_LATEST = 'check-latest'; export const INPUT_CHECK_LATEST = 'check-latest';
export const INPUT_SET_DEFAULT = 'set-default'; export const INPUT_SET_DEFAULT = 'set-default';
export const INPUT_VERIFY_SIGNATURE = 'verify-signature'; export const INPUT_VERIFY_SIGNATURE = 'verify-signature';

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,
@@ -19,6 +23,7 @@ export abstract class JavaBase {
protected architecture: string; protected architecture: string;
protected packageType: string; protected packageType: string;
protected stable: boolean; protected stable: boolean;
protected latest: boolean;
protected checkLatest: boolean; protected checkLatest: boolean;
protected setDefault: boolean; protected setDefault: boolean;
protected verifySignature: boolean; protected verifySignature: boolean;
@@ -33,9 +38,11 @@ export abstract class JavaBase {
maxRetries: 3 maxRetries: 3
}); });
({version: this.version, stable: this.stable} = this.normalizeVersion( ({
installerOptions.version version: this.version,
)); stable: this.stable,
latest: this.latest
} = this.normalizeVersion(installerOptions.version));
this.architecture = installerOptions.architecture || os.arch(); this.architecture = installerOptions.architecture || os.arch();
this.packageType = installerOptions.packageType; this.packageType = installerOptions.packageType;
this.checkLatest = installerOptions.checkLatest; this.checkLatest = installerOptions.checkLatest;
@@ -62,7 +69,7 @@ export abstract class JavaBase {
} }
let foundJava = this.findInToolcache(); let foundJava = this.findInToolcache();
if (foundJava && !this.checkLatest) { if (foundJava && !this.checkLatest && !this.latest) {
core.info(`Resolved Java ${foundJava.version} from tool-cache`); core.info(`Resolved Java ${foundJava.version} from tool-cache`);
} else { } else {
core.info('Trying to resolve the latest version from remote'); core.info('Trying to resolve the latest version from remote');
@@ -263,6 +270,30 @@ export abstract class JavaBase {
protected normalizeVersion(version: string) { protected normalizeVersion(version: string) {
let stable = true; let stable = true;
const latest = false;
// Support the `latest` alias (case-insensitive), which floats to the newest
// available stable/GA release. It is translated to the SemVer wildcard `x`
// so the existing "newest satisfying version wins" resolution applies.
const normalized = version.trim().toLowerCase();
if (normalized === 'latest') {
return {
version: 'x',
stable: true,
latest: true
};
}
// Reject `latest` combined with any qualifier (e.g. `latest-ea`). Such inputs
// would otherwise have their `-ea` suffix stripped and fall through to the
// generic SemVer check, which fails with a confusing "'latest' is not valid
// SemVer" message even though `latest` is a supported value. Fail early with a
// targeted explanation instead.
if (normalized.startsWith('latest')) {
throw new Error(
`The 'latest' alias resolves stable (GA) releases only and cannot be combined with '-ea' or other qualifiers (received '${version}'). Use 'latest' on its own, or specify a concrete version.`
);
}
if (version.endsWith('-ea')) { if (version.endsWith('-ea')) {
version = version.replace(/-ea$/, ''); version = version.replace(/-ea$/, '');
@@ -273,6 +304,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`
@@ -281,7 +321,8 @@ export abstract class JavaBase {
return { return {
version, version,
stable stable,
latest
}; };
} }

View File

@@ -59,10 +59,28 @@ export class CorrettoDistribution extends JavaBase {
if (!this.stable) { if (!this.stable) {
throw new Error('Early access versions are not supported'); throw new Error('Early access versions are not supported');
} }
const availableVersions = await this.getAvailableVersions();
// The `latest` alias is normalized to the SemVer wildcard, but Corretto
// matches on an exact major version, so resolve it to the newest available
// major from Corretto's own list.
if (this.latest) {
const majors = availableVersions
.map(item => parseInt(item.version, 10))
.filter(major => Number.isFinite(major) && major > 0);
if (majors.length === 0) {
throw new Error(
'Could not determine the latest available Corretto major version from remote metadata'
);
}
version = Math.max(...majors).toString();
}
if (version.includes('.')) { if (version.includes('.')) {
throw new Error('Only major versions are supported'); throw new Error('Only major versions are supported');
} }
const availableVersions = await this.getAvailableVersions();
const matchingVersions = availableVersions const matchingVersions = availableVersions
.filter(item => item.version == version) .filter(item => item.version == version)
.map(item => { .map(item => {

View File

@@ -16,6 +16,7 @@ import {
extractJdkFile, extractJdkFile,
getDownloadArchiveExtension, getDownloadArchiveExtension,
getGitHubHttpHeaders, getGitHubHttpHeaders,
getLatestMajorVersion,
getNextPageUrlFromLinkHeader, getNextPageUrlFromLinkHeader,
isVersionSatisfies, isVersionSatisfies,
MAX_PAGINATION_PAGES, MAX_PAGINATION_PAGES,
@@ -119,6 +120,13 @@ export class GraalVMDistribution extends JavaBase {
return this.findEABuildDownloadUrl(`${range}-ea`); return this.findEABuildDownloadUrl(`${range}-ea`);
} }
// The `latest` alias is normalized to the SemVer wildcard. Oracle GraalVM
// builds its download URLs from a concrete major and has no endpoint to list
// releases, so resolve the newest available GA major from the Adoptium API.
if (this.latest) {
range = (await getLatestMajorVersion(this.http)).toString();
}
const {platform, extension, major} = this.validateStableBuildRequest(range); const {platform, extension, major} = this.validateStableBuildRequest(range);
const fileUrl = this.constructFileUrl( const fileUrl = this.constructFileUrl(
@@ -203,6 +211,9 @@ export class GraalVMDistribution extends JavaBase {
if (statusCode === HttpCodes.NotFound) { if (statusCode === HttpCodes.NotFound) {
// Create the standard error with additional hint about checking the download URL // Create the standard error with additional hint about checking the download URL
const error = this.createVersionNotFoundError(range); const error = this.createVersionNotFoundError(range);
if (this.latest) {
error.message += `\nThe latest Java major version (${range}) is not yet available for the ${this.distribution} distribution. Please specify a concrete version instead of 'latest'.`;
}
error.message += `\nPlease check if this version is available at ${GRAALVM_DOWNLOAD_URL} . Pick a version from the list.`; error.message += `\nPlease check if this version is available at ${GRAALVM_DOWNLOAD_URL} . Pick a version from the list.`;
throw error; throw error;
} }
@@ -354,7 +365,27 @@ export class GraalVMCommunityDistribution extends GraalVMDistribution {
} }
const arch = this.getSupportedArchitecture(); const arch = this.getSupportedArchitecture();
const {platform, extension} = this.validateStableBuildRequest(range);
// GraalVM Community publishes its releases on GitHub, so the `latest` alias
// (normalized to the SemVer wildcard `x`) can float to the newest GA it
// actually ships. Unlike Oracle GraalVM (which has no listing endpoint and
// must derive the newest major from the Adoptium API), we match against the
// real release list here, so `latest` never fails when GraalVM lags behind a
// brand-new Java major.
let platform: OsVersions;
let extension: string;
if (this.latest) {
if (this.packageType !== 'jdk') {
throw new Error(
`${this.distribution} provides only the \`jdk\` package type`
);
}
platform = this.getPlatform();
extension = getDownloadArchiveExtension();
} else {
({platform, extension} = this.validateStableBuildRequest(range));
}
// GraalVM Community asset names embed the platform, architecture and // GraalVM Community asset names embed the platform, architecture and
// archive type, e.g. `graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz`. // archive type, e.g. `graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz`.
const assetSuffix = `_${platform}-${arch}_bin.${extension}`; const assetSuffix = `_${platform}-${arch}_bin.${extension}`;

View File

@@ -22,6 +22,12 @@ export class LocalDistribution extends JavaBase {
} }
public async setupJava(): Promise<JavaInstallerResults> { public async setupJava(): Promise<JavaInstallerResults> {
if (this.latest) {
throw new Error(
"The 'latest' version alias is not supported for the 'jdkfile' distribution. Please specify a concrete version."
);
}
let foundJava = this.findInToolcache(); let foundJava = this.findInToolcache();
if (foundJava) { if (foundJava) {

View File

@@ -13,6 +13,7 @@ import {
import { import {
extractJdkFile, extractJdkFile,
getDownloadArchiveExtension, getDownloadArchiveExtension,
getLatestMajorVersion,
renameWinArchive renameWinArchive
} from '../../util.js'; } from '../../util.js';
import {HttpCodes} from '@actions/http-client'; import {HttpCodes} from '@actions/http-client';
@@ -73,6 +74,14 @@ export class OracleDistribution extends JavaBase {
const platform = this.getPlatform(); const platform = this.getPlatform();
const extension = getDownloadArchiveExtension(); const extension = getDownloadArchiveExtension();
// The `latest` alias is normalized to the SemVer wildcard. Oracle builds its
// download URLs from a concrete major and has no endpoint to list releases,
// so resolve the newest available GA major from the Adoptium API and use it.
if (this.latest) {
const latestMajor = await getLatestMajorVersion(this.http);
range = latestMajor.toString();
}
const isOnlyMajorProvided = !range.includes('.'); const isOnlyMajorProvided = !range.includes('.');
const major = isOnlyMajorProvided ? range : range.split('.')[0]; const major = isOnlyMajorProvided ? range : range.split('.')[0];
@@ -113,6 +122,12 @@ export class OracleDistribution extends JavaBase {
} }
} }
if (this.latest) {
const error = this.createVersionNotFoundError(range);
error.message += `\nThe latest Java major version (${range}) is not yet available for the Oracle JDK distribution. Please specify a concrete version instead of 'latest'.`;
throw error;
}
throw this.createVersionNotFoundError(range); throw this.createVersionNotFoundError(range);
} }

View File

@@ -22,7 +22,7 @@ async function run() {
const versionFile = core.getInput(constants.INPUT_JAVA_VERSION_FILE); const versionFile = core.getInput(constants.INPUT_JAVA_VERSION_FILE);
const architecture = core.getInput(constants.INPUT_ARCHITECTURE); const architecture = core.getInput(constants.INPUT_ARCHITECTURE);
const packageType = core.getInput(constants.INPUT_JAVA_PACKAGE); const packageType = core.getInput(constants.INPUT_JAVA_PACKAGE);
const jdkFile = core.getInput(constants.INPUT_JDK_FILE); const jdkFile = getJdkFileInput();
const cache = core.getInput(constants.INPUT_CACHE); const cache = core.getInput(constants.INPUT_CACHE);
const cacheDependencyPath = core.getInput( const cacheDependencyPath = core.getInput(
constants.INPUT_CACHE_DEPENDENCY_PATH constants.INPUT_CACHE_DEPENDENCY_PATH
@@ -134,6 +134,19 @@ async function run() {
run(); run();
function getJdkFileInput(): string {
const jdkFile = core.getInput(constants.INPUT_JDK_FILE);
const deprecatedJdkFile = core.getInput(constants.INPUT_JDK_FILE_DEPRECATED);
if (deprecatedJdkFile) {
core.warning(
`The '${constants.INPUT_JDK_FILE_DEPRECATED}' input is deprecated and may be removed in a future release. Please use '${constants.INPUT_JDK_FILE}' instead.`
);
}
return jdkFile || deprecatedJdkFile;
}
async function installVersion( async function installVersion(
version: string, version: string,
options: installerInputsOptions, options: installerInputsOptions,
@@ -173,8 +186,14 @@ async function installVersion(
} }
const result = await distribution.setupJava(); const result = await distribution.setupJava();
// When the `latest` alias is used, the literal input isn't a real version, so
// pass the resolved version to the toolchains configuration instead.
const isLatest = version.trim().toLowerCase() === 'latest';
const toolchainVersion = isLatest ? result.version : version;
await toolchains.configureToolchains( await toolchains.configureToolchains(
version, toolchainVersion,
distributionName, distributionName,
result.path, result.path,
toolchainIds[toolchainId] toolchainIds[toolchainId]

View File

@@ -6,6 +6,7 @@ import * as cache from '@actions/cache';
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as tc from '@actions/tool-cache'; import * as tc from '@actions/tool-cache';
import * as httpm from '@actions/http-client';
import { import {
INPUT_JOB_STATUS, INPUT_JOB_STATUS,
DISTRIBUTIONS_ONLY_MAJOR_VERSION DISTRIBUTIONS_ONLY_MAJOR_VERSION
@@ -149,8 +150,10 @@ export function getVersionFromFileContent(
const versionFileName = getFileName(versionFile); const versionFileName = getFileName(versionFile);
if (versionFileName == '.tool-versions') { if (versionFileName == '.tool-versions') {
// Capture an optional asdf-java vendor prefix (e.g. `temurin-`, `corretto-`)
// in the `distribution` group so it can be mapped to a setup-java distribution.
javaVersionRegExp = javaVersionRegExp =
/^java\s+(?:\S*-)?(?<version>\d+(?:\.\d+)*([+_.-](?:openj9[-._]?\d[\w.-]*|java\d+|jre[-_\w]*|OpenJDK\d+[\w_.-]*|[a-z0-9]+))*)/im; /^java\s+(?:(?<distribution>\S*)-)?(?<version>\d+(?:\.\d+)*([+_.-](?:openj9[-._]?\d[\w.-]*|java\d+|jre[-_\w]*|OpenJDK\d+[\w_.-]*|[a-z0-9]+))*)/im;
} else if (versionFileName == '.sdkmanrc') { } else if (versionFileName == '.sdkmanrc') {
// Match both version and optional distribution identifier // Match both version and optional distribution identifier
javaVersionRegExp = javaVersionRegExp =
@@ -173,6 +176,17 @@ export function getVersionFromFileContent(
); );
} }
// Extract distribution from asdf .tool-versions file
if (versionFileName == '.tool-versions' && match?.groups?.distribution) {
const asdfDist = match.groups.distribution;
extractedDistribution = mapAsdfDistribution(asdfDist);
if (extractedDistribution) {
core.debug(
`Parsed distribution '${extractedDistribution}' from asdf identifier '${asdfDist}'`
);
}
}
core.debug( core.debug(
`Parsed version '${capturedVersion}' from file '${versionFileName}'` `Parsed version '${capturedVersion}' from file '${versionFileName}'`
); );
@@ -238,6 +252,49 @@ function mapSdkmanDistribution(sdkmanDist: string): string | undefined {
return mapped; return mapped;
} }
// Map asdf-java (.tool-versions) vendor identifiers to setup-java distribution names.
// asdf-java encodes the vendor as a prefix on the version string, e.g.
// `java temurin-17.0.3+7` or `java semeru-openj9-11.0.25+9`. Packaging variants
// (`-jre`, `-musl`, `-openj9`, `-crac`, `-javafx`, ...) are collapsed onto the
// base vendor since setup-java does not distinguish them here.
function mapAsdfDistribution(asdfDist: string): string | undefined {
const normalized = asdfDist.toLowerCase();
// Multi-segment vendors that map to a distinct setup-java distribution.
if (normalized.startsWith('graalvm-community')) {
return 'graalvm-community';
}
if (normalized.startsWith('oracle-graalvm')) {
return 'graalvm';
}
const baseVendor = normalized.split('-')[0];
const distributionMap: Record<string, string> = {
temurin: 'temurin',
adoptopenjdk: 'temurin',
zulu: 'zulu',
corretto: 'corretto',
liberica: 'liberica',
microsoft: 'microsoft',
semeru: 'semeru',
ibm: 'semeru',
dragonwell: 'dragonwell',
graalvm: 'graalvm',
oracle: 'oracle',
sapmachine: 'sapmachine',
kona: 'kona',
jetbrains: 'jetbrains'
};
const mapped = distributionMap[baseVendor];
if (!mapped) {
core.warning(
`Unknown asdf distribution identifier '${asdfDist}'. Please specify the distribution explicitly.`
);
}
return mapped;
}
// By convention, action expects version 8 in the format `8.*` instead of `1.8` // By convention, action expects version 8 in the format `8.*` instead of `1.8`
function avoidOldNotation(content: string): string { function avoidOldNotation(content: string): string {
return content.startsWith('1.') ? content.substring(2) : content; return content.startsWith('1.') ? content.substring(2) : content;
@@ -326,3 +383,33 @@ export function renameWinArchive(javaArchivePath: string): string {
fs.renameSync(javaArchivePath, javaArchivePathRenamed); fs.renameSync(javaArchivePath, javaArchivePathRenamed);
return javaArchivePathRenamed; return javaArchivePathRenamed;
} }
interface IAdoptiumAvailableReleases {
most_recent_feature_release: number;
}
// Resolve the newest available stable/GA feature (major) release.
//
// Some distributions (e.g. Oracle, GraalVM) construct their download URLs from a
// concrete major version and don't expose an endpoint to list every available
// release, so a bare `latest` alias can't be resolved from their own metadata.
// The Adoptium (Temurin) API is used as a proxy for "what is the newest GA major
// version out there", which those distributions typically publish at the same time.
export async function getLatestMajorVersion(
http: httpm.HttpClient
): Promise<number> {
const availableReleasesUrl =
'https://api.adoptium.net/v3/info/available_releases';
const response =
await http.getJson<IAdoptiumAvailableReleases>(availableReleasesUrl);
const mostRecent = response.result?.most_recent_feature_release;
if (!mostRecent || Number.isNaN(Number(mostRecent))) {
throw new Error(
`Could not determine the latest available Java major version from ${availableReleasesUrl}`
);
}
return Number(mostRecent);
}