Compare commits

..

19 Commits

Author SHA1 Message Date
Bruno Borges
da3bbf55d2 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>
2026-07-09 17:19:16 -04:00
Bruno Borges
28eade3106 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-09 17:17:13 -04:00
Bruno Borges
b7bc23658d Merge remote-tracking branch 'origin/main' into brunoborges-support-java-version-latest 2026-07-09 17:15:48 -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
ef25c468d7 Merge remote-tracking branch 'origin/main' into brunoborges-support-java-version-latest
# Conflicts:
#	__tests__/distributors/base-installer.test.ts
2026-07-09 17:13:41 -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
94eba1b8f5 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>
2026-07-09 15:44:30 -04:00
Bruno Borges
7155e1ca90 Clarify latest note for oracle/graalvm version resolution vs download
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-09 15:30:23 -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
71ee2e9aa6 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>
2026-07-09 15:20:59 -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
Bruno Borges
8a3e8157a1 Map Zulu x86 architecture to i686 for Azul Metadata API (#1079)
The Azul Metadata API's `arch=x86` returns both 32-bit (i686) and 64-bit
(x64) packages. Because the two variants share identical java_version and
distro_version, setup-java cannot distinguish them and may resolve an
explicit `architecture: x86` request to a 64-bit JDK (and for Java 21+,
where 32-bit is dropped, x86 silently returns x64 instead of failing).

The legacy Zulu Discovery API used `arch=x86&hw_bitness=32` to target only
32-bit builds. The Metadata API exposes the equivalent via `arch=i686`,
which returns only genuine 32-bit builds with full version parity to the
old behavior. Map x86 -> i686 to restore correct 32-bit resolution.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-08 09:46:49 -04:00
Priya Gupta
f7121373a9 Migrate to ESM and upgrade dependencies (#1078)
* Migrate to ESM and upgrade dependencies

* fix: update import statement for JSON module in kona-installer test

---------

Co-authored-by: George Adams <georgeadams1995@gmail.com>
2026-07-08 10:15:00 +01:00
Copilot
2a07c83aea feat: add .mvn/extensions.xml to Maven cache key pattern (#1041)
* Initial plan

* feat: add .mvn/extensions.xml to Maven cache key pattern

Closes #990

Maven build extensions declared in `.mvn/extensions.xml` can introduce
additional plugin dependencies (e.g. lifecycle participants, custom
packaging types). Including this file in the cache key hash ensures that
changes to extensions — which affect what plugin JARs Maven downloads —
properly invalidate the cache, preventing stale caches from missing
newly-required plugin dependencies.

Changes:
- src/cache.ts: add `**/.mvn/extensions.xml` to Maven pattern array
- __tests__/cache.test.ts: update pattern expectations; add new test
- README.md: document the new file in the Maven cache key hash list

* test: update maven cache error test name for extensions.xml

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Bruno Borges <brborges@microsoft.com>
2026-07-08 10:07:54 +01:00
James Wald
d3530c1eb4 dist: Migrate from Zulu Discovery API to Azul Metadata API (#1010)
* Use Azul metadata API

* Document arm64 -> aarch64 mapping in README.md

* Paginate through all available versions

* Fix typo: win_aarhc4

* Only query for linux_glibc packages

* Add Zulu CRaC package support to metadata migration

Fold CRaC-related work into the Zulu metadata API migration by wiring crac_supported query handling, extending Zulu package docs, and updating installer tests for jdk+crac/jre+crac behavior.

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

* Harden Zulu metadata pagination with safety cap

- Stop paginating on a short page to avoid an extra empty request
- Guard against undefined results (not just null)
- Cap iterations at 100 pages and warn if the limit is hit to
  prevent a runaway loop if the API misbehaves

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

* Preserve JDK build number from Azul Metadata API

The Azul Metadata API returns java_version as a 3-element array
(e.g. [17,0,7]) and reports the build number separately in
openjdk_build_number. The migration mapped version directly from
java_version, dropping the build and breaking exact-version lookups
like 17.0.7+7 (e2e failure: "No matching version found for SemVer").

Add openjdk_build_number to IZuluVersions and append it to
java_version before converting to semver so resolved versions retain
the build (e.g. 17.0.7+7). Update the zulu test fixtures to mirror the
real API shape (3-element java_version plus openjdk_build_number) so
unit tests exercise the actual response format, and rebuild dist.

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

---------

Co-authored-by: Bruno Borges <brborges@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-08 10:00:43 +01:00
98 changed files with 136668 additions and 124751 deletions

View File

@@ -1,6 +0,0 @@
# Ignore list
/*
# Do not ignore these folders:
!__tests__/
!src/

View File

@@ -1,51 +0,0 @@
// This is a reusable configuration file copied from https://github.com/actions/reusable-workflows/tree/main/reusable-configurations. Please don't make changes to this file as it's the subject of an automatic update.
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:eslint-plugin-jest/recommended',
'eslint-config-prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'eslint-plugin-node', 'eslint-plugin-jest'],
rules: {
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-ignore': 'allow-with-description'
}
],
'no-console': 'error',
'yoda': 'error',
'prefer-const': [
'error',
{
destructuring: 'all'
}
],
'no-control-regex': 'off',
'no-constant-condition': ['error', {checkLoops: false}],
'node/no-extraneous-import': 'error'
},
overrides: [
{
files: ['**/*{test,spec}.ts'],
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'jest/no-standalone-expect': 'off',
'jest/no-conditional-expect': 'off',
'no-console': 'off',
}
}
],
env: {
node: true,
es6: true,
'jest/globals': true
}
};

View File

@@ -10,8 +10,11 @@ allowed:
- mit - mit
- cc0-1.0 - cc0-1.0
- unlicense - unlicense
- blueoak-1.0.0
reviewed: reviewed:
npm: npm:
- "@actions/http-client" # MIT (license text present), but detected as "other" - "@actions/http-client" # MIT (license text present), but detected as "other"
- "argparse" # Python Software Foundation License (PSF), but detected as "other" - "argparse" # Python Software Foundation License (PSF), but detected as "other"
- "balanced-match"
- "brace-expansion"

View File

@@ -1,6 +1,6 @@
--- ---
name: "@actions/cache" name: "@actions/cache"
version: 5.1.0 version: 6.1.0
type: npm type: npm
summary: Actions cache lib summary: Actions cache lib
homepage: https://github.com/actions/toolkit/tree/main/packages/cache homepage: https://github.com/actions/toolkit/tree/main/packages/cache

View File

@@ -1,6 +1,6 @@
--- ---
name: "@actions/core" name: "@actions/core"
version: 2.0.3 version: 3.0.1
type: npm type: npm
summary: Actions core lib summary: Actions core lib
homepage: https://github.com/actions/toolkit/tree/main/packages/core homepage: https://github.com/actions/toolkit/tree/main/packages/core

View File

@@ -1,6 +1,6 @@
--- ---
name: "@actions/exec" name: "@actions/exec"
version: 2.0.0 version: 3.0.0
type: npm type: npm
summary: Actions exec lib summary: Actions exec lib
homepage: https://github.com/actions/toolkit/tree/main/packages/exec homepage: https://github.com/actions/toolkit/tree/main/packages/exec

View File

@@ -1,6 +1,6 @@
--- ---
name: "@actions/glob" name: "@actions/glob"
version: 0.5.1 version: 0.6.1
type: npm type: npm
summary: Actions glob lib summary: Actions glob lib
homepage: https://github.com/actions/toolkit/tree/main/packages/glob homepage: https://github.com/actions/toolkit/tree/main/packages/glob

View File

@@ -0,0 +1,20 @@
---
name: "@actions/glob"
version: 0.7.0
type: npm
summary: Actions glob lib
homepage: https://github.com/actions/toolkit/tree/main/packages/glob
license: mit
licenses:
- sources: LICENSE.md
text: |-
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
notices: []

View File

@@ -1,6 +1,6 @@
--- ---
name: "@actions/http-client" name: "@actions/http-client"
version: 3.0.2 version: 4.0.1
type: npm type: npm
summary: Actions Http Client summary: Actions Http Client
homepage: https://github.com/actions/toolkit/tree/main/packages/http-client homepage: https://github.com/actions/toolkit/tree/main/packages/http-client

View File

@@ -1,6 +1,6 @@
--- ---
name: "@actions/io" name: "@actions/io"
version: 2.0.0 version: 3.0.2
type: npm type: npm
summary: Actions io lib summary: Actions io lib
homepage: https://github.com/actions/toolkit/tree/main/packages/io homepage: https://github.com/actions/toolkit/tree/main/packages/io

View File

@@ -1,6 +1,6 @@
--- ---
name: "@actions/tool-cache" name: "@actions/tool-cache"
version: 3.0.1 version: 4.0.0
type: npm type: npm
summary: Actions tool-cache lib summary: Actions tool-cache lib
homepage: https://github.com/actions/toolkit/tree/main/packages/tool-cache homepage: https://github.com/actions/toolkit/tree/main/packages/tool-cache

View File

@@ -1,6 +1,6 @@
--- ---
name: "@azure/core-client" name: "@azure/core-client"
version: 1.10.1 version: 1.10.2
type: npm type: npm
summary: Core library for interfacing with AutoRest generated code summary: Core library for interfacing with AutoRest generated code
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-client/ homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-client/

View File

@@ -1,6 +1,6 @@
--- ---
name: "@azure/core-http-compat" name: "@azure/core-http-compat"
version: 2.3.1 version: 2.4.0
type: npm type: npm
summary: Core HTTP Compatibility Library to bridge the gap between Core V1 & V2 packages. summary: Core HTTP Compatibility Library to bridge the gap between Core V1 & V2 packages.
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-compat/ homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-compat/

View File

@@ -1,9 +1,9 @@
--- ---
name: "@azure/core-rest-pipeline" name: "@azure/core-rest-pipeline"
version: 1.22.2 version: 1.24.0
type: npm type: npm
summary: Isomorphic client library for making HTTP requests in node.js and browser. summary: Isomorphic client library for making HTTP requests in node.js and browser.
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-rest-pipeline/ homepage: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/core/core-rest-pipeline/README.md
license: mit license: mit
licenses: licenses:
- sources: LICENSE - sources: LICENSE

View File

@@ -1,6 +1,6 @@
--- ---
name: "@azure/core-xml" name: "@azure/core-xml"
version: 1.5.0 version: 1.5.1
type: npm type: npm
summary: Core library for interacting with XML payloads summary: Core library for interacting with XML payloads
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-xml/ homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-xml/

View File

@@ -1,9 +1,9 @@
--- ---
name: "@azure/storage-blob" name: "@azure/storage-blob"
version: 12.29.1 version: 12.33.0
type: npm type: npm
summary: Microsoft Azure Storage SDK for JavaScript - Blob summary: Microsoft Azure Storage SDK for JavaScript - Blob
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/storage/storage-blob/ homepage: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/storage/storage-blob/README.md
license: mit license: mit
licenses: licenses:
- sources: LICENSE - sources: LICENSE

View File

@@ -1,9 +1,9 @@
--- ---
name: "@azure/storage-common" name: "@azure/storage-common"
version: 12.1.1 version: 12.4.1
type: npm type: npm
summary: Azure Storage Common Client Library for JavaScript summary: Azure Storage Common Client Library for JavaScript
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/storage/storage-internal-avro/ homepage: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/storage/storage-common/README.md
license: mit license: mit
licenses: licenses:
- sources: LICENSE - sources: LICENSE

View File

@@ -1,9 +1,9 @@
--- ---
name: "@typespec/ts-http-runtime" name: "@typespec/ts-http-runtime"
version: 0.3.2 version: 0.3.6
type: npm type: npm
summary: Isomorphic client library for making HTTP requests in node.js and browser. summary: Isomorphic client library for making HTTP requests in node.js and browser.
homepage: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/ts-http-runtime/ homepage: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/core/ts-http-runtime/README.md
license: mit license: mit
licenses: licenses:
- sources: LICENSE - sources: LICENSE

View File

@@ -1,6 +1,6 @@
--- ---
name: anynum name: anynum
version: 1.0.0 version: 1.0.1
type: npm type: npm
summary: Normalize all Unicode decimal digits (Devanagari, Arabic, Thai, etc.) to summary: Normalize all Unicode decimal digits (Devanagari, Arabic, Thai, etc.) to
ASCII numerals. Zero dependencies, performance-first. ASCII numerals. Zero dependencies, performance-first.
@@ -30,4 +30,6 @@ licenses:
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
- sources: README.md
text: MIT
notices: [] notices: []

View File

@@ -0,0 +1,34 @@
---
name: balanced-match
version: 4.0.4
type: npm
summary: Match balanced character pairs, like "{" and "}"
homepage:
license: other
licenses:
- sources: LICENSE.md
text: |
(MIT)
Original code Copyright Julian Gruber <julian@juliangruber.com>
Port to TypeScript Copyright Isaac Z. Schlueter <i@izs.me>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
notices: []

View File

@@ -1,6 +1,6 @@
--- ---
name: brace-expansion name: brace-expansion
version: 1.1.13 version: 1.1.15
type: npm type: npm
summary: Brace expansion as known from sh/bash summary: Brace expansion as known from sh/bash
homepage: https://github.com/juliangruber/brace-expansion homepage: https://github.com/juliangruber/brace-expansion

View File

@@ -0,0 +1,34 @@
---
name: brace-expansion
version: 5.0.7
type: npm
summary: Brace expansion as known from sh/bash
homepage:
license: other
licenses:
- sources: LICENSE
text: |
MIT License
Copyright Julian Gruber <julian@juliangruber.com>
TypeScript port Copyright Isaac Z. Schlueter <i@izs.me>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
notices: []

View File

@@ -1,6 +1,6 @@
--- ---
name: fast-xml-builder name: fast-xml-builder
version: 1.2.0 version: 1.2.1
type: npm type: npm
summary: Build XML from JSON without C/C++ based libraries summary: Build XML from JSON without C/C++ based libraries
homepage: homepage:

View File

@@ -1,6 +1,6 @@
--- ---
name: fast-xml-parser name: fast-xml-parser
version: 5.8.0 version: 5.9.3
type: npm type: npm
summary: Validate XML, Parse XML, Build XML without C/C++ based libraries summary: Validate XML, Parse XML, Build XML without C/C++ based libraries
homepage: homepage:

View File

@@ -1,16 +1,17 @@
--- ---
name: "@azure/abort-controller" name: is-unsafe
version: 1.1.0 version: 1.0.1
type: npm type: npm
summary: Microsoft Azure SDK for JavaScript - Aborter summary: Zero-dependency, DOM-free, pure predicate for detecting unsafe strings across
homepage: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/core/abort-controller/README.md HTML, XML, SVG, SQL, SHELL, and REGEX contexts
homepage:
license: mit license: mit
licenses: licenses:
- sources: LICENSE - sources: LICENSE
text: | text: |
The MIT License (MIT) MIT License
Copyright (c) 2020 Microsoft Copyright (c) 2026 Natural Intelligence
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -29,4 +30,6 @@ licenses:
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
- sources: README.md
text: MIT
notices: [] notices: []

View File

@@ -1,6 +1,6 @@
--- ---
name: js-yaml name: js-yaml
version: 4.1.1 version: 4.3.0
type: npm type: npm
summary: YAML 1.2 parser and serializer summary: YAML 1.2 parser and serializer
homepage: homepage:

66
.licenses/npm/minimatch-10.2.5.dep.yml generated Normal file
View File

@@ -0,0 +1,66 @@
---
name: minimatch
version: 10.2.5
type: npm
summary: a glob matcher in javascript
homepage:
license: blueoak-1.0.0
licenses:
- sources: LICENSE.md
text: |
# Blue Oak Model License
Version 1.0.0
## Purpose
This license gives everyone as much permission to work with
this software as possible, while protecting contributors
from liability.
## Acceptance
In order to receive this license, you must agree to its
rules. The rules of this license are both obligations
under that agreement and conditions to your license.
You must not do anything with this software that triggers
a rule that you cannot or will not follow.
## Copyright
Each contributor licenses you to do everything with this
software that would otherwise infringe that contributor's
copyright in it.
## Notices
You must ensure that everyone who gets a copy of
any part of this software from you, with or without
changes, also gets the text of this license or a link to
<https://blueoakcouncil.org/license/1.0.0>.
## Excuse
If anyone notifies you in writing that you have not
complied with [Notices](#notices), you can keep your
license by taking all practical steps to comply within 30
days after the notice. If you do not do so, your license
ends immediately.
## Patent
Each contributor licenses you to do everything with this
software that would otherwise infringe any patent claims
they can license or become able to license.
## Reliability
No contributor can revoke this license.
## No Liability
**_As far as the law allows, this software comes as is,
without any warranty or condition, and no contributor
will be liable to anyone for any damages related to this
software or this license, under any kind of legal claim._**
notices: []

View File

@@ -1,6 +1,6 @@
--- ---
name: path-expression-matcher name: path-expression-matcher
version: 1.5.0 version: 1.6.1
type: npm type: npm
summary: Efficient path tracking and pattern matching for XML/JSON parsers summary: Efficient path tracking and pattern matching for XML/JSON parsers
homepage: https://github.com/NaturalIntelligence/path-expression-matcher#readme homepage: https://github.com/NaturalIntelligence/path-expression-matcher#readme

View File

@@ -1,26 +0,0 @@
---
name: semver
version: 6.3.1
type: npm
summary: The semantic version parser used by npm.
homepage:
license: isc
licenses:
- sources: LICENSE
text: |
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
notices: []

View File

@@ -1,6 +1,6 @@
--- ---
name: semver name: semver
version: 7.8.4 version: 7.8.5
type: npm type: npm
summary: The semantic version parser used by npm. summary: The semantic version parser used by npm.
homepage: homepage:

View File

@@ -1,6 +1,6 @@
--- ---
name: strnum name: strnum
version: 2.4.0 version: 2.4.1
type: npm type: npm
summary: Parse String to Number based on configuration summary: Parse String to Number based on configuration
homepage: homepage:

View File

@@ -1,11 +0,0 @@
// This is a reusable configuration file copied from https://github.com/actions/reusable-workflows/tree/main/reusable-configurations. Please don't make changes to this file as it's the subject of an automatic update.
module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: true,
trailingComma: 'none',
bracketSpacing: false,
arrowParens: 'avoid'
};

10
.prettierrc.json Normal file
View File

@@ -0,0 +1,10 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": false,
"arrowParens": "avoid"
}

View File

@@ -18,6 +18,10 @@ 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.
## What's new in V6
- **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
- Upgraded action from node20 to node24 - Upgraded action from node20 to node24
@@ -100,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:
@@ -185,6 +198,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

@@ -1,24 +1,63 @@
import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import {fileURLToPath} from 'url';
import * as io from '@actions/io'; import * as io from '@actions/io';
import * as core from '@actions/core';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import os from 'os'; import os from 'os';
import * as auth from '../src/auth'; // Mock @actions/core before importing source modules that depend on it
import {M2_DIR, MVN_SETTINGS_FILE} from '../src/constants'; jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const auth = await import('../src/auth.js');
const {M2_DIR, MVN_SETTINGS_FILE} = await import('../src/constants.js');
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const m2Dir = path.join(__dirname, M2_DIR); const m2Dir = path.join(__dirname, M2_DIR);
const settingsFile = path.join(m2Dir, MVN_SETTINGS_FILE); const settingsFile = path.join(m2Dir, MVN_SETTINGS_FILE);
describe('auth tests', () => { describe('auth tests', () => {
let spyOSHomedir: jest.SpyInstance; let spyOSHomedir: any;
let spyInfo: jest.SpyInstance; let spyInfo: any;
beforeEach(async () => { beforeEach(async () => {
await io.rmRF(m2Dir); await io.rmRF(m2Dir);
spyOSHomedir = jest.spyOn(os, 'homedir'); spyOSHomedir = jest.spyOn(os, 'homedir');
spyOSHomedir.mockReturnValue(__dirname); spyOSHomedir.mockReturnValue(__dirname);
spyInfo = jest.spyOn(core, 'info'); spyInfo = core.info as jest.Mock;
spyInfo.mockImplementation(() => null); spyInfo.mockImplementation(() => null);
}, 300000); }, 300000);

View File

@@ -1,23 +1,84 @@
import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import {mkdtempSync} from 'fs'; import {mkdtempSync} from 'fs';
import {tmpdir} from 'os'; import {tmpdir} from 'os';
import {join} from 'path'; import {join} from 'path';
import {restore, save} from '../src/cache';
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as core from '@actions/core';
import * as cache from '@actions/cache'; // Mock @actions/core before importing source modules that depend on it
import * as glob from '@actions/glob'; jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
jest.unstable_mockModule('@actions/cache', () => ({
restoreCache: jest.fn(),
saveCache: jest.fn(),
isFeatureAvailable: jest.fn(),
ValidationError: class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
},
ReserveCacheError: class ReserveCacheError extends Error {
constructor(message: string) {
super(message);
this.name = 'ReserveCacheError';
}
}
}));
jest.unstable_mockModule('@actions/glob', () => ({
hashFiles: jest.fn(),
create: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const cache = await import('@actions/cache');
const glob = await import('@actions/glob');
const {restore, save} = await import('../src/cache.js');
describe('dependency cache', () => { describe('dependency cache', () => {
const ORIGINAL_RUNNER_OS = process.env['RUNNER_OS']; const ORIGINAL_RUNNER_OS = process.env['RUNNER_OS'];
const ORIGINAL_GITHUB_WORKSPACE = process.env['GITHUB_WORKSPACE']; const ORIGINAL_GITHUB_WORKSPACE = process.env['GITHUB_WORKSPACE'];
const ORIGINAL_CWD = process.cwd(); const ORIGINAL_CWD = process.cwd();
let workspace: string; let workspace: string;
let spyInfo: jest.SpyInstance<void, Parameters<typeof core.info>>; let spyInfo: any;
let spyWarning: jest.SpyInstance<void, Parameters<typeof core.warning>>; let spyWarning: any;
let spyDebug: jest.SpyInstance<void, Parameters<typeof core.debug>>; let spyDebug: any;
let spySaveState: jest.SpyInstance<void, Parameters<typeof core.saveState>>; let spySaveState: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
beforeEach(() => { beforeEach(() => {
workspace = mkdtempSync(join(tmpdir(), 'setup-java-cache-')); workspace = mkdtempSync(join(tmpdir(), 'setup-java-cache-'));
@@ -41,20 +102,20 @@ describe('dependency cache', () => {
}); });
beforeEach(() => { beforeEach(() => {
spyInfo = jest.spyOn(core, 'info'); spyInfo = core.info as jest.Mock;
spyInfo.mockImplementation(() => null); spyInfo.mockImplementation(() => null);
spyWarning = jest.spyOn(core, 'warning'); spyWarning = core.warning as jest.Mock;
spyWarning.mockImplementation(() => null); spyWarning.mockImplementation(() => null);
spyDebug = jest.spyOn(core, 'debug'); spyDebug = core.debug as jest.Mock;
spyDebug.mockImplementation(() => null); spyDebug.mockImplementation(() => null);
spySaveState = jest.spyOn(core, 'saveState'); spySaveState = core.saveState as jest.Mock;
spySaveState.mockImplementation(() => null); spySaveState.mockImplementation(() => null);
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
}); });
@@ -70,27 +131,18 @@ describe('dependency cache', () => {
}); });
describe('restore', () => { describe('restore', () => {
let spyCacheRestore: jest.SpyInstance< let spyCacheRestore: any;
ReturnType<typeof cache.restoreCache>, let spyGlobHashFiles: any;
Parameters<typeof cache.restoreCache> let spySetOutput: any;
>;
let spyGlobHashFiles: jest.SpyInstance<
ReturnType<typeof glob.hashFiles>,
Parameters<typeof glob.hashFiles>
>;
let spySetOutput: jest.SpyInstance<
ReturnType<typeof core.setOutput>,
Parameters<typeof core.setOutput>
>;
beforeEach(() => { beforeEach(() => {
spyCacheRestore = jest spyCacheRestore = (cache.restoreCache as any).mockImplementation(
.spyOn(cache, 'restoreCache') (paths: string[], primaryKey: string) => Promise.resolve(undefined)
.mockImplementation((paths: string[], primaryKey: string) => );
Promise.resolve(undefined) spyGlobHashFiles = glob.hashFiles as jest.Mock;
); spyGlobHashFiles.mockResolvedValue('hash-stub');
spyGlobHashFiles = jest.spyOn(glob, 'hashFiles'); spySetOutput = core.setOutput as jest.Mock;
spySetOutput = jest.spyOn(core, 'setOutput').mockImplementation(() => {}); spySetOutput.mockImplementation(() => null);
spyWarning.mockImplementation(() => null); spyWarning.mockImplementation(() => null);
}); });
@@ -102,6 +154,7 @@ describe('dependency cache', () => {
describe('for maven', () => { describe('for maven', () => {
it('throws error if no pom.xml, maven-wrapper.properties, or extensions.xml found', async () => { it('throws error if no pom.xml, maven-wrapper.properties, or extensions.xml found', async () => {
spyGlobHashFiles.mockResolvedValue('');
await expect(restore('maven', '')).rejects.toThrow( await expect(restore('maven', '')).rejects.toThrow(
`No file in ${projectRoot( `No file in ${projectRoot(
workspace workspace
@@ -176,6 +229,7 @@ describe('dependency cache', () => {
}); });
describe('for gradle', () => { describe('for gradle', () => {
it('throws error if no build.gradle found', async () => { it('throws error if no build.gradle found', async () => {
spyGlobHashFiles.mockResolvedValue('');
await expect(restore('gradle', '')).rejects.toThrow( await expect(restore('gradle', '')).rejects.toThrow(
`No file in ${projectRoot( `No file in ${projectRoot(
workspace workspace
@@ -231,6 +285,7 @@ describe('dependency cache', () => {
}); });
describe('for sbt', () => { describe('for sbt', () => {
it('throws error if no build.sbt found', async () => { it('throws error if no build.sbt found', async () => {
spyGlobHashFiles.mockResolvedValue('');
await expect(restore('sbt', '')).rejects.toThrow( await expect(restore('sbt', '')).rejects.toThrow(
`No file in ${projectRoot( `No file in ${projectRoot(
workspace workspace
@@ -249,6 +304,13 @@ describe('dependency cache', () => {
expect(spyInfo).toHaveBeenCalledWith('sbt cache is not found'); expect(spyInfo).toHaveBeenCalledWith('sbt cache is not found');
}); });
it('detects scala and sbt changes under **/project/ folder', async () => { it('detects scala and sbt changes under **/project/ folder', async () => {
let callCount = 0;
spyGlobHashFiles.mockImplementation(async () => {
callCount++;
// Return same hash for first two calls, different for third
return callCount <= 2 ? 'hash-v1' : 'hash-v2';
});
createFile(join(workspace, 'build.sbt')); createFile(join(workspace, 'build.sbt'));
createDirectory(join(workspace, 'project')); createDirectory(join(workspace, 'project'));
createFile(join(workspace, 'project/DependenciesV1.scala')); createFile(join(workspace, 'project/DependenciesV1.scala'));
@@ -284,6 +346,7 @@ describe('dependency cache', () => {
}); });
describe('cache-dependency-path', () => { describe('cache-dependency-path', () => {
it('throws error if no matching dependency file found', async () => { it('throws error if no matching dependency file found', async () => {
spyGlobHashFiles.mockResolvedValue('');
createFile(join(workspace, 'build.gradle.kts')); createFile(join(workspace, 'build.gradle.kts'));
await expect( await expect(
restore('gradle', 'sub-project/**/build.gradle.kts') restore('gradle', 'sub-project/**/build.gradle.kts')
@@ -325,17 +388,12 @@ describe('dependency cache', () => {
}); });
}); });
describe('save', () => { describe('save', () => {
let spyCacheSave: jest.SpyInstance< let spyCacheSave: any;
ReturnType<typeof cache.saveCache>,
Parameters<typeof cache.saveCache>
>;
beforeEach(() => { beforeEach(() => {
spyCacheSave = jest spyCacheSave = (cache.saveCache as any).mockImplementation(
.spyOn(cache, 'saveCache') (paths: string[], key: string) => Promise.resolve(0)
.mockImplementation((paths: string[], key: string) => );
Promise.resolve(0)
);
spyWarning.mockImplementation(() => null); spyWarning.mockImplementation(() => null);
}); });
@@ -495,14 +553,14 @@ describe('dependency cache', () => {
}); });
function resetState() { function resetState() {
jest.spyOn(core, 'getState').mockReset(); (core.getState as jest.Mock).mockReset();
} }
/** /**
* Create states to emulate a restore process without build file. * Create states to emulate a restore process without build file.
*/ */
function createStateForMissingBuildFile() { function createStateForMissingBuildFile() {
jest.spyOn(core, 'getState').mockImplementation(name => { (core.getState as jest.Mock<any>).mockImplementation((name: any) => {
switch (name) { switch (name) {
case 'cache-primary-key': case 'cache-primary-key':
return 'setup-java-cache-'; return 'setup-java-cache-';
@@ -516,7 +574,7 @@ function createStateForMissingBuildFile() {
* Create states to emulate a successful restore process. * Create states to emulate a successful restore process.
*/ */
function createStateForSuccessfulRestore() { function createStateForSuccessfulRestore() {
jest.spyOn(core, 'getState').mockImplementation(name => { (core.getState as jest.Mock<any>).mockImplementation((name: any) => {
switch (name) { switch (name) {
case 'cache-primary-key': case 'cache-primary-key':
return 'setup-java-cache-primary-key'; return 'setup-java-cache-primary-key';

View File

@@ -1,32 +1,87 @@
import {run as cleanup} from '../src/cleanup-java'; import {
import * as core from '@actions/core'; jest,
import * as cache from '@actions/cache'; describe,
import * as util from '../src/util'; it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
// Mock @actions/cache before importing source modules
const real_cache_module = await import('@actions/cache');
jest.unstable_mockModule('@actions/cache', () => ({
...real_cache_module,
saveCache: jest.fn(),
restoreCache: jest.fn()
}));
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
const real_util_module = await import('../src/util.js');
jest.unstable_mockModule('../src/util.js', () => ({
...real_util_module,
extractJdkFile: jest.fn(),
getDownloadArchiveExtension: jest.fn(),
getToolcachePath: jest.fn(),
isJobStatusSuccess: jest.fn(),
renameWinArchive: jest.fn(),
isVersionSatisfies: real_util_module.isVersionSatisfies,
getTempDir: real_util_module.getTempDir
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const cache = await import('@actions/cache');
const {run: cleanup} = await import('../src/cleanup-java.js');
const util = await import('../src/util.js');
describe('cleanup', () => { describe('cleanup', () => {
let spyWarning: jest.SpyInstance<void, Parameters<typeof core.warning>>; let spyWarning: any;
let spyInfo: jest.SpyInstance<void, Parameters<typeof core.info>>; let spyInfo: any;
let spyCacheSave: jest.SpyInstance< let spyCacheSave: any;
ReturnType<typeof cache.saveCache>, let spyJobStatusSuccess: any;
Parameters<typeof cache.saveCache> let spyCoreError: any;
>;
let spyJobStatusSuccess: jest.SpyInstance;
let spyCoreError: jest.SpyInstance;
beforeEach(() => { beforeEach(() => {
spyWarning = jest.spyOn(core, 'warning'); spyWarning = core.warning as jest.Mock;
spyWarning.mockImplementation(() => null); spyWarning.mockImplementation(() => null);
spyInfo = jest.spyOn(core, 'info'); spyInfo = core.info as jest.Mock;
spyInfo.mockImplementation(() => null); spyInfo.mockImplementation(() => null);
spyCacheSave = jest.spyOn(cache, 'saveCache'); spyCacheSave = cache.saveCache as jest.Mock;
spyJobStatusSuccess = jest.spyOn(util, 'isJobStatusSuccess'); spyJobStatusSuccess = util.isJobStatusSuccess as jest.Mock;
spyJobStatusSuccess.mockReturnValue(true); spyJobStatusSuccess.mockReturnValue(true);
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
createStateForSuccessfulRestore(); createStateForSuccessfulRestore();
@@ -47,7 +102,7 @@ describe('cleanup', () => {
) )
) )
); );
jest.spyOn(core, 'getInput').mockImplementation((name: string) => { (core.getInput as jest.Mock<any>).mockImplementation((name: string) => {
return name === 'cache' ? 'gradle' : ''; return name === 'cache' ? 'gradle' : '';
}); });
await cleanup(); await cleanup();
@@ -59,7 +114,7 @@ describe('cleanup', () => {
spyCacheSave.mockImplementation((paths: string[], key: string) => spyCacheSave.mockImplementation((paths: string[], key: string) =>
Promise.reject(new Error('Unexpected error')) Promise.reject(new Error('Unexpected error'))
); );
jest.spyOn(core, 'getInput').mockImplementation((name: string) => { (core.getInput as jest.Mock<any>).mockImplementation((name: string) => {
return name === 'cache' ? 'gradle' : ''; return name === 'cache' ? 'gradle' : '';
}); });
await cleanup(); await cleanup();
@@ -68,14 +123,14 @@ describe('cleanup', () => {
}); });
function resetState() { function resetState() {
jest.spyOn(core, 'getState').mockReset(); (core.getState as jest.Mock).mockReset();
} }
/** /**
* Create states to emulate a successful restore process. * Create states to emulate a successful restore process.
*/ */
function createStateForSuccessfulRestore() { function createStateForSuccessfulRestore() {
jest.spyOn(core, 'getState').mockImplementation(name => { (core.getState as jest.Mock<any>).mockImplementation((name: any) => {
switch (name) { switch (name) {
case 'cache-primary-key': case 'cache-primary-key':
return 'setup-java-cache-primary-key'; return 'setup-java-cache-primary-key';

View File

@@ -1,21 +1,59 @@
import {HttpClient} from '@actions/http-client';
import {IAdoptAvailableVersions} from '../../src/distributions/adopt/models';
import { import {
AdoptDistribution, jest,
AdoptImplementation describe,
} from '../../src/distributions/adopt/installer'; it,
import {TemurinDistribution} from '../../src/distributions/temurin/installer'; expect,
import {JavaInstallerOptions} from '../../src/distributions/base-models'; beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import {HttpClient} from '@actions/http-client';
import os from 'os'; import os from 'os';
import manifestData from '../data/adopt.json'; import manifestData from '../data/adopt.json' with {type: 'json'};
import * as core from '@actions/core';
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const {AdoptDistribution, AdoptImplementation} =
await import('../../src/distributions/adopt/installer.js');
const {TemurinDistribution} =
await import('../../src/distributions/temurin/installer.js');
import type {IAdoptAvailableVersions} from '../../src/distributions/adopt/models.js';
import type {AdoptImplementation as AdoptImplementationType} from '../../src/distributions/adopt/installer.js';
import type {JavaInstallerOptions} from '../../src/distributions/base-models.js';
describe('getAvailableVersions', () => { describe('getAvailableVersions', () => {
let spyHttpClient: jest.SpyInstance; let spyHttpClient: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
let spyCoreWarning: jest.SpyInstance; let spyCoreWarning: any;
beforeEach(() => { beforeEach(() => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
@@ -26,9 +64,9 @@ describe('getAvailableVersions', () => {
}); });
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
spyCoreWarning = jest.spyOn(core, 'warning'); spyCoreWarning = core.warning as jest.Mock;
spyCoreWarning.mockImplementation(() => {}); spyCoreWarning.mockImplementation(() => {});
}); });
@@ -123,7 +161,7 @@ describe('getAvailableVersions', () => {
'build correct url for %s', 'build correct url for %s',
async ( async (
installerOptions: JavaInstallerOptions, installerOptions: JavaInstallerOptions,
impl: AdoptImplementation, impl: AdoptImplementationType,
expectedParameters expectedParameters
) => { ) => {
const distribution = new AdoptDistribution(installerOptions, impl); const distribution = new AdoptDistribution(installerOptions, impl);
@@ -204,7 +242,7 @@ describe('getAvailableVersions', () => {
[AdoptImplementation.OpenJ9, 'jre', 'Java_Adopt-OpenJ9_jre'] [AdoptImplementation.OpenJ9, 'jre', 'Java_Adopt-OpenJ9_jre']
])( ])(
'find right toolchain folder', 'find right toolchain folder',
(impl: AdoptImplementation, packageType: string, expected: string) => { (impl: AdoptImplementationType, packageType: string, expected: string) => {
const distribution = new AdoptDistribution( const distribution = new AdoptDistribution(
{ {
version: '11', version: '11',
@@ -263,11 +301,11 @@ describe('findPackageForDownload', () => {
url: 'https://example.test/temurin-11.tar.gz' url: 'https://example.test/temurin-11.tar.gz'
}; };
const temurinFindPackageForDownload = jest const temurinFindPackageForDownload = jest
.fn() .fn<any>()
.mockResolvedValue(temurinRelease); .mockResolvedValue(temurinRelease);
const temurinDistribution = { const temurinDistribution = {
findPackageForDownload: temurinFindPackageForDownload findPackageForDownload: temurinFindPackageForDownload
} as unknown as TemurinDistribution; } as any;
const distribution = new AdoptDistribution( const distribution = new AdoptDistribution(
{ {
@@ -279,7 +317,7 @@ describe('findPackageForDownload', () => {
AdoptImplementation.Hotspot, AdoptImplementation.Hotspot,
temurinDistribution temurinDistribution
); );
const adoptLookupSpy = jest.fn(); const adoptLookupSpy = jest.fn<any>();
distribution['getAvailableVersions'] = adoptLookupSpy; distribution['getAvailableVersions'] = adoptLookupSpy;
const resolvedVersion = await distribution['findPackageForDownload']('11'); const resolvedVersion = await distribution['findPackageForDownload']('11');

View File

@@ -1,19 +1,90 @@
import * as tc from '@actions/tool-cache'; import {
import * as core from '@actions/core'; jest,
import * as util from '../../src/util'; describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import type {
JavaDownloadRelease,
JavaInstallerOptions,
JavaInstallerResults
} from '../../src/distributions/base-models.js';
import path from 'path'; import path from 'path';
import * as semver from 'semver'; import * as semver from 'semver';
import {JavaBase} from '../../src/distributions/base-installer';
import {
JavaDownloadRelease,
JavaInstallerOptions,
JavaInstallerResults
} from '../../src/distributions/base-models';
import os from 'os'; import os from 'os';
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
jest.unstable_mockModule('@actions/tool-cache', () => ({
find: jest.fn(),
findAllVersions: jest.fn(),
downloadTool: jest.fn(),
extractZip: jest.fn(),
extractTar: jest.fn(),
extract7z: jest.fn(),
extractXar: jest.fn(),
cacheDir: jest.fn(),
cacheFile: jest.fn(),
getManifestFromRepo: jest.fn(),
findFromManifest: jest.fn(),
evaluateVersions: jest.fn(),
HTTPError: class HTTPError extends Error {
httpStatusCode: number;
constructor(statusCode: number) {
super(`HTTP Error: ${statusCode}`);
this.httpStatusCode = statusCode;
}
}
}));
const real_util_module = await import('../../src/util.js');
jest.unstable_mockModule('../../src/util.js', () => ({
...real_util_module,
extractJdkFile: jest.fn(),
getDownloadArchiveExtension: jest.fn(),
getToolcachePath: jest.fn(),
isJobStatusSuccess: jest.fn(),
renameWinArchive: jest.fn(),
isVersionSatisfies: real_util_module.isVersionSatisfies,
getTempDir: real_util_module.getTempDir
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const tc = await import('@actions/tool-cache');
const util = await import('../../src/util.js');
const {JavaBase} = await import('../../src/distributions/base-installer.js');
class EmptyJavaBase extends JavaBase { class EmptyJavaBase extends JavaBase {
constructor(installerOptions: JavaInstallerOptions) { constructor(installerOptions: JavaInstallerOptions) {
super('Empty', installerOptions); super('Empty', installerOptions);
@@ -53,12 +124,12 @@ describe('findInToolcache', () => {
const javaPath = path.join('Java_Empty_jdk', actualJavaVersion, 'x64'); const javaPath = path.join('Java_Empty_jdk', actualJavaVersion, 'x64');
let mockJavaBase: EmptyJavaBase; let mockJavaBase: EmptyJavaBase;
let spyGetToolcachePath: jest.SpyInstance; let spyGetToolcachePath: any;
let spyTcFindAllVersions: jest.SpyInstance; let spyTcFindAllVersions: any;
beforeEach(() => { beforeEach(() => {
spyGetToolcachePath = jest.spyOn(util, 'getToolcachePath'); spyGetToolcachePath = util.getToolcachePath as jest.Mock;
spyTcFindAllVersions = jest.spyOn(tc, 'findAllVersions'); spyTcFindAllVersions = tc.findAllVersions as jest.Mock;
}); });
afterEach(() => { afterEach(() => {
@@ -241,17 +312,17 @@ describe('setupJava', () => {
let mockJavaBase: EmptyJavaBase; let mockJavaBase: EmptyJavaBase;
let spyGetToolcachePath: jest.SpyInstance; let spyGetToolcachePath: any;
let spyTcFindAllVersions: jest.SpyInstance; let spyTcFindAllVersions: any;
let spyCoreDebug: jest.SpyInstance; let spyCoreDebug: any;
let spyCoreInfo: jest.SpyInstance; let spyCoreInfo: any;
let spyCoreExportVariable: jest.SpyInstance; let spyCoreExportVariable: any;
let spyCoreAddPath: jest.SpyInstance; let spyCoreAddPath: any;
let spyCoreSetOutput: jest.SpyInstance; let spyCoreSetOutput: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
beforeEach(() => { beforeEach(() => {
spyGetToolcachePath = jest.spyOn(util, 'getToolcachePath'); spyGetToolcachePath = util.getToolcachePath as jest.Mock;
spyGetToolcachePath.mockImplementation( spyGetToolcachePath.mockImplementation(
(toolname: string, javaVersion: string, architecture: string) => { (toolname: string, javaVersion: string, architecture: string) => {
const semverVersion = new semver.Range(javaVersion); const semverVersion = new semver.Range(javaVersion);
@@ -269,27 +340,27 @@ describe('setupJava', () => {
} }
); );
spyTcFindAllVersions = jest.spyOn(tc, 'findAllVersions'); spyTcFindAllVersions = tc.findAllVersions as jest.Mock;
spyTcFindAllVersions.mockReturnValue([installedJavaVersion]); spyTcFindAllVersions.mockReturnValue([installedJavaVersion]);
// Spy on core methods // Spy on core methods
spyCoreDebug = jest.spyOn(core, 'debug'); spyCoreDebug = core.debug as jest.Mock;
spyCoreDebug.mockImplementation(() => undefined); spyCoreDebug.mockImplementation(() => undefined);
spyCoreInfo = jest.spyOn(core, 'info'); spyCoreInfo = core.info as jest.Mock;
spyCoreInfo.mockImplementation(() => undefined); spyCoreInfo.mockImplementation(() => undefined);
spyCoreAddPath = jest.spyOn(core, 'addPath'); spyCoreAddPath = core.addPath as jest.Mock;
spyCoreAddPath.mockImplementation(() => undefined); spyCoreAddPath.mockImplementation(() => undefined);
spyCoreExportVariable = jest.spyOn(core, 'exportVariable'); spyCoreExportVariable = core.exportVariable as jest.Mock;
spyCoreExportVariable.mockImplementation(() => undefined); spyCoreExportVariable.mockImplementation(() => undefined);
spyCoreSetOutput = jest.spyOn(core, 'setOutput'); spyCoreSetOutput = core.setOutput as jest.Mock;
spyCoreSetOutput.mockImplementation(() => undefined); spyCoreSetOutput.mockImplementation(() => undefined);
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => undefined); spyCoreError.mockImplementation(() => undefined);
jest.spyOn(os, 'arch').mockReturnValue('x86' as ReturnType<typeof os.arch>); jest.spyOn(os, 'arch').mockReturnValue('x86' as ReturnType<typeof os.arch>);
@@ -349,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([
[ [
{ {
@@ -673,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
@@ -692,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

@@ -1,17 +1,62 @@
import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import type {JavaInstallerOptions} from '../../src/distributions/base-models.js';
import {HttpClient} from '@actions/http-client'; import {HttpClient} from '@actions/http-client';
import {JavaInstallerOptions} from '../../src/distributions/base-models';
import {CorrettoDistribution} from '../../src/distributions/corretto/installer';
import * as util from '../../src/util';
import os from 'os'; import os from 'os';
import * as core from '@actions/core';
import manifestData from '../data/corretto.json'; import manifestData from '../data/corretto.json' with {type: 'json'};
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
const real_util_module = await import('../../src/util.js');
jest.unstable_mockModule('../../src/util.js', () => ({
...real_util_module,
getDownloadArchiveExtension: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const {CorrettoDistribution} =
await import('../../src/distributions/corretto/installer.js');
const util = await import('../../src/util.js');
describe('getAvailableVersions', () => { describe('getAvailableVersions', () => {
let spyHttpClient: jest.SpyInstance; let spyHttpClient: ReturnType<typeof jest.spyOn>;
let spyGetDownloadArchiveExtension: jest.SpyInstance; let spyGetDownloadArchiveExtension: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
beforeEach(() => { beforeEach(() => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
@@ -20,13 +65,11 @@ describe('getAvailableVersions', () => {
headers: {}, headers: {},
result: manifestData result: manifestData
}); });
spyGetDownloadArchiveExtension = jest.spyOn( spyGetDownloadArchiveExtension =
util, util.getDownloadArchiveExtension as jest.Mock;
'getDownloadArchiveExtension'
);
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
}); });
@@ -161,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({
@@ -235,7 +296,7 @@ describe('getAvailableVersions', () => {
}); });
const mockPlatform = ( const mockPlatform = (
distribution: CorrettoDistribution, distribution: InstanceType<typeof CorrettoDistribution>,
platform: string platform: string
) => { ) => {
distribution['getPlatformOption'] = () => platform; distribution['getPlatformOption'] = () => platform;

View File

@@ -1,14 +1,59 @@
import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import {HttpClient} from '@actions/http-client'; import {HttpClient} from '@actions/http-client';
import {DragonwellDistribution} from '../../src/distributions/dragonwell/installer';
import * as utils from '../../src/util';
import * as core from '@actions/core';
import manifestData from '../data/dragonwell.json'; import manifestData from '../data/dragonwell.json' with {type: 'json'};
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
const real_util_module = await import('../../src/util.js');
jest.unstable_mockModule('../../src/util.js', () => ({
...real_util_module,
getDownloadArchiveExtension: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const {DragonwellDistribution} =
await import('../../src/distributions/dragonwell/installer.js');
const utils = await import('../../src/util.js');
describe('getAvailableVersions', () => { describe('getAvailableVersions', () => {
let spyHttpClient: jest.SpyInstance; let spyHttpClient: any;
let spyUtilGetDownloadArchiveExtension: jest.SpyInstance; let spyUtilGetDownloadArchiveExtension: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
beforeEach(() => { beforeEach(() => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
@@ -18,14 +63,12 @@ describe('getAvailableVersions', () => {
result: manifestData result: manifestData
}); });
spyUtilGetDownloadArchiveExtension = jest.spyOn( spyUtilGetDownloadArchiveExtension =
utils, utils.getDownloadArchiveExtension as jest.Mock;
'getDownloadArchiveExtension'
);
spyUtilGetDownloadArchiveExtension.mockReturnValue('tar.gz'); spyUtilGetDownloadArchiveExtension.mockReturnValue('tar.gz');
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
}); });
@@ -36,7 +79,7 @@ describe('getAvailableVersions', () => {
}); });
const mockPlatform = ( const mockPlatform = (
distribution: DragonwellDistribution, distribution: InstanceType<typeof DragonwellDistribution>,
platform: string platform: string
) => { ) => {
distribution['getPlatformOption'] = () => platform; distribution['getPlatformOption'] = () => platform;

View File

@@ -1,53 +1,116 @@
import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
import * as http from '@actions/http-client';
import fs from 'fs';
import path from 'path';
import { import {
GraalVMCommunityDistribution, jest,
GraalVMDistribution describe,
} from '../../src/distributions/graalvm/installer'; it,
import {getJavaDistribution} from '../../src/distributions/distribution-factory'; expect,
import {JavaInstallerOptions} from '../../src/distributions/base-models'; beforeEach,
import * as util from '../../src/util'; beforeAll,
afterAll
} from '@jest/globals';
import path from 'path';
jest.mock('@actions/core'); // Mock @actions modules
jest.mock('@actions/tool-cache'); jest.unstable_mockModule('@actions/core', () => ({
jest.mock('@actions/http-client'); info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
jest.mock('../../src/util', () => ({ jest.unstable_mockModule('@actions/tool-cache', () => ({
...jest.requireActual('../../src/util'), find: jest.fn(),
findAllVersions: jest.fn(),
downloadTool: jest.fn(),
extractZip: jest.fn(),
extractTar: jest.fn(),
extract7z: jest.fn(),
extractXar: jest.fn(),
cacheDir: jest.fn(),
cacheFile: jest.fn(),
getManifestFromRepo: jest.fn(),
findFromManifest: jest.fn(),
evaluateVersions: jest.fn()
}));
jest.unstable_mockModule('@actions/http-client', () => ({
HttpClient: jest.fn().mockImplementation(() => ({
getJson: jest.fn(),
head: jest.fn(),
get: jest.fn()
})),
HttpClientError: class HttpClientError extends Error {
statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
}
},
HttpCodes: {OK: 200, NotFound: 404, Unauthorized: 401, Forbidden: 403}
}));
// Get real util first, then mock specific functions
const realUtil = await import('../../src/util.js');
jest.unstable_mockModule('../../src/util.js', () => ({
...realUtil,
extractJdkFile: jest.fn(), extractJdkFile: jest.fn(),
getDownloadArchiveExtension: jest.fn(), getDownloadArchiveExtension: jest.fn(),
renameWinArchive: jest.fn(), renameWinArchive: jest.fn(),
getGitHubHttpHeaders: jest.fn().mockReturnValue({Accept: 'application/json'}) getGitHubHttpHeaders: jest.fn().mockReturnValue({Accept: 'application/json'})
})); }));
jest.mock('fs', () => ({ const real_fs_module = await import('fs');
...jest.requireActual('fs'), jest.unstable_mockModule('fs', () => ({
...real_fs_module,
default: {
...real_fs_module.default,
readdirSync: jest.fn(),
existsSync: jest.fn()
},
readdirSync: jest.fn(), readdirSync: jest.fn(),
existsSync: jest.fn() existsSync: jest.fn()
})); }));
// Dynamic imports after mocking
const core = await import('@actions/core');
const tc = await import('@actions/tool-cache');
const http = await import('@actions/http-client');
const fs = (await import('fs')).default;
const util = await import('../../src/util.js');
const {GraalVMCommunityDistribution, GraalVMDistribution} =
await import('../../src/distributions/graalvm/installer.js');
const {getJavaDistribution} =
await import('../../src/distributions/distribution-factory.js');
import type {JavaInstallerOptions} from '../../src/distributions/base-models.js';
beforeAll(() => { beforeAll(() => {
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
if (!jest.isMockFunction(http.HttpClient)) {
throw new Error('HTTP client must be mocked in tests!');
}
if (!jest.isMockFunction(tc.downloadTool)) {
throw new Error('Tool cache downloadTool must be mocked in tests!');
}
console.log('✅ All external dependencies are properly mocked'); console.log('✅ All external dependencies are properly mocked');
}); });
describe('GraalVMDistribution', () => { describe('GraalVMDistribution', () => {
let distribution: GraalVMDistribution; let distribution: InstanceType<typeof GraalVMDistribution>;
let communityDistribution: GraalVMCommunityDistribution; let communityDistribution: InstanceType<typeof GraalVMCommunityDistribution>;
let mockHttpClient: jest.Mocked<http.HttpClient>; let mockHttpClient: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
const defaultOptions: JavaInstallerOptions = { const defaultOptions: JavaInstallerOptions = {
version: '17', version: '17',
@@ -62,23 +125,20 @@ describe('GraalVMDistribution', () => {
distribution = new GraalVMDistribution(defaultOptions); distribution = new GraalVMDistribution(defaultOptions);
communityDistribution = new GraalVMCommunityDistribution(defaultOptions); communityDistribution = new GraalVMCommunityDistribution(defaultOptions);
mockHttpClient = new http.HttpClient() as jest.Mocked<http.HttpClient>; mockHttpClient = new (http.HttpClient as any)();
(distribution as any).http = mockHttpClient; (distribution as any).http = mockHttpClient;
(communityDistribution as any).http = mockHttpClient; (communityDistribution as any).http = mockHttpClient;
(util.getDownloadArchiveExtension as jest.Mock).mockReturnValue('tar.gz'); (util.getDownloadArchiveExtension as jest.Mock<any>).mockReturnValue(
'tar.gz'
);
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
}); });
afterAll(() => { afterAll(() => {
expect(jest.isMockFunction(http.HttpClient)).toBe(true);
expect(jest.isMockFunction(tc.downloadTool)).toBe(true);
expect(jest.isMockFunction(tc.cacheDir)).toBe(true);
jest.restoreAllMocks(); jest.restoreAllMocks();
jest.clearAllMocks(); jest.clearAllMocks();
}); });
@@ -113,20 +173,24 @@ describe('GraalVMDistribution', () => {
}; };
beforeEach(() => { beforeEach(() => {
(tc.downloadTool as jest.Mock).mockResolvedValue('/tmp/archive.tar.gz'); (tc.downloadTool as any).mockResolvedValue('/tmp/archive.tar.gz');
(tc.cacheDir as jest.Mock).mockResolvedValue('/cached/java/path'); (tc.cacheDir as any).mockResolvedValue('/cached/java/path');
(util.extractJdkFile as jest.Mock).mockResolvedValue('/tmp/extracted'); (util.extractJdkFile as any).mockResolvedValue('/tmp/extracted');
// Mock renameWinArchive - it returns the same path (no renaming) // Mock renameWinArchive - it returns the same path (no renaming)
(util.renameWinArchive as jest.Mock).mockImplementation((p: string) => p); (util.renameWinArchive as any).mockImplementation((p: string) => p);
(util.getDownloadArchiveExtension as jest.Mock).mockReturnValue('tar.gz'); (util.getDownloadArchiveExtension as jest.Mock<any>).mockReturnValue(
'tar.gz'
);
// Mock fs.existsSync to return true for extracted path // Mock fs.existsSync to return true for extracted path
(fs.existsSync as jest.Mock).mockReturnValue(true); (fs.existsSync as jest.Mock<any>).mockReturnValue(true);
(fs.readdirSync as jest.Mock).mockReturnValue(['graalvm-jdk-17.0.5']); (fs.readdirSync as jest.Mock<any>).mockReturnValue([
'graalvm-jdk-17.0.5'
]);
jest jest
.spyOn(distribution as any, 'getToolcacheVersionName') .spyOn(distribution as any, 'getToolcacheVersionName')
@@ -173,7 +237,7 @@ describe('GraalVMDistribution', () => {
}); });
it('should throw error when extracted path does not exist', async () => { it('should throw error when extracted path does not exist', async () => {
(fs.existsSync as jest.Mock).mockReturnValue(false); (fs.existsSync as jest.Mock<any>).mockReturnValue(false);
await expect( await expect(
(distribution as any).downloadTool(javaRelease) (distribution as any).downloadTool(javaRelease)
@@ -187,8 +251,8 @@ describe('GraalVMDistribution', () => {
}); });
it('should throw error when extracted directory is empty', async () => { it('should throw error when extracted directory is empty', async () => {
(fs.existsSync as jest.Mock).mockReturnValue(true); (fs.existsSync as jest.Mock<any>).mockReturnValue(true);
(fs.readdirSync as jest.Mock).mockReturnValue([]); (fs.readdirSync as jest.Mock<any>).mockReturnValue([]);
await expect( await expect(
(distribution as any).downloadTool(javaRelease) (distribution as any).downloadTool(javaRelease)
@@ -203,7 +267,7 @@ describe('GraalVMDistribution', () => {
it('should handle download errors', async () => { it('should handle download errors', async () => {
const downloadError = new Error('Network error during download'); const downloadError = new Error('Network error during download');
(tc.downloadTool as jest.Mock).mockRejectedValue(downloadError); (tc.downloadTool as any).mockRejectedValue(downloadError);
await expect( await expect(
(distribution as any).downloadTool(javaRelease) (distribution as any).downloadTool(javaRelease)
@@ -216,7 +280,7 @@ describe('GraalVMDistribution', () => {
it('should handle extraction errors', async () => { it('should handle extraction errors', async () => {
const extractError = new Error('Failed to extract archive'); const extractError = new Error('Failed to extract archive');
(util.extractJdkFile as jest.Mock).mockRejectedValue(extractError); (util.extractJdkFile as any).mockRejectedValue(extractError);
await expect( await expect(
(distribution as any).downloadTool(javaRelease) (distribution as any).downloadTool(javaRelease)
@@ -229,8 +293,10 @@ describe('GraalVMDistribution', () => {
it('should handle different archive extensions', async () => { it('should handle different archive extensions', async () => {
// Test with a .zip file // Test with a .zip file
(util.getDownloadArchiveExtension as jest.Mock).mockReturnValue('zip'); (util.getDownloadArchiveExtension as jest.Mock<any>).mockReturnValue(
(tc.downloadTool as jest.Mock).mockResolvedValue('/tmp/archive.zip'); 'zip'
);
(tc.downloadTool as any).mockResolvedValue('/tmp/archive.zip');
const zipRelease = { const zipRelease = {
version: '17.0.5', version: '17.0.5',
@@ -309,7 +375,7 @@ describe('GraalVMDistribution', () => {
it('should construct correct URL for specific version', async () => { it('should construct correct URL for specific version', async () => {
const mockResponse = { const mockResponse = {
message: {statusCode: 200} message: {statusCode: 200}
} as http.HttpClientResponse; } as any;
mockHttpClient.head.mockResolvedValue(mockResponse); mockHttpClient.head.mockResolvedValue(mockResponse);
const result = await (distribution as any).findPackageForDownload( const result = await (distribution as any).findPackageForDownload(
@@ -326,7 +392,7 @@ describe('GraalVMDistribution', () => {
it('should construct correct URL for major version (latest)', async () => { it('should construct correct URL for major version (latest)', async () => {
const mockResponse = { const mockResponse = {
message: {statusCode: 200} message: {statusCode: 200}
} as http.HttpClientResponse; } as any;
mockHttpClient.head.mockResolvedValue(mockResponse); mockHttpClient.head.mockResolvedValue(mockResponse);
const result = await (distribution as any).findPackageForDownload('21'); const result = await (distribution as any).findPackageForDownload('21');
@@ -351,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')
@@ -374,7 +495,7 @@ describe('GraalVMDistribution', () => {
it('should throw error when file not found (404)', async () => { it('should throw error when file not found (404)', async () => {
const mockResponse = { const mockResponse = {
message: {statusCode: 404} message: {statusCode: 404}
} as http.HttpClientResponse; } as any;
mockHttpClient.head.mockResolvedValue(mockResponse); mockHttpClient.head.mockResolvedValue(mockResponse);
// Verify the error is thrown with the expected message // Verify the error is thrown with the expected message
@@ -395,7 +516,7 @@ describe('GraalVMDistribution', () => {
it('should throw error for unauthorized access (401)', async () => { it('should throw error for unauthorized access (401)', async () => {
const mockResponse = { const mockResponse = {
message: {statusCode: 401} message: {statusCode: 401}
} as http.HttpClientResponse; } as any;
mockHttpClient.head.mockResolvedValue(mockResponse); mockHttpClient.head.mockResolvedValue(mockResponse);
await expect( await expect(
@@ -408,7 +529,7 @@ describe('GraalVMDistribution', () => {
it('should throw error for forbidden access (403)', async () => { it('should throw error for forbidden access (403)', async () => {
const mockResponse = { const mockResponse = {
message: {statusCode: 403} message: {statusCode: 403}
} as http.HttpClientResponse; } as any;
mockHttpClient.head.mockResolvedValue(mockResponse); mockHttpClient.head.mockResolvedValue(mockResponse);
await expect( await expect(
@@ -424,7 +545,7 @@ describe('GraalVMDistribution', () => {
statusCode: 500, statusCode: 500,
statusMessage: 'Internal Server Error' statusMessage: 'Internal Server Error'
} }
} as http.HttpClientResponse; } as any;
mockHttpClient.head.mockResolvedValue(mockResponse); mockHttpClient.head.mockResolvedValue(mockResponse);
await expect( await expect(
@@ -437,7 +558,7 @@ describe('GraalVMDistribution', () => {
it('should throw error for other HTTP errors without status message', async () => { it('should throw error for other HTTP errors without status message', async () => {
const mockResponse = { const mockResponse = {
message: {statusCode: 500} message: {statusCode: 500}
} as http.HttpClientResponse; } as any;
mockHttpClient.head.mockResolvedValue(mockResponse); mockHttpClient.head.mockResolvedValue(mockResponse);
await expect( await expect(
@@ -713,7 +834,7 @@ describe('GraalVMDistribution', () => {
} }
]; ];
let fetchEASpy: jest.SpyInstance; let fetchEASpy: any;
beforeEach(() => { beforeEach(() => {
fetchEASpy = jest.spyOn(distribution as any, 'fetchEAJson'); fetchEASpy = jest.spyOn(distribution as any, 'fetchEAJson');
@@ -932,7 +1053,7 @@ describe('GraalVMDistribution', () => {
const mockResponse = { const mockResponse = {
message: {statusCode: 200} message: {statusCode: 200}
} as http.HttpClientResponse; } as any;
mockHttpClient.head.mockResolvedValue(mockResponse); mockHttpClient.head.mockResolvedValue(mockResponse);
const result = await (distribution as any).findPackageForDownload('17'); const result = await (distribution as any).findPackageForDownload('17');
@@ -960,7 +1081,7 @@ describe('GraalVMDistribution', () => {
const mockResponse = { const mockResponse = {
message: {statusCode: 200} message: {statusCode: 200}
} as http.HttpClientResponse; } as any;
mockHttpClient.head.mockResolvedValue(mockResponse); mockHttpClient.head.mockResolvedValue(mockResponse);
const result = await (distribution as any).findPackageForDownload('17'); const result = await (distribution as any).findPackageForDownload('17');
@@ -1049,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

@@ -1,14 +1,53 @@
import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import https from 'https'; import https from 'https';
import {HttpClient} from '@actions/http-client'; import {HttpClient} from '@actions/http-client';
import {JetBrainsDistribution} from '../../src/distributions/jetbrains/installer';
import manifestData from '../data/jetbrains.json'; import manifestData from '../data/jetbrains.json' with {type: 'json'};
import os from 'os'; import os from 'os';
import * as core from '@actions/core';
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const {JetBrainsDistribution} =
await import('../../src/distributions/jetbrains/installer.js');
describe('getAvailableVersions', () => { describe('getAvailableVersions', () => {
let spyHttpClient: jest.SpyInstance; let spyHttpClient: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
beforeEach(() => { beforeEach(() => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
@@ -19,7 +58,7 @@ describe('getAvailableVersions', () => {
}); });
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
}); });
@@ -31,11 +70,17 @@ describe('getAvailableVersions', () => {
it('load available versions', async () => { it('load available versions', async () => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
spyHttpClient.mockReturnValueOnce({ spyHttpClient
statusCode: 200, .mockReturnValueOnce({
headers: {}, statusCode: 200,
result: manifestData as any headers: {},
}); result: manifestData as any
})
.mockReturnValue({
statusCode: 200,
headers: {},
result: []
});
const distribution = new JetBrainsDistribution({ const distribution = new JetBrainsDistribution({
version: '17', version: '17',

View File

@@ -1,6 +1,6 @@
import {KonaDistribution} from '../../src/distributions/kona/installer'; import {KonaDistribution} from '../../src/distributions/kona/installer.js';
import manifestData from '../data/kona.json'; import manifestData from '../data/kona.json' with {type: 'json'};
function mockDistr( function mockDistr(
version: string, version: string,

View File

@@ -1,17 +1,56 @@
import {LibericaDistributions} from '../../src/distributions/liberica/installer';
import { import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import type {
ArchitectureOptions, ArchitectureOptions,
LibericaVersion LibericaVersion
} from '../../src/distributions/liberica/models'; } from '../../src/distributions/liberica/models.js';
import {HttpClient} from '@actions/http-client'; import {HttpClient} from '@actions/http-client';
import os from 'os'; import os from 'os';
import * as core from '@actions/core';
import manifestData from '../data/liberica.json'; import manifestData from '../data/liberica.json' with {type: 'json'};
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const {LibericaDistributions} =
await import('../../src/distributions/liberica/installer.js');
describe('getAvailableVersions', () => { describe('getAvailableVersions', () => {
let spyHttpClient: jest.SpyInstance; let spyHttpClient: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
beforeEach(() => { beforeEach(() => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
@@ -22,7 +61,7 @@ describe('getAvailableVersions', () => {
}); });
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
}); });
@@ -184,7 +223,7 @@ describe('getArchitectureOptions', () => {
}); });
describe('findPackageForDownload', () => { describe('findPackageForDownload', () => {
let distribution: LibericaDistributions; let distribution: InstanceType<typeof LibericaDistributions>;
beforeEach(() => { beforeEach(() => {
distribution = new LibericaDistributions({ distribution = new LibericaDistributions({

View File

@@ -1,17 +1,56 @@
import {LibericaDistributions} from '../../src/distributions/liberica/installer';
import { import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import type {
ArchitectureOptions, ArchitectureOptions,
LibericaVersion LibericaVersion
} from '../../src/distributions/liberica/models'; } from '../../src/distributions/liberica/models.js';
import {HttpClient} from '@actions/http-client'; import {HttpClient} from '@actions/http-client';
import os from 'os'; import os from 'os';
import * as core from '@actions/core';
import manifestData from '../data/liberica-linux.json'; import manifestData from '../data/liberica-linux.json' with {type: 'json'};
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const {LibericaDistributions} =
await import('../../src/distributions/liberica/installer.js');
describe('getAvailableVersions', () => { describe('getAvailableVersions', () => {
let spyHttpClient: jest.SpyInstance; let spyHttpClient: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
beforeEach(() => { beforeEach(() => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
@@ -22,7 +61,7 @@ describe('getAvailableVersions', () => {
}); });
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
}); });
@@ -184,7 +223,7 @@ describe('getArchitectureOptions', () => {
}); });
describe('findPackageForDownload', () => { describe('findPackageForDownload', () => {
let distribution: LibericaDistributions; let distribution: InstanceType<typeof LibericaDistributions>;
beforeEach(() => { beforeEach(() => {
distribution = new LibericaDistributions({ distribution = new LibericaDistributions({

View File

@@ -1,17 +1,56 @@
import {LibericaDistributions} from '../../src/distributions/liberica/installer';
import { import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import type {
ArchitectureOptions, ArchitectureOptions,
LibericaVersion LibericaVersion
} from '../../src/distributions/liberica/models'; } from '../../src/distributions/liberica/models.js';
import {HttpClient} from '@actions/http-client'; import {HttpClient} from '@actions/http-client';
import os from 'os'; import os from 'os';
import * as core from '@actions/core';
import manifestData from '../data/liberica-windows.json'; import manifestData from '../data/liberica-windows.json' with {type: 'json'};
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const {LibericaDistributions} =
await import('../../src/distributions/liberica/installer.js');
describe('getAvailableVersions', () => { describe('getAvailableVersions', () => {
let spyHttpClient: jest.SpyInstance; let spyHttpClient: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
beforeEach(() => { beforeEach(() => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
@@ -21,7 +60,7 @@ describe('getAvailableVersions', () => {
result: manifestData as LibericaVersion[] result: manifestData as LibericaVersion[]
}); });
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
}); });
@@ -183,7 +222,7 @@ describe('getArchitectureOptions', () => {
}); });
describe('findPackageForDownload', () => { describe('findPackageForDownload', () => {
let distribution: LibericaDistributions; let distribution: InstanceType<typeof LibericaDistributions>;
beforeEach(() => { beforeEach(() => {
distribution = new LibericaDistributions({ distribution = new LibericaDistributions({

View File

@@ -1,37 +1,101 @@
import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import fs from 'fs'; import fs from 'fs';
import * as tc from '@actions/tool-cache';
import * as core from '@actions/core';
import path from 'path'; import path from 'path';
import * as semver from 'semver'; import * as semver from 'semver';
import * as util from '../../src/util';
import {LocalDistribution} from '../../src/distributions/local/installer'; // Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
jest.unstable_mockModule('@actions/tool-cache', () => ({
find: jest.fn(),
findAllVersions: jest.fn(),
downloadTool: jest.fn(),
extractZip: jest.fn(),
extractTar: jest.fn(),
extract7z: jest.fn(),
extractXar: jest.fn(),
cacheDir: jest.fn(),
cacheFile: jest.fn(),
getManifestFromRepo: jest.fn(),
findFromManifest: jest.fn(),
evaluateVersions: jest.fn()
}));
const real_util_module = await import('../../src/util.js');
jest.unstable_mockModule('../../src/util.js', () => ({
...real_util_module,
extractJdkFile: jest.fn(),
getDownloadArchiveExtension: jest.fn(),
getToolcachePath: jest.fn(),
isJobStatusSuccess: jest.fn(),
renameWinArchive: jest.fn(),
isVersionSatisfies: real_util_module.isVersionSatisfies,
getTempDir: real_util_module.getTempDir
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const tc = await import('@actions/tool-cache');
const util = await import('../../src/util.js');
const {LocalDistribution} =
await import('../../src/distributions/local/installer.js');
describe('setupJava', () => { describe('setupJava', () => {
const actualJavaVersion = '11.1.10'; const actualJavaVersion = '11.1.10';
const javaPath = path.join('Java_jdkfile_jdk', actualJavaVersion, 'x86'); const javaPath = path.join('Java_jdkfile_jdk', actualJavaVersion, 'x86');
let mockJavaBase: LocalDistribution; let mockJavaBase: InstanceType<typeof LocalDistribution>;
let spyGetToolcachePath: jest.SpyInstance; let spyGetToolcachePath: any;
let spyTcCacheDir: jest.SpyInstance; let spyTcCacheDir: any;
let spyTcFindAllVersions: jest.SpyInstance; let spyTcFindAllVersions: any;
let spyCoreDebug: jest.SpyInstance; let spyCoreDebug: any;
let spyCoreInfo: jest.SpyInstance; let spyCoreInfo: any;
let spyCoreExportVariable: jest.SpyInstance; let spyCoreExportVariable: any;
let spyCoreAddPath: jest.SpyInstance; let spyCoreAddPath: any;
let spyCoreSetOutput: jest.SpyInstance; let spyCoreSetOutput: any;
let spyFsStat: jest.SpyInstance; let spyFsStat: any;
let spyFsReadDir: jest.SpyInstance; let spyFsReadDir: any;
let spyUtilsExtractJdkFile: jest.SpyInstance; let spyUtilsExtractJdkFile: any;
let spyPathResolve: jest.SpyInstance; let spyPathResolve: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
const expectedJdkFile = 'JavaLocalJdkFile'; const expectedJdkFile = 'JavaLocalJdkFile';
beforeEach(() => { beforeEach(() => {
spyGetToolcachePath = jest.spyOn(util, 'getToolcachePath'); spyGetToolcachePath = util.getToolcachePath as jest.Mock;
spyGetToolcachePath.mockImplementation( spyGetToolcachePath.mockImplementation(
(toolname: string, javaVersion: string, architecture: string) => { (toolname: string, javaVersion: string, architecture: string) => {
const semverVersion = new semver.Range(javaVersion); const semverVersion = new semver.Range(javaVersion);
@@ -49,7 +113,7 @@ describe('setupJava', () => {
} }
); );
spyTcCacheDir = jest.spyOn(tc, 'cacheDir'); spyTcCacheDir = tc.cacheDir as jest.Mock;
spyTcCacheDir.mockImplementation( spyTcCacheDir.mockImplementation(
( (
archivePath: string, archivePath: string,
@@ -59,23 +123,23 @@ describe('setupJava', () => {
) => path.join(toolcacheFolderName, version, architecture) ) => path.join(toolcacheFolderName, version, architecture)
); );
spyTcFindAllVersions = jest.spyOn(tc, 'findAllVersions'); spyTcFindAllVersions = tc.findAllVersions as jest.Mock;
spyTcFindAllVersions.mockReturnValue([actualJavaVersion]); spyTcFindAllVersions.mockReturnValue([actualJavaVersion]);
// Spy on core methods // Spy on core methods
spyCoreDebug = jest.spyOn(core, 'debug'); spyCoreDebug = core.debug as jest.Mock;
spyCoreDebug.mockImplementation(() => undefined); spyCoreDebug.mockImplementation(() => undefined);
spyCoreInfo = jest.spyOn(core, 'info'); spyCoreInfo = core.info as jest.Mock;
spyCoreInfo.mockImplementation(() => undefined); spyCoreInfo.mockImplementation(() => undefined);
spyCoreAddPath = jest.spyOn(core, 'addPath'); spyCoreAddPath = core.addPath as jest.Mock;
spyCoreAddPath.mockImplementation(() => undefined); spyCoreAddPath.mockImplementation(() => undefined);
spyCoreExportVariable = jest.spyOn(core, 'exportVariable'); spyCoreExportVariable = core.exportVariable as jest.Mock;
spyCoreExportVariable.mockImplementation(() => undefined); spyCoreExportVariable.mockImplementation(() => undefined);
spyCoreSetOutput = jest.spyOn(core, 'setOutput'); spyCoreSetOutput = core.setOutput as jest.Mock;
spyCoreSetOutput.mockImplementation(() => undefined); spyCoreSetOutput.mockImplementation(() => undefined);
// Spy on fs methods // Spy on fs methods
@@ -88,7 +152,7 @@ describe('setupJava', () => {
}); });
// Spy on util methods // Spy on util methods
spyUtilsExtractJdkFile = jest.spyOn(util, 'extractJdkFile'); spyUtilsExtractJdkFile = util.extractJdkFile as jest.Mock;
spyUtilsExtractJdkFile.mockImplementation(() => 'some/random/path/'); spyUtilsExtractJdkFile.mockImplementation(() => 'some/random/path/');
// Spy on path methods // Spy on path methods
@@ -96,7 +160,7 @@ describe('setupJava', () => {
spyPathResolve.mockImplementation((path: string) => path); spyPathResolve.mockImplementation((path: string) => path);
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
}); });
@@ -106,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

@@ -1,23 +1,115 @@
import { import {
MicrosoftDistributions, jest,
MICROSOFT_PUBLIC_KEY describe,
} from '../../src/distributions/microsoft/installer'; it,
import os from 'os'; expect,
import data from '../data/microsoft.json'; beforeEach,
import * as httpm from '@actions/http-client'; afterEach,
import * as core from '@actions/core'; beforeAll,
import * as tc from '@actions/tool-cache'; afterAll
import * as gpg from '../../src/gpg'; } from '@jest/globals';
import * as util from '../../src/util'; import {HttpClient} from '@actions/http-client';
import fs from 'fs'; import data from '../data/microsoft.json' with {type: 'json'};
const mockOsArch = jest.fn(() => 'x64');
const mockOsPlatform = jest.fn(() => 'linux');
const real_os_module = await import('os');
jest.unstable_mockModule('os', () => ({
...real_os_module,
default: {
...real_os_module.default,
arch: mockOsArch,
platform: mockOsPlatform,
homedir: real_os_module.default.homedir
},
arch: mockOsArch,
platform: mockOsPlatform
}));
const real_fs_module = await import('fs');
const mockReaddirSync = jest.fn();
jest.unstable_mockModule('fs', () => ({
...real_fs_module,
default: {
...real_fs_module.default,
readdirSync: mockReaddirSync
},
readdirSync: mockReaddirSync
}));
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
const real_tc_module = await import('@actions/tool-cache');
jest.unstable_mockModule('@actions/tool-cache', () => ({
...real_tc_module,
downloadTool: jest.fn(),
cacheDir: jest.fn(),
cacheFile: jest.fn()
}));
const real_util_module = await import('../../src/util.js');
jest.unstable_mockModule('../../src/util.js', () => ({
...real_util_module,
extractJdkFile: jest.fn(),
getDownloadArchiveExtension: jest.fn(),
getToolcachePath: jest.fn(),
isJobStatusSuccess: jest.fn(),
renameWinArchive: jest.fn(),
isVersionSatisfies: real_util_module.isVersionSatisfies,
getTempDir: real_util_module.getTempDir
}));
jest.unstable_mockModule('../../src/gpg.js', () => ({
importKey: jest.fn(),
deleteKey: jest.fn(),
verifyPackageSignature: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const gpg = await import('../../src/gpg.js');
const tc = await import('@actions/tool-cache');
const os = (await import('os')).default;
const fs = (await import('fs')).default;
const {MicrosoftDistributions, MICROSOFT_PUBLIC_KEY} =
await import('../../src/distributions/microsoft/installer.js');
const util = await import('../../src/util.js');
describe('findPackageForDownload', () => { describe('findPackageForDownload', () => {
let distribution: MicrosoftDistributions; let distribution: InstanceType<typeof MicrosoftDistributions>;
let spyGetManifestFromRepo: jest.SpyInstance; let spyGetManifestFromRepo: any;
let spyDebug: jest.SpyInstance; let spyDebug: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
beforeEach(() => { beforeEach(() => {
mockOsArch.mockReturnValue('x64');
mockOsPlatform.mockReturnValue(process.platform);
distribution = new MicrosoftDistributions({ distribution = new MicrosoftDistributions({
version: '', version: '',
architecture: 'x64', architecture: 'x64',
@@ -25,18 +117,18 @@ describe('findPackageForDownload', () => {
checkLatest: false checkLatest: false
}); });
spyGetManifestFromRepo = jest.spyOn(httpm.HttpClient.prototype, 'getJson'); spyGetManifestFromRepo = jest.spyOn(HttpClient.prototype, 'getJson');
spyGetManifestFromRepo.mockReturnValue({ spyGetManifestFromRepo.mockReturnValue({
result: data, result: data,
statusCode: 200, statusCode: 200,
headers: {} headers: {}
}); });
spyDebug = jest.spyOn(core, 'debug'); spyDebug = core.debug as jest.Mock;
spyDebug.mockImplementation(() => {}); spyDebug.mockImplementation(() => {});
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
}); });
@@ -118,10 +210,8 @@ describe('findPackageForDownload', () => {
])( ])(
'defaults to os.arch(): %s mapped to distro arch: %s', 'defaults to os.arch(): %s mapped to distro arch: %s',
async (osArch: string, distroArch: string) => { async (osArch: string, distroArch: string) => {
jest mockOsArch.mockReturnValue(osArch);
.spyOn(os, 'arch') mockOsPlatform.mockReturnValue('darwin');
.mockReturnValue(osArch as ReturnType<typeof os.arch>);
jest.spyOn(os, 'platform').mockReturnValue('darwin');
const version = '17'; const version = '17';
const distro = new MicrosoftDistributions({ const distro = new MicrosoftDistributions({
@@ -144,10 +234,8 @@ describe('findPackageForDownload', () => {
])( ])(
'defaults to os.arch(): %s mapped to distro arch: %s', 'defaults to os.arch(): %s mapped to distro arch: %s',
async (osArch: string, distroArch: string) => { async (osArch: string, distroArch: string) => {
jest mockOsArch.mockReturnValue(osArch);
.spyOn(os, 'arch') mockOsPlatform.mockReturnValue('linux');
.mockReturnValue(osArch as ReturnType<typeof os.arch>);
jest.spyOn(os, 'platform').mockReturnValue('linux');
const version = '17'; const version = '17';
const distro = new MicrosoftDistributions({ const distro = new MicrosoftDistributions({
@@ -170,10 +258,8 @@ describe('findPackageForDownload', () => {
])( ])(
'defaults to os.arch(): %s mapped to distro arch: %s', 'defaults to os.arch(): %s mapped to distro arch: %s',
async (osArch: string, distroArch: string) => { async (osArch: string, distroArch: string) => {
jest mockOsArch.mockReturnValue(osArch);
.spyOn(os, 'arch') mockOsPlatform.mockReturnValue('win32');
.mockReturnValue(osArch as ReturnType<typeof os.arch>);
jest.spyOn(os, 'platform').mockReturnValue('win32');
const version = '17'; const version = '17';
const distro = new MicrosoftDistributions({ const distro = new MicrosoftDistributions({
@@ -217,7 +303,7 @@ describe('findPackageForDownload', () => {
statusCode: 200, statusCode: 200,
headers: {} headers: {}
}); });
jest.spyOn(os, 'platform').mockReturnValue('linux'); mockOsPlatform.mockReturnValue('linux');
const result = await distribution['findPackageForDownload']('17.0.10'); const result = await distribution['findPackageForDownload']('17.0.10');
@@ -228,16 +314,14 @@ describe('findPackageForDownload', () => {
}); });
describe('downloadTool', () => { describe('downloadTool', () => {
let spyDownloadTool: jest.SpyInstance; let spyDownloadTool: any;
let spyExtractJdkFile: jest.SpyInstance; let spyExtractJdkFile: any;
let spyCacheDir: jest.SpyInstance; let spyCacheDir: any;
let spyVerifySignature: jest.SpyInstance; let spyVerifySignature: any;
let distribution: MicrosoftDistributions; let distribution: InstanceType<typeof MicrosoftDistributions>;
beforeEach(() => { beforeEach(() => {
jest mockOsPlatform.mockReturnValue(process.platform);
.spyOn(os, 'platform')
.mockReturnValue(process.platform as ReturnType<typeof os.platform>);
distribution = new MicrosoftDistributions({ distribution = new MicrosoftDistributions({
version: '17', version: '17',
@@ -246,27 +330,27 @@ describe('downloadTool', () => {
checkLatest: false checkLatest: false
}); });
spyDownloadTool = jest.spyOn(tc, 'downloadTool'); spyDownloadTool = tc.downloadTool as jest.Mock;
spyDownloadTool.mockImplementation(async () => { spyDownloadTool.mockImplementation(async () => {
return '/tmp/jdk.tar.gz'; return '/tmp/jdk.tar.gz';
}); });
spyExtractJdkFile = jest.spyOn(util, 'extractJdkFile'); spyExtractJdkFile = util.extractJdkFile as jest.Mock;
spyExtractJdkFile.mockImplementation(async () => { spyExtractJdkFile.mockImplementation(async () => {
return '/tmp/unpacked'; return '/tmp/unpacked';
}); });
jest.spyOn(fs, 'readdirSync').mockReturnValue(['jdk'] as any); mockReaddirSync.mockReturnValue(['jdk'] as any);
spyCacheDir = jest.spyOn(tc, 'cacheDir'); spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir.mockImplementation(async () => { spyCacheDir.mockImplementation(async () => {
return '/tmp/cached'; return '/tmp/cached';
}); });
jest (util.renameWinArchive as jest.Mock<any>).mockImplementation(
.spyOn(util, 'renameWinArchive') (archivePath: string) => `${archivePath}.zip`
.mockImplementation((archivePath: string) => `${archivePath}.zip`); );
spyVerifySignature = jest.spyOn(gpg, 'verifyPackageSignature'); spyVerifySignature = gpg.verifyPackageSignature as jest.Mock;
spyVerifySignature.mockImplementation(async () => {}); spyVerifySignature.mockImplementation(async () => {});
}); });

View File

@@ -1,14 +1,53 @@
import {OracleDistribution} from '../../src/distributions/oracle/installer'; import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import os from 'os'; import os from 'os';
import * as core from '@actions/core';
import {getDownloadArchiveExtension} from '../../src/util';
import {HttpClient} from '@actions/http-client'; import {HttpClient} from '@actions/http-client';
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const {OracleDistribution} =
await import('../../src/distributions/oracle/installer.js');
const {getDownloadArchiveExtension} = await import('../../src/util.js');
describe('findPackageForDownload', () => { describe('findPackageForDownload', () => {
let distribution: OracleDistribution; let distribution: InstanceType<typeof OracleDistribution>;
let spyDebug: jest.SpyInstance; let spyDebug: any;
let spyHttpClient: jest.SpyInstance; let spyHttpClient: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
beforeEach(() => { beforeEach(() => {
distribution = new OracleDistribution({ distribution = new OracleDistribution({
@@ -18,11 +57,11 @@ describe('findPackageForDownload', () => {
checkLatest: false checkLatest: false
}); });
spyDebug = jest.spyOn(core, 'debug'); spyDebug = core.debug as jest.Mock;
spyDebug.mockImplementation(() => {}); spyDebug.mockImplementation(() => {});
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
}); });
@@ -135,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

@@ -1,14 +1,59 @@
import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import {HttpClient} from '@actions/http-client'; import {HttpClient} from '@actions/http-client';
import {SapMachineDistribution} from '../../src/distributions/sapmachine/installer';
import * as utils from '../../src/util';
import * as core from '@actions/core';
import manifestData from '../data/sapmachine.json'; import manifestData from '../data/sapmachine.json' with {type: 'json'};
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
const real_util_module = await import('../../src/util.js');
jest.unstable_mockModule('../../src/util.js', () => ({
...real_util_module,
getDownloadArchiveExtension: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const {SapMachineDistribution} =
await import('../../src/distributions/sapmachine/installer.js');
const utils = await import('../../src/util.js');
describe('getAvailableVersions', () => { describe('getAvailableVersions', () => {
let spyHttpClient: jest.SpyInstance; let spyHttpClient: any;
let spyUtilGetDownloadArchiveExtension: jest.SpyInstance; let spyUtilGetDownloadArchiveExtension: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
beforeEach(() => { beforeEach(() => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
@@ -18,14 +63,12 @@ describe('getAvailableVersions', () => {
result: manifestData result: manifestData
}); });
spyUtilGetDownloadArchiveExtension = jest.spyOn( spyUtilGetDownloadArchiveExtension =
utils, utils.getDownloadArchiveExtension as jest.Mock<any>;
'getDownloadArchiveExtension'
);
spyUtilGetDownloadArchiveExtension.mockReturnValue('tar.gz'); spyUtilGetDownloadArchiveExtension.mockReturnValue('tar.gz');
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
}); });
@@ -36,7 +79,7 @@ describe('getAvailableVersions', () => {
}); });
const mockPlatform = ( const mockPlatform = (
distribution: SapMachineDistribution, distribution: InstanceType<typeof SapMachineDistribution>,
platform: string platform: string
) => { ) => {
distribution['getPlatformOption'] = () => platform; distribution['getPlatformOption'] = () => platform;

View File

@@ -1,15 +1,53 @@
import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import type {JavaInstallerOptions} from '../../src/distributions/base-models.js';
import {HttpClient} from '@actions/http-client'; import {HttpClient} from '@actions/http-client';
import {JavaInstallerOptions} from '../../src/distributions/base-models'; import manifestData from '../data/semeru.json' with {type: 'json'};
import {SemeruDistribution} from '../../src/distributions/semeru/installer';
import manifestData from '../data/semeru.json'; // Mock @actions/core before importing source modules that depend on it
import * as core from '@actions/core'; jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const {SemeruDistribution} =
await import('../../src/distributions/semeru/installer.js');
describe('getAvailableVersions', () => { describe('getAvailableVersions', () => {
let spyHttpClient: jest.SpyInstance; let spyHttpClient: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
let spyCoreWarning: jest.SpyInstance; let spyCoreWarning: any;
beforeEach(() => { beforeEach(() => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
@@ -19,9 +57,9 @@ describe('getAvailableVersions', () => {
result: [] result: []
}); });
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
spyCoreWarning = jest.spyOn(core, 'warning'); spyCoreWarning = core.warning as jest.Mock;
spyCoreWarning.mockImplementation(() => {}); spyCoreWarning.mockImplementation(() => {});
}); });

View File

@@ -1,23 +1,92 @@
import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import type {JavaInstallerOptions} from '../../src/distributions/base-models.js';
import type {TemurinImplementation as TemurinImplementationType} from '../../src/distributions/temurin/installer.js';
import {HttpClient} from '@actions/http-client'; import {HttpClient} from '@actions/http-client';
import * as tc from '@actions/tool-cache';
import fs from 'fs'; import fs from 'fs';
import os from 'os'; import os from 'os';
import {
TemurinDistribution,
TemurinImplementation,
ADOPTIUM_PUBLIC_KEY
} from '../../src/distributions/temurin/installer';
import {JavaInstallerOptions} from '../../src/distributions/base-models';
import * as util from '../../src/util';
import * as gpg from '../../src/gpg';
import manifestData from '../data/temurin.json'; import manifestData from '../data/temurin.json' with {type: 'json'};
import * as core from '@actions/core';
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
jest.unstable_mockModule('@actions/tool-cache', () => ({
find: jest.fn(),
findAllVersions: jest.fn(),
downloadTool: jest.fn(),
extractZip: jest.fn(),
extractTar: jest.fn(),
extract7z: jest.fn(),
extractXar: jest.fn(),
cacheDir: jest.fn(),
cacheFile: jest.fn(),
getManifestFromRepo: jest.fn(),
findFromManifest: jest.fn(),
evaluateVersions: jest.fn()
}));
const real_util_module = await import('../../src/util.js');
jest.unstable_mockModule('../../src/util.js', () => ({
...real_util_module,
extractJdkFile: jest.fn(),
getDownloadArchiveExtension: jest.fn(),
getToolcachePath: jest.fn(),
isJobStatusSuccess: jest.fn(),
renameWinArchive: jest.fn(),
isVersionSatisfies: real_util_module.isVersionSatisfies,
getTempDir: real_util_module.getTempDir
}));
jest.unstable_mockModule('../../src/gpg.js', () => ({
importKey: jest.fn(),
deleteKey: jest.fn(),
verifyPackageSignature: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const gpg = await import('../../src/gpg.js');
const tc = await import('@actions/tool-cache');
const {TemurinDistribution, TemurinImplementation, ADOPTIUM_PUBLIC_KEY} =
await import('../../src/distributions/temurin/installer.js');
const util = await import('../../src/util.js');
describe('getAvailableVersions', () => { describe('getAvailableVersions', () => {
let spyHttpClient: jest.SpyInstance; let spyHttpClient: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
let spyCoreWarning: jest.SpyInstance; let spyCoreWarning: any;
beforeEach(() => { beforeEach(() => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
@@ -27,9 +96,9 @@ describe('getAvailableVersions', () => {
result: [] result: []
}); });
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
spyCoreWarning = jest.spyOn(core, 'warning'); spyCoreWarning = core.warning as jest.Mock;
spyCoreWarning.mockImplementation(() => {}); spyCoreWarning.mockImplementation(() => {});
}); });
@@ -84,7 +153,7 @@ describe('getAvailableVersions', () => {
'build correct url for %s', 'build correct url for %s',
async ( async (
installerOptions: JavaInstallerOptions, installerOptions: JavaInstallerOptions,
impl: TemurinImplementation, impl: TemurinImplementationType,
expectedParameters expectedParameters
) => { ) => {
const distribution = new TemurinDistribution(installerOptions, impl); const distribution = new TemurinDistribution(installerOptions, impl);
@@ -163,7 +232,11 @@ describe('getAvailableVersions', () => {
[TemurinImplementation.Hotspot, 'jre', 'Java_Temurin-Hotspot_jre'] [TemurinImplementation.Hotspot, 'jre', 'Java_Temurin-Hotspot_jre']
])( ])(
'find right toolchain folder', 'find right toolchain folder',
(impl: TemurinImplementation, packageType: string, expected: string) => { (
impl: TemurinImplementationType,
packageType: string,
expected: string
) => {
const distribution = new TemurinDistribution( const distribution = new TemurinDistribution(
{ {
version: '8', version: '8',
@@ -239,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(
{ {
@@ -289,25 +380,25 @@ describe('findPackageForDownload', () => {
}); });
describe('downloadTool', () => { describe('downloadTool', () => {
let spyDownloadTool: jest.SpyInstance; let spyDownloadTool: any;
let spyVerifySignature: jest.SpyInstance; let spyVerifySignature: any;
let spyExtractJdkFile: jest.SpyInstance; let spyExtractJdkFile: any;
let spyCacheDir: jest.SpyInstance; let spyCacheDir: any;
let spyReadDirSync: jest.SpyInstance; let spyReadDirSync: any;
let spyRenameWinArchive: jest.SpyInstance; let spyRenameWinArchive: any;
beforeEach(() => { beforeEach(() => {
spyDownloadTool = jest.spyOn(tc, 'downloadTool'); spyDownloadTool = tc.downloadTool as jest.Mock;
spyDownloadTool.mockResolvedValue('/tmp/jdk.tar.gz'); spyDownloadTool.mockResolvedValue('/tmp/jdk.tar.gz');
spyVerifySignature = jest.spyOn(gpg, 'verifyPackageSignature'); spyVerifySignature = gpg.verifyPackageSignature as jest.Mock;
spyVerifySignature.mockResolvedValue(undefined); spyVerifySignature.mockResolvedValue(undefined);
spyExtractJdkFile = jest.spyOn(util, 'extractJdkFile'); spyExtractJdkFile = util.extractJdkFile as jest.Mock;
spyExtractJdkFile.mockResolvedValue('/tmp/extracted'); spyExtractJdkFile.mockResolvedValue('/tmp/extracted');
spyCacheDir = jest.spyOn(tc, 'cacheDir'); spyCacheDir = tc.cacheDir as jest.Mock;
spyCacheDir.mockResolvedValue('/tmp/toolcache'); spyCacheDir.mockResolvedValue('/tmp/toolcache');
spyReadDirSync = jest.spyOn(fs, 'readdirSync'); spyReadDirSync = jest.spyOn(fs, 'readdirSync');
spyReadDirSync.mockReturnValue(['jdk-17'] as any); spyReadDirSync.mockReturnValue(['jdk-17'] as any);
spyRenameWinArchive = jest.spyOn(util, 'renameWinArchive'); spyRenameWinArchive = util.renameWinArchive as jest.Mock;
spyRenameWinArchive.mockReturnValue('/tmp/jdk.tar.gz.zip'); spyRenameWinArchive.mockReturnValue('/tmp/jdk.tar.gz.zip');
}); });

View File

@@ -1,16 +1,60 @@
import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import type {IZuluVersions} from '../../src/distributions/zulu/models.js';
import {HttpClient} from '@actions/http-client'; import {HttpClient} from '@actions/http-client';
import {ZuluDistribution} from '../../src/distributions/zulu/installer';
import {IZuluVersions} from '../../src/distributions/zulu/models';
import * as utils from '../../src/util';
import os from 'os'; import os from 'os';
import * as core from '@actions/core';
import manifestData from '../data/zulu-releases-default.json'; import manifestData from '../data/zulu-releases-default.json' with {type: 'json'};
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
const real_util_module = await import('../../src/util.js');
jest.unstable_mockModule('../../src/util.js', () => ({
...real_util_module,
getDownloadArchiveExtension: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const {ZuluDistribution} =
await import('../../src/distributions/zulu/installer.js');
const utils = await import('../../src/util.js');
describe('getAvailableVersions', () => { describe('getAvailableVersions', () => {
let spyHttpClient: jest.SpyInstance; let spyHttpClient: any;
let spyUtilGetDownloadArchiveExtension: jest.SpyInstance; let spyCoreError: any;
let spyCoreError: jest.SpyInstance;
beforeEach(() => { beforeEach(() => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
@@ -20,14 +64,12 @@ describe('getAvailableVersions', () => {
result: [] as IZuluVersions[] result: [] as IZuluVersions[]
}); });
spyUtilGetDownloadArchiveExtension = jest.spyOn( (utils.getDownloadArchiveExtension as jest.Mock<any>).mockReturnValue(
utils, 'tar.gz'
'getDownloadArchiveExtension'
); );
spyUtilGetDownloadArchiveExtension.mockReturnValue('tar.gz');
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
}); });

View File

@@ -1,17 +1,62 @@
import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import type {IZuluVersions} from '../../src/distributions/zulu/models.js';
import {HttpClient} from '@actions/http-client'; import {HttpClient} from '@actions/http-client';
import * as semver from 'semver'; import * as semver from 'semver';
import {ZuluDistribution} from '../../src/distributions/zulu/installer';
import {IZuluVersions} from '../../src/distributions/zulu/models';
import * as utils from '../../src/util';
import os from 'os'; import os from 'os';
import * as core from '@actions/core';
import manifestData from '../data/zulu-linux.json'; import manifestData from '../data/zulu-linux.json' with {type: 'json'};
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
const real_util_module = await import('../../src/util.js');
jest.unstable_mockModule('../../src/util.js', () => ({
...real_util_module,
getDownloadArchiveExtension: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const {ZuluDistribution} =
await import('../../src/distributions/zulu/installer.js');
const utils = await import('../../src/util.js');
describe('getAvailableVersions', () => { describe('getAvailableVersions', () => {
let spyHttpClient: jest.SpyInstance; let spyHttpClient: any;
let spyUtilGetDownloadArchiveExtension: jest.SpyInstance; let spyUtilGetDownloadArchiveExtension: any;
let spyCoreError: jest.SpyInstance; let spyCoreError: any;
beforeEach(() => { beforeEach(() => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
@@ -21,14 +66,12 @@ describe('getAvailableVersions', () => {
result: [] as IZuluVersions[] result: [] as IZuluVersions[]
}); });
spyUtilGetDownloadArchiveExtension = jest.spyOn( spyUtilGetDownloadArchiveExtension =
utils, utils.getDownloadArchiveExtension as jest.Mock<any>;
'getDownloadArchiveExtension'
);
spyUtilGetDownloadArchiveExtension.mockReturnValue('zip'); spyUtilGetDownloadArchiveExtension.mockReturnValue('zip');
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
}); });

View File

@@ -1,17 +1,61 @@
import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import type {IZuluVersions} from '../../src/distributions/zulu/models.js';
import {HttpClient} from '@actions/http-client'; import {HttpClient} from '@actions/http-client';
import * as semver from 'semver'; import * as semver from 'semver';
import {ZuluDistribution} from '../../src/distributions/zulu/installer';
import {IZuluVersions} from '../../src/distributions/zulu/models';
import * as utils from '../../src/util';
import os from 'os'; import os from 'os';
import * as core from '@actions/core';
import manifestData from '../data/zulu-windows.json'; import manifestData from '../data/zulu-windows.json' with {type: 'json'};
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
const real_util_module = await import('../../src/util.js');
jest.unstable_mockModule('../../src/util.js', () => ({
...real_util_module,
getDownloadArchiveExtension: jest.fn()
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const {ZuluDistribution} =
await import('../../src/distributions/zulu/installer.js');
const utils = await import('../../src/util.js');
describe('getAvailableVersions', () => { describe('getAvailableVersions', () => {
let spyHttpClient: jest.SpyInstance; let spyHttpClient: any;
let spyUtilGetDownloadArchiveExtension: jest.SpyInstance; let spyCoreError: any;
let spyCoreError: jest.SpyInstance;
beforeEach(() => { beforeEach(() => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
@@ -21,14 +65,12 @@ describe('getAvailableVersions', () => {
result: [] as IZuluVersions[] result: [] as IZuluVersions[]
}); });
spyUtilGetDownloadArchiveExtension = jest.spyOn( (utils.getDownloadArchiveExtension as jest.Mock<any>).mockReturnValue(
utils, 'zip'
'getDownloadArchiveExtension'
); );
spyUtilGetDownloadArchiveExtension.mockReturnValue('zip');
// Mock core.error to suppress error logs // Mock core.error to suppress error logs
spyCoreError = jest.spyOn(core, 'error'); spyCoreError = core.error as jest.Mock;
spyCoreError.mockImplementation(() => {}); spyCoreError.mockImplementation(() => {});
}); });

View File

@@ -1,20 +1,29 @@
import {
jest,
describe,
it,
expect,
beforeEach,
afterAll,
afterEach
} from '@jest/globals';
import {fileURLToPath} from 'url';
import * as path from 'path'; import * as path from 'path';
import * as io from '@actions/io'; import * as io from '@actions/io';
import * as exec from '@actions/exec';
import * as tc from '@actions/tool-cache';
import * as gpg from '../src/gpg';
jest.mock('@actions/exec', () => { const __dirname = path.dirname(fileURLToPath(import.meta.url));
return {
exec: jest.fn()
};
});
jest.mock('@actions/tool-cache', () => { jest.unstable_mockModule('@actions/exec', () => ({
return { exec: jest.fn()
downloadTool: jest.fn() }));
};
}); jest.unstable_mockModule('@actions/tool-cache', () => ({
downloadTool: jest.fn()
}));
const exec = await import('@actions/exec');
const tc = await import('@actions/tool-cache');
const gpg = await import('../src/gpg.js');
const tempDir = path.join(__dirname, 'runner', 'temp'); const tempDir = path.join(__dirname, 'runner', 'temp');
process.env['RUNNER_TEMP'] = tempDir; process.env['RUNNER_TEMP'] = tempDir;
@@ -92,7 +101,9 @@ describe('gpg tests', () => {
it('imports bundled key and verifies package', async () => { it('imports bundled key and verifies package', async () => {
const publicKeyContent = const publicKeyContent =
'-----BEGIN PGP PUBLIC KEY BLOCK-----\ntest\n-----END PGP PUBLIC KEY BLOCK-----'; '-----BEGIN PGP PUBLIC KEY BLOCK-----\ntest\n-----END PGP PUBLIC KEY BLOCK-----';
(tc.downloadTool as jest.Mock).mockResolvedValue('/tmp/jdk.tar.gz.sig'); (tc.downloadTool as jest.Mock<any>).mockResolvedValue(
'/tmp/jdk.tar.gz.sig'
);
await gpg.verifyPackageSignature( await gpg.verifyPackageSignature(
'/tmp/jdk.tar.gz', '/tmp/jdk.tar.gz',
'https://example.com/jdk.tar.gz.sig', 'https://example.com/jdk.tar.gz.sig',

View File

@@ -1,42 +1,68 @@
import * as core from '@actions/core'; import {afterEach, beforeEach, describe, expect, it, jest} from '@jest/globals';
import {configureMavenArgs} from '../src/maven-args'; const mockGetInput = jest.fn<(...args: any[]) => any>();
import { const mockExportVariable = jest.fn<(...args: any[]) => any>();
const mockInfo = jest.fn<(...args: any[]) => any>();
const mockDebug = jest.fn<(...args: any[]) => any>();
const mockWarning = jest.fn<(...args: any[]) => any>();
jest.unstable_mockModule('@actions/core', () => ({
getInput: mockGetInput,
exportVariable: mockExportVariable,
info: mockInfo,
debug: mockDebug,
warning: mockWarning,
setSecret: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
addPath: jest.fn(),
getMultilineInput: jest.fn(),
getBooleanInput: jest.fn(),
getState: jest.fn(),
saveState: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn(),
isDebug: jest.fn(),
setCommandEcho: jest.fn(),
getIDToken: jest.fn(),
ExitCode: {Success: 0, Failure: 1},
summary: {},
markdownSummary: {},
platform: {},
toPosixPath: jest.fn(),
toWin32Path: jest.fn(),
toPlatformPath: jest.fn()
}));
const {configureMavenArgs} = await import('../src/maven-args.js');
const {
INPUT_SHOW_DOWNLOAD_PROGRESS, INPUT_SHOW_DOWNLOAD_PROGRESS,
MAVEN_ARGS_ENV, MAVEN_ARGS_ENV,
MAVEN_NO_TRANSFER_PROGRESS_FLAG MAVEN_NO_TRANSFER_PROGRESS_FLAG
} from '../src/constants'; } = await import('../src/constants.js');
describe('configureMavenArgs', () => { describe('configureMavenArgs', () => {
let inputs: Record<string, string>; let inputs: Record<string, string>;
let spyGetInput: jest.SpyInstance;
let spyExportVariable: jest.SpyInstance;
let spyInfo: jest.SpyInstance;
let spyDebug: jest.SpyInstance;
const originalMavenArgs = process.env[MAVEN_ARGS_ENV]; const originalMavenArgs = process.env[MAVEN_ARGS_ENV];
beforeEach(() => { beforeEach(() => {
inputs = {}; inputs = {};
spyGetInput = jest.spyOn(core, 'getInput'); mockGetInput.mockImplementation((name: string) => inputs[name] ?? '');
spyGetInput.mockImplementation((name: string) => inputs[name] ?? ''); mockExportVariable.mockImplementation((name: string, value: string) => {
spyExportVariable = jest.spyOn(core, 'exportVariable');
spyExportVariable.mockImplementation((name: string, value: string) => {
process.env[name] = value; process.env[name] = value;
}); });
mockInfo.mockImplementation(() => undefined);
spyInfo = jest.spyOn(core, 'info'); mockDebug.mockImplementation(() => undefined);
spyInfo.mockImplementation(() => undefined);
spyDebug = jest.spyOn(core, 'debug');
spyDebug.mockImplementation(() => undefined);
delete process.env[MAVEN_ARGS_ENV]; delete process.env[MAVEN_ARGS_ENV];
}); });
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); jest.resetAllMocks();
if (originalMavenArgs === undefined) { if (originalMavenArgs === undefined) {
delete process.env[MAVEN_ARGS_ENV]; delete process.env[MAVEN_ARGS_ENV];
} else { } else {
@@ -47,7 +73,7 @@ describe('configureMavenArgs', () => {
it('sets MAVEN_ARGS with -ntp by default', () => { it('sets MAVEN_ARGS with -ntp by default', () => {
configureMavenArgs(); configureMavenArgs();
expect(spyExportVariable).toHaveBeenCalledWith( expect(mockExportVariable).toHaveBeenCalledWith(
MAVEN_ARGS_ENV, MAVEN_ARGS_ENV,
MAVEN_NO_TRANSFER_PROGRESS_FLAG MAVEN_NO_TRANSFER_PROGRESS_FLAG
); );
@@ -59,7 +85,7 @@ describe('configureMavenArgs', () => {
configureMavenArgs(); configureMavenArgs();
expect(spyExportVariable).not.toHaveBeenCalled(); expect(mockExportVariable).not.toHaveBeenCalled();
expect(process.env[MAVEN_ARGS_ENV]).toBeUndefined(); expect(process.env[MAVEN_ARGS_ENV]).toBeUndefined();
}); });
@@ -68,7 +94,7 @@ describe('configureMavenArgs', () => {
configureMavenArgs(); configureMavenArgs();
expect(spyExportVariable).toHaveBeenCalledWith( expect(mockExportVariable).toHaveBeenCalledWith(
MAVEN_ARGS_ENV, MAVEN_ARGS_ENV,
`-B -Dstyle.color=always ${MAVEN_NO_TRANSFER_PROGRESS_FLAG}` `-B -Dstyle.color=always ${MAVEN_NO_TRANSFER_PROGRESS_FLAG}`
); );
@@ -79,7 +105,7 @@ describe('configureMavenArgs', () => {
configureMavenArgs(); configureMavenArgs();
expect(spyExportVariable).not.toHaveBeenCalled(); expect(mockExportVariable).not.toHaveBeenCalled();
expect(process.env[MAVEN_ARGS_ENV]).toBe('-B -ntp'); expect(process.env[MAVEN_ARGS_ENV]).toBe('-B -ntp');
}); });
@@ -88,7 +114,7 @@ describe('configureMavenArgs', () => {
configureMavenArgs(); configureMavenArgs();
expect(spyExportVariable).not.toHaveBeenCalled(); expect(mockExportVariable).not.toHaveBeenCalled();
expect(process.env[MAVEN_ARGS_ENV]).toBe('--no-transfer-progress -B'); expect(process.env[MAVEN_ARGS_ENV]).toBe('--no-transfer-progress -B');
}); });
@@ -98,7 +124,7 @@ describe('configureMavenArgs', () => {
configureMavenArgs(); configureMavenArgs();
expect(spyExportVariable).not.toHaveBeenCalled(); expect(mockExportVariable).not.toHaveBeenCalled();
expect(process.env[MAVEN_ARGS_ENV]).toBe('-B'); expect(process.env[MAVEN_ARGS_ENV]).toBe('-B');
}); });
}); });

View File

@@ -1,23 +1,63 @@
import {
jest,
describe,
it,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll
} from '@jest/globals';
import {fileURLToPath} from 'url';
import * as fs from 'fs'; import * as fs from 'fs';
import os from 'os'; import os from 'os';
import * as path from 'path'; import * as path from 'path';
import * as core from '@actions/core';
import * as io from '@actions/io'; import * as io from '@actions/io';
import * as toolchains from '../src/toolchains';
import {M2_DIR, MVN_TOOLCHAINS_FILE} from '../src/constants';
// Mock @actions/core before importing source modules that depend on it
jest.unstable_mockModule('@actions/core', () => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn(),
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
startGroup: jest.fn(),
endGroup: jest.fn(),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
// Dynamic imports after mocking
const core = await import('@actions/core');
const toolchains = await import('../src/toolchains.js');
const {M2_DIR, MVN_TOOLCHAINS_FILE} = await import('../src/constants.js');
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const m2Dir = path.join(__dirname, M2_DIR); const m2Dir = path.join(__dirname, M2_DIR);
const toolchainsFile = path.join(m2Dir, MVN_TOOLCHAINS_FILE); const toolchainsFile = path.join(m2Dir, MVN_TOOLCHAINS_FILE);
describe('toolchains tests', () => { describe('toolchains tests', () => {
let spyOSHomedir: jest.SpyInstance; let spyOSHomedir: any;
let spyInfo: jest.SpyInstance; let spyInfo: any;
beforeEach(async () => { beforeEach(async () => {
await io.rmRF(m2Dir); await io.rmRF(m2Dir);
spyOSHomedir = jest.spyOn(os, 'homedir'); spyOSHomedir = jest.spyOn(os, 'homedir');
spyOSHomedir.mockReturnValue(__dirname); spyOSHomedir.mockReturnValue(__dirname);
spyInfo = jest.spyOn(core, 'info'); spyInfo = core.info as jest.Mock;
spyInfo.mockImplementation(() => null); spyInfo.mockImplementation(() => null);
}, 300000); }, 300000);
@@ -895,6 +935,11 @@ describe('toolchains tests', () => {
const jdkHome = const jdkHome =
'/opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.1-12/x64'; '/opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.1-12/x64';
(core.getInput as jest.Mock<any>).mockImplementation((name: string) => {
if (name === 'settings-path') return m2Dir;
return '';
});
await toolchains.configureToolchains( await toolchains.configureToolchains(
version, version,
distributionName, distributionName,

View File

@@ -1,19 +1,64 @@
import * as cache from '@actions/cache'; import {
import * as core from '@actions/core'; jest,
describe,
it,
expect,
beforeEach,
afterAll,
afterEach
} from '@jest/globals';
import {fileURLToPath} from 'url';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Mock @actions/cache
jest.unstable_mockModule('@actions/cache', () => ({
isFeatureAvailable: jest.fn(),
saveCache: jest.fn(),
restoreCache: jest.fn()
}));
// Mock @actions/core
jest.unstable_mockModule('@actions/core', () => ({
getInput: jest.fn(),
getBooleanInput: jest.fn(),
getMultilineInput: jest.fn(),
setOutput: jest.fn(),
setFailed: jest.fn(),
warning: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
notice: jest.fn(),
startGroup: jest.fn(),
endGroup: jest.fn(),
addPath: jest.fn(),
exportVariable: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(),
setSecret: jest.fn(),
isDebug: jest.fn(() => false),
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
toPlatformPath: jest.fn((p: string) => p),
toWin32Path: jest.fn((p: string) => p),
toPosixPath: jest.fn((p: string) => p)
}));
const cache = await import('@actions/cache');
const core = await import('@actions/core');
const {
convertVersionToSemver, convertVersionToSemver,
getNextPageUrlFromLinkHeader, getNextPageUrlFromLinkHeader,
getVersionFromFileContent, getVersionFromFileContent,
isVersionSatisfies, isVersionSatisfies,
isCacheFeatureAvailable, isCacheFeatureAvailable,
isGhes, isGhes,
validatePaginationUrl validatePaginationUrl,
} from '../src/util'; getLatestMajorVersion
} = await import('../src/util.js');
jest.mock('@actions/cache');
jest.mock('@actions/core');
describe('isVersionSatisfies', () => { describe('isVersionSatisfies', () => {
it.each([ it.each([
@@ -45,8 +90,10 @@ describe('isVersionSatisfies', () => {
describe('isCacheFeatureAvailable', () => { describe('isCacheFeatureAvailable', () => {
it('isCacheFeatureAvailable disabled on GHES', () => { it('isCacheFeatureAvailable disabled on GHES', () => {
jest.spyOn(cache, 'isFeatureAvailable').mockImplementation(() => false); (cache.isFeatureAvailable as jest.Mock<any>).mockImplementation(
const infoMock = jest.spyOn(core, 'warning'); () => false
);
const infoMock = core.warning as jest.Mock;
const message = const message =
'Caching is only supported on GHES version >= 3.5. If you are on a version >= 3.5, please check with your GHES admin if the Actions cache service is enabled or not.'; 'Caching is only supported on GHES version >= 3.5. If you are on a version >= 3.5, please check with your GHES admin if the Actions cache service is enabled or not.';
try { try {
@@ -59,8 +106,10 @@ describe('isCacheFeatureAvailable', () => {
}); });
it('isCacheFeatureAvailable disabled on dotcom', () => { it('isCacheFeatureAvailable disabled on dotcom', () => {
jest.spyOn(cache, 'isFeatureAvailable').mockImplementation(() => false); (cache.isFeatureAvailable as jest.Mock<any>).mockImplementation(
const infoMock = jest.spyOn(core, 'warning'); () => false
);
const infoMock = core.warning as jest.Mock;
const message = const message =
'The runner was not able to contact the cache service. Caching will be skipped'; 'The runner was not able to contact the cache service. Caching will be skipped';
try { try {
@@ -73,9 +122,14 @@ describe('isCacheFeatureAvailable', () => {
}); });
it('isCacheFeatureAvailable is enabled', () => { it('isCacheFeatureAvailable is enabled', () => {
jest.spyOn(cache, 'isFeatureAvailable').mockImplementation(() => true); (cache.isFeatureAvailable as jest.Mock<any>).mockImplementation(() => true);
expect(isCacheFeatureAvailable()).toBe(true); expect(isCacheFeatureAvailable()).toBe(true);
}); });
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
}); });
describe('convertVersionToSemver', () => { describe('convertVersionToSemver', () => {
@@ -315,7 +369,6 @@ describe('isGhes', () => {
const pristineEnv = process.env; const pristineEnv = process.env;
beforeEach(() => { beforeEach(() => {
jest.resetModules();
process.env = {...pristineEnv}; process.env = {...pristineEnv};
}); });
@@ -348,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'

118593
dist/cleanup/index.js vendored

File diff suppressed because one or more lines are too long

3
dist/cleanup/package.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

136236
dist/setup/index.js vendored

File diff suppressed because one or more lines are too long

3
dist/setup/package.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

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

74
eslint.config.mjs Normal file
View File

@@ -0,0 +1,74 @@
// This is a reusable configuration file copied from https://github.com/actions/reusable-workflows/tree/main/reusable-configurations. Please don't make changes to this file as it's the subject of an automatic update.
import js from '@eslint/js';
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import jest from 'eslint-plugin-jest';
import n from 'eslint-plugin-n';
import prettier from 'eslint-config-prettier';
import globals from 'globals';
export default [
{
ignores: ['**/*', '!src/**', '!__tests__/**']
},
js.configs.recommended,
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
ecmaVersion: 2022,
sourceType: 'module',
globals: {
...globals.node,
...globals.es2015
}
},
plugins: {
'@typescript-eslint': tsPlugin,
n
},
rules: {
...tsPlugin.configs.recommended.rules,
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-ignore': 'allow-with-description'
}
],
'no-console': 'error',
yoda: 'error',
'prefer-const': [
'error',
{
destructuring: 'all'
}
],
'no-control-regex': 'off',
'no-constant-condition': ['error', {checkLoops: false}],
'no-undef': 'off',
'no-useless-assignment': 'off',
'n/no-extraneous-import': 'error'
}
},
{
files: ['**/*{test,spec}.ts'],
plugins: {jest},
languageOptions: {
globals: {
...globals.jest
}
},
rules: {
...jest.configs['flat/recommended'].rules,
'@typescript-eslint/no-unused-vars': 'off',
'jest/no-standalone-expect': 'off',
'jest/no-conditional-expect': 'off',
'no-console': 'off'
}
},
prettier
];

View File

@@ -1,11 +0,0 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}

24
jest.config.ts Normal file
View File

@@ -0,0 +1,24 @@
export default {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
roots: ['<rootDir>'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': [
'ts-jest',
{
useESM: true,
diagnostics: {
ignoreCodes: [151002]
}
}
]
},
extensionsToTreatAsEsm: ['.ts'],
transformIgnorePatterns: ['node_modules/(?!(@actions)/)'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1'
},
verbose: true
}

3099
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
{ {
"name": "setup-java", "name": "setup-java",
"version": "5.6.0", "version": "6.0.0",
"type": "module",
"private": true, "private": true,
"description": "setup java action", "description": "setup java action",
"main": "dist/setup/index.js", "main": "dist/setup/index.js",
@@ -9,24 +10,24 @@
}, },
"scripts": { "scripts": {
"build": "ncc build -o dist/setup src/setup-java.ts && ncc build -o dist/cleanup src/cleanup-java.ts", "build": "ncc build -o dist/setup src/setup-java.ts && ncc build -o dist/cleanup src/cleanup-java.ts",
"format": "prettier --no-error-on-unmatched-pattern --config ./.prettierrc.js --write \"**/*.{ts,yml,yaml}\"", "format": "prettier --no-error-on-unmatched-pattern --write \"**/*.{ts,yml,yaml}\"",
"format-check": "prettier --no-error-on-unmatched-pattern --config ./.prettierrc.js --check \"**/*.{ts,yml,yaml}\"", "format-check": "prettier --no-error-on-unmatched-pattern --check \"**/*.{ts,yml,yaml}\"",
"lint": "eslint --config ./.eslintrc.js \"**/*.ts\"", "lint": "eslint \"**/*.ts\"",
"lint:fix": "eslint --config ./.eslintrc.js \"**/*.ts\" --fix", "lint:fix": "eslint \"**/*.ts\" --fix",
"check": "npm run format-check && npm run lint && npm run build && npm test", "check": "npm run format-check && npm run lint && npm run build && npm test",
"fix": "npm run format && npm run lint:fix && npm run build", "fix": "npm run format && npm run lint:fix && npm run build",
"prepare": "husky install", "prepare": "husky install",
"prerelease": "npm run-script build", "prerelease": "npm run-script build",
"release": "git add -f dist/setup/index.js dist/cleanup/index.js", "release": "git add -f dist/setup/index.js dist/cleanup/index.js",
"test": "jest" "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --runInBand --coverage"
}, },
"lint-staged": { "lint-staged": {
"*.ts": [ "*.ts": [
"prettier --no-error-on-unmatched-pattern --config ./.prettierrc.js --write", "prettier --no-error-on-unmatched-pattern --write",
"eslint --config ./.eslintrc.js --fix" "eslint --fix"
], ],
"*.{yml,yaml}": [ "*.{yml,yaml}": [
"prettier --no-error-on-unmatched-pattern --config ./.prettierrc.js --write" "prettier --no-error-on-unmatched-pattern --write"
] ]
}, },
"repository": { "repository": {
@@ -41,34 +42,35 @@
"author": "GitHub", "author": "GitHub",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/cache": "^5.1.0", "@actions/cache": "^6.1.0",
"@actions/core": "^2.0.3", "@actions/core": "^3.0.1",
"@actions/exec": "^2.0.0", "@actions/exec": "^3.0.0",
"@actions/glob": "^0.5.1", "@actions/glob": "^0.7.0",
"@actions/http-client": "^3.0.2", "@actions/http-client": "^4.0.1",
"@actions/io": "^2.0.0", "@actions/io": "^3.0.2",
"@actions/tool-cache": "^3.0.1", "@actions/tool-cache": "^4.0.0",
"semver": "^7.6.0", "semver": "^7.8.5",
"xmlbuilder2": "^4.0.3" "xmlbuilder2": "^4.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^30.0.0", "@eslint/js": "^10.0.1",
"@jest/globals": "^30.4.1",
"@types/node": "^26.0.0", "@types/node": "^26.0.0",
"@types/semver": "^7.5.8", "@types/semver": "^7.7.0",
"@typescript-eslint/eslint-plugin": "^8.48.0", "@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/parser": "^8.62.0", "@typescript-eslint/parser": "^8.62.0",
"@vercel/ncc": "^0.44.0", "@vercel/ncc": "^0.44.0",
"eslint": "^8.57.0", "eslint": "^10.5.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-jest": "^29.15.4", "eslint-plugin-jest": "^29.15.4",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-n": "^18.1.0",
"globals": "^17.7.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"jest": "^30.4.2", "jest": "^30.4.2",
"jest-circus": "^30.4.2",
"lint-staged": "^17.0.8", "lint-staged": "^17.0.8",
"prettier": "^3.9.1", "prettier": "^3.9.1",
"ts-jest": "^29.4.11", "ts-jest": "^29.4.11",
"typescript": "^5.3.3" "typescript": "^6.0.3"
}, },
"bugs": { "bugs": {
"url": "https://github.com/actions/setup-java/issues" "url": "https://github.com/actions/setup-java/issues"

View File

@@ -6,9 +6,9 @@ import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import {create as xmlCreate} from 'xmlbuilder2'; import {create as xmlCreate} from 'xmlbuilder2';
import * as constants from './constants'; import * as constants from './constants.js';
import * as gpg from './gpg'; import * as gpg from './gpg.js';
import {getBooleanInput} from './util'; import {getBooleanInput} from './util.js';
export async function configureAuthentication() { export async function configureAuthentication() {
const id = core.getInput(constants.INPUT_SERVER_ID); const id = core.getInput(constants.INPUT_SERVER_ID);

View File

@@ -1,8 +1,9 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as gpg from './gpg'; import * as gpg from './gpg.js';
import * as constants from './constants'; import * as constants from './constants.js';
import {isJobStatusSuccess} from './util'; import {isJobStatusSuccess} from './util.js';
import {save} from './cache'; import {save} from './cache.js';
import {fileURLToPath} from 'url';
async function removePrivateKeyFromKeychain() { async function removePrivateKeyFromKeychain() {
if (core.getInput(constants.INPUT_GPG_PRIVATE_KEY, {required: false})) { if (core.getInput(constants.INPUT_GPG_PRIVATE_KEY, {required: false})) {
@@ -52,7 +53,7 @@ export async function run() {
await ignoreError(saveCache()); await ignoreError(saveCache());
} }
if (require.main === module) { if (process.argv[1] === fileURLToPath(import.meta.url)) {
run(); run();
} else { } else {
// https://nodejs.org/api/modules.html#modules_accessing_the_main_module // https://nodejs.org/api/modules.html#modules_accessing_the_main_module

View File

@@ -5,13 +5,13 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import semver from 'semver'; import semver from 'semver';
import {JavaBase} from '../base-installer'; import {JavaBase} from '../base-installer.js';
import {IAdoptAvailableVersions} from './models'; import {IAdoptAvailableVersions} from './models.js';
import { import {
JavaDownloadRelease, JavaDownloadRelease,
JavaInstallerOptions, JavaInstallerOptions,
JavaInstallerResults JavaInstallerResults
} from '../base-models'; } from '../base-models.js';
import { import {
extractJdkFile, extractJdkFile,
getNextPageUrlFromLinkHeader, getNextPageUrlFromLinkHeader,
@@ -20,8 +20,11 @@ import {
renameWinArchive, renameWinArchive,
MAX_PAGINATION_PAGES, MAX_PAGINATION_PAGES,
validatePaginationUrl validatePaginationUrl
} from '../../util'; } from '../../util.js';
import {TemurinDistribution, TemurinImplementation} from '../temurin/installer'; import {
TemurinDistribution,
TemurinImplementation
} from '../temurin/installer.js';
export enum AdoptImplementation { export enum AdoptImplementation {
Hotspot = 'Hotspot', Hotspot = 'Hotspot',

View File

@@ -4,13 +4,17 @@ 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'; import {
convertVersionToSemver,
getToolcachePath,
isVersionSatisfies
} from '../util.js';
import { import {
JavaDownloadRelease, JavaDownloadRelease,
JavaInstallerOptions, JavaInstallerOptions,
JavaInstallerResults JavaInstallerResults
} from './base-models'; } from './base-models.js';
import {MACOS_JAVA_CONTENT_POSTFIX} from '../constants'; import {MACOS_JAVA_CONTENT_POSTFIX} from '../constants.js';
import os from 'os'; import os from 'os';
export abstract class JavaBase { export abstract class JavaBase {
@@ -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

@@ -7,17 +7,17 @@ import {
getDownloadArchiveExtension, getDownloadArchiveExtension,
convertVersionToSemver, convertVersionToSemver,
renameWinArchive renameWinArchive
} from '../../util'; } from '../../util.js';
import {JavaBase} from '../base-installer'; import {JavaBase} from '../base-installer.js';
import { import {
JavaDownloadRelease, JavaDownloadRelease,
JavaInstallerOptions, JavaInstallerOptions,
JavaInstallerResults JavaInstallerResults
} from '../base-models'; } from '../base-models.js';
import { import {
ICorrettoAllAvailableVersions, ICorrettoAllAvailableVersions,
ICorrettoAvailableVersions ICorrettoAvailableVersions
} from './models'; } from './models.js';
export class CorrettoDistribution extends JavaBase { export class CorrettoDistribution extends JavaBase {
constructor(installerOptions: JavaInstallerOptions) { constructor(installerOptions: JavaInstallerOptions) {
@@ -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

@@ -1,22 +1,25 @@
import {JavaBase} from './base-installer'; import {JavaBase} from './base-installer.js';
import {JavaInstallerOptions} from './base-models'; import {JavaInstallerOptions} from './base-models.js';
import {LocalDistribution} from './local/installer'; import {LocalDistribution} from './local/installer.js';
import {ZuluDistribution} from './zulu/installer'; import {ZuluDistribution} from './zulu/installer.js';
import {AdoptDistribution, AdoptImplementation} from './adopt/installer'; import {AdoptDistribution, AdoptImplementation} from './adopt/installer.js';
import {TemurinDistribution, TemurinImplementation} from './temurin/installer'; import {
import {LibericaDistributions} from './liberica/installer'; TemurinDistribution,
import {MicrosoftDistributions} from './microsoft/installer'; TemurinImplementation
import {SemeruDistribution} from './semeru/installer'; } from './temurin/installer.js';
import {CorrettoDistribution} from './corretto/installer'; import {LibericaDistributions} from './liberica/installer.js';
import {OracleDistribution} from './oracle/installer'; import {MicrosoftDistributions} from './microsoft/installer.js';
import {DragonwellDistribution} from './dragonwell/installer'; import {SemeruDistribution} from './semeru/installer.js';
import {SapMachineDistribution} from './sapmachine/installer'; import {CorrettoDistribution} from './corretto/installer.js';
import {OracleDistribution} from './oracle/installer.js';
import {DragonwellDistribution} from './dragonwell/installer.js';
import {SapMachineDistribution} from './sapmachine/installer.js';
import { import {
GraalVMCommunityDistribution, GraalVMCommunityDistribution,
GraalVMDistribution GraalVMDistribution
} from './graalvm/installer'; } from './graalvm/installer.js';
import {JetBrainsDistribution} from './jetbrains/installer'; import {JetBrainsDistribution} from './jetbrains/installer.js';
import {KonaDistribution} from './kona/installer'; import {KonaDistribution} from './kona/installer.js';
enum JavaDistribution { enum JavaDistribution {
Adopt = 'adopt', Adopt = 'adopt',

View File

@@ -5,7 +5,7 @@ import semver from 'semver';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {JavaBase} from '../base-installer'; import {JavaBase} from '../base-installer.js';
import { import {
convertVersionToSemver, convertVersionToSemver,
extractJdkFile, extractJdkFile,
@@ -13,13 +13,13 @@ import {
getGitHubHttpHeaders, getGitHubHttpHeaders,
isVersionSatisfies, isVersionSatisfies,
renameWinArchive renameWinArchive
} from '../../util'; } from '../../util.js';
import {IDragonwellVersions, IDragonwellAllVersions} from './models'; import {IDragonwellVersions, IDragonwellAllVersions} from './models.js';
import { import {
JavaDownloadRelease, JavaDownloadRelease,
JavaInstallerOptions, JavaInstallerOptions,
JavaInstallerResults JavaInstallerResults
} from '../base-models'; } from '../base-models.js';
export class DragonwellDistribution extends JavaBase { export class DragonwellDistribution extends JavaBase {
constructor(installerOptions: JavaInstallerOptions) { constructor(installerOptions: JavaInstallerOptions) {

View File

@@ -3,25 +3,26 @@ import * as tc from '@actions/tool-cache';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import semver from 'semver'; import semver from 'semver';
import {JavaBase} from '../base-installer'; import {JavaBase} from '../base-installer.js';
import {HttpCodes} from '@actions/http-client'; import {HttpCodes} from '@actions/http-client';
import {GraalVMEAVersion} from './models'; import {GraalVMEAVersion} from './models.js';
import { import {
JavaDownloadRelease, JavaDownloadRelease,
JavaInstallerOptions, JavaInstallerOptions,
JavaInstallerResults JavaInstallerResults
} from '../base-models'; } from '../base-models.js';
import { import {
convertVersionToSemver, convertVersionToSemver,
extractJdkFile, extractJdkFile,
getDownloadArchiveExtension, getDownloadArchiveExtension,
getGitHubHttpHeaders, getGitHubHttpHeaders,
getLatestMajorVersion,
getNextPageUrlFromLinkHeader, getNextPageUrlFromLinkHeader,
isVersionSatisfies, isVersionSatisfies,
MAX_PAGINATION_PAGES, MAX_PAGINATION_PAGES,
renameWinArchive, renameWinArchive,
validatePaginationUrl validatePaginationUrl
} from '../../util'; } from '../../util.js';
const GRAALVM_DL_BASE = 'https://download.oracle.com/graalvm'; const GRAALVM_DL_BASE = 'https://download.oracle.com/graalvm';
const GRAALVM_DOWNLOAD_URL = 'https://www.graalvm.org/downloads/'; const GRAALVM_DOWNLOAD_URL = 'https://www.graalvm.org/downloads/';
@@ -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;
} }
@@ -300,17 +311,20 @@ export class GraalVMDistribution extends JavaBase {
// Check if it's a 404 error (file not found) // Check if it's a 404 error (file not found)
if (error.message?.includes('404')) { if (error.message?.includes('404')) {
throw new Error( throw new Error(
`GraalVM EA version '${javaEaVersion}' not found. Please verify the version exists in the EA builds repository.` `GraalVM EA version '${javaEaVersion}' not found. Please verify the version exists in the EA builds repository.`,
{cause: error}
); );
} }
// Re-throw with more context // Re-throw with more context
throw new Error( throw new Error(
`Failed to fetch GraalVM EA version information for '${javaEaVersion}': ${error.message}` `Failed to fetch GraalVM EA version information for '${javaEaVersion}': ${error.message}`,
{cause: error}
); );
} }
// If it's not an Error instance, throw a generic error // If it's not an Error instance, throw a generic error
throw new Error( throw new Error(
`Failed to fetch GraalVM EA version information for '${javaEaVersion}'` `Failed to fetch GraalVM EA version information for '${javaEaVersion}'`,
{cause: error}
); );
} }
} }
@@ -351,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

@@ -5,14 +5,14 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import semver from 'semver'; import semver from 'semver';
import {JavaBase} from '../base-installer'; import {JavaBase} from '../base-installer.js';
import {IJetBrainsRawVersion, IJetBrainsVersion} from './models'; import {IJetBrainsRawVersion, IJetBrainsVersion} from './models.js';
import { import {
JavaDownloadRelease, JavaDownloadRelease,
JavaInstallerOptions, JavaInstallerOptions,
JavaInstallerResults JavaInstallerResults
} from '../base-models'; } from '../base-models.js';
import {extractJdkFile, isVersionSatisfies} from '../../util'; import {extractJdkFile, isVersionSatisfies} from '../../util.js';
import {OutgoingHttpHeaders} from 'http'; import {OutgoingHttpHeaders} from 'http';
import {HttpCodes} from '@actions/http-client'; import {HttpCodes} from '@actions/http-client';

View File

@@ -5,19 +5,19 @@ import semver from 'semver';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {JavaBase} from '../base-installer'; import {JavaBase} from '../base-installer.js';
import {IKonaReleaseInfo, IKonaRelease} from './models'; import {IKonaReleaseInfo, IKonaRelease} from './models.js';
import { import {
JavaDownloadRelease, JavaDownloadRelease,
JavaInstallerOptions, JavaInstallerOptions,
JavaInstallerResults JavaInstallerResults
} from '../base-models'; } from '../base-models.js';
import { import {
extractJdkFile, extractJdkFile,
getDownloadArchiveExtension, getDownloadArchiveExtension,
isVersionSatisfies, isVersionSatisfies,
renameWinArchive renameWinArchive
} from '../../util'; } from '../../util.js';
export class KonaDistribution extends JavaBase { export class KonaDistribution extends JavaBase {
constructor(installerOptions: JavaInstallerOptions) { constructor(installerOptions: JavaInstallerOptions) {

View File

@@ -1,18 +1,18 @@
import {JavaBase} from '../base-installer'; import {JavaBase} from '../base-installer.js';
import { import {
JavaDownloadRelease, JavaDownloadRelease,
JavaInstallerOptions, JavaInstallerOptions,
JavaInstallerResults JavaInstallerResults
} from '../base-models'; } from '../base-models.js';
import semver from 'semver'; import semver from 'semver';
import { import {
extractJdkFile, extractJdkFile,
getDownloadArchiveExtension, getDownloadArchiveExtension,
isVersionSatisfies, isVersionSatisfies,
renameWinArchive renameWinArchive
} from '../../util'; } from '../../util.js';
import * as core from '@actions/core'; import * as core from '@actions/core';
import {ArchitectureOptions, LibericaVersion, OsVersions} from './models'; import {ArchitectureOptions, LibericaVersion, OsVersions} from './models.js';
import * as tc from '@actions/tool-cache'; import * as tc from '@actions/tool-cache';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';

View File

@@ -4,14 +4,14 @@ import * as core from '@actions/core';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {JavaBase} from '../base-installer'; import {JavaBase} from '../base-installer.js';
import { import {
JavaInstallerOptions, JavaInstallerOptions,
JavaDownloadRelease, JavaDownloadRelease,
JavaInstallerResults JavaInstallerResults
} from '../base-models'; } from '../base-models.js';
import {extractJdkFile} from '../../util'; import {extractJdkFile} from '../../util.js';
import {MACOS_JAVA_CONTENT_POSTFIX} from '../../constants'; import {MACOS_JAVA_CONTENT_POSTFIX} from '../../constants.js';
export class LocalDistribution extends JavaBase { export class LocalDistribution extends JavaBase {
constructor( constructor(
@@ -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

@@ -1,24 +1,24 @@
import {JavaBase} from '../base-installer'; import {JavaBase} from '../base-installer.js';
import { import {
JavaDownloadRelease, JavaDownloadRelease,
JavaInstallerOptions, JavaInstallerOptions,
JavaInstallerResults JavaInstallerResults
} from '../base-models'; } from '../base-models.js';
import { import {
extractJdkFile, extractJdkFile,
getDownloadArchiveExtension, getDownloadArchiveExtension,
getGitHubHttpHeaders, getGitHubHttpHeaders,
renameWinArchive renameWinArchive
} from '../../util'; } from '../../util.js';
import * as gpg from '../../gpg'; import * as gpg from '../../gpg.js';
import {MICROSOFT_PUBLIC_KEY} from './microsoft-key'; import {MICROSOFT_PUBLIC_KEY} from './microsoft-key.js';
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 fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {TypedResponse} from '@actions/http-client/lib/interfaces'; import {TypedResponse} from '@actions/http-client/lib/interfaces';
export {MICROSOFT_PUBLIC_KEY} from './microsoft-key'; export {MICROSOFT_PUBLIC_KEY} from './microsoft-key.js';
export class MicrosoftDistributions extends JavaBase { export class MicrosoftDistributions extends JavaBase {
constructor(installerOptions: JavaInstallerOptions) { constructor(installerOptions: JavaInstallerOptions) {
@@ -48,7 +48,8 @@ export class MicrosoftDistributions extends JavaBase {
); );
} catch (error) { } catch (error) {
throw new Error( throw new Error(
`Failed to verify signature for Microsoft Build of OpenJDK version ${javaRelease.version}. Signature URL: ${javaRelease.signatureUrl}. Error: ${(error as Error).message}` `Failed to verify signature for Microsoft Build of OpenJDK version ${javaRelease.version}. Signature URL: ${javaRelease.signatureUrl}. Error: ${(error as Error).message}`,
{cause: error}
); );
} }
} }

View File

@@ -4,18 +4,20 @@ import * as tc from '@actions/tool-cache';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {JavaBase} from '../base-installer'; import {JavaBase} from '../base-installer.js';
import { import {
JavaDownloadRelease, JavaDownloadRelease,
JavaInstallerOptions, JavaInstallerOptions,
JavaInstallerResults JavaInstallerResults
} from '../base-models'; } from '../base-models.js';
import { import {
extractJdkFile, extractJdkFile,
getDownloadArchiveExtension, getDownloadArchiveExtension,
getLatestMajorVersion,
renameWinArchive renameWinArchive
} from '../../util'; } from '../../util.js';
import {HttpCodes} from '@actions/http-client'; import {HttpCodes} from '@actions/http-client';
import {OsVersions} from './models.js';
const ORACLE_DL_BASE = 'https://download.oracle.com/java'; const ORACLE_DL_BASE = 'https://download.oracle.com/java';
@@ -72,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];
@@ -112,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

@@ -10,14 +10,14 @@ import {
getDownloadArchiveExtension, getDownloadArchiveExtension,
isVersionSatisfies, isVersionSatisfies,
renameWinArchive renameWinArchive
} from '../../util'; } from '../../util.js';
import {JavaBase} from '../base-installer'; import {JavaBase} from '../base-installer.js';
import { import {
JavaDownloadRelease, JavaDownloadRelease,
JavaInstallerOptions, JavaInstallerOptions,
JavaInstallerResults JavaInstallerResults
} from '../base-models'; } from '../base-models.js';
import {ISapMachineAllVersions, ISapMachineVersions} from './models'; import {ISapMachineAllVersions, ISapMachineVersions} from './models.js';
export class SapMachineDistribution extends JavaBase { export class SapMachineDistribution extends JavaBase {
constructor(installerOptions: JavaInstallerOptions) { constructor(installerOptions: JavaInstallerOptions) {

View File

@@ -1,9 +1,9 @@
import {JavaBase} from '../base-installer'; import {JavaBase} from '../base-installer.js';
import { import {
JavaDownloadRelease, JavaDownloadRelease,
JavaInstallerOptions, JavaInstallerOptions,
JavaInstallerResults JavaInstallerResults
} from '../base-models'; } from '../base-models.js';
import semver from 'semver'; import semver from 'semver';
import { import {
extractJdkFile, extractJdkFile,
@@ -13,12 +13,12 @@ import {
renameWinArchive, renameWinArchive,
MAX_PAGINATION_PAGES, MAX_PAGINATION_PAGES,
validatePaginationUrl validatePaginationUrl
} from '../../util'; } from '../../util.js';
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 fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {ISemeruAvailableVersions} from './models'; import {ISemeruAvailableVersions} from './models.js';
const supportedArchitectures = [ const supportedArchitectures = [
'x64', 'x64',

View File

@@ -4,16 +4,16 @@ import * as tc from '@actions/tool-cache';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import semver from 'semver'; import semver from 'semver';
import * as gpg from '../../gpg'; import * as gpg from '../../gpg.js';
import {ADOPTIUM_PUBLIC_KEY} from './adoptium-key'; import {ADOPTIUM_PUBLIC_KEY} from './adoptium-key.js';
import {JavaBase} from '../base-installer'; import {JavaBase} from '../base-installer.js';
import {ITemurinAvailableVersions} from './models'; import {ITemurinAvailableVersions} from './models.js';
import { import {
JavaDownloadRelease, JavaDownloadRelease,
JavaInstallerOptions, JavaInstallerOptions,
JavaInstallerResults JavaInstallerResults
} from '../base-models'; } from '../base-models.js';
import { import {
extractJdkFile, extractJdkFile,
getNextPageUrlFromLinkHeader, getNextPageUrlFromLinkHeader,
@@ -22,9 +22,9 @@ import {
renameWinArchive, renameWinArchive,
MAX_PAGINATION_PAGES, MAX_PAGINATION_PAGES,
validatePaginationUrl validatePaginationUrl
} from '../../util'; } from '../../util.js';
export {ADOPTIUM_PUBLIC_KEY} from './adoptium-key'; export {ADOPTIUM_PUBLIC_KEY} from './adoptium-key.js';
export enum TemurinImplementation { export enum TemurinImplementation {
Hotspot = 'Hotspot' Hotspot = 'Hotspot'
@@ -102,7 +102,8 @@ export class TemurinDistribution extends JavaBase {
throw new Error( throw new Error(
`Failed to verify signature for Temurin version ${javaRelease.version} from ${javaRelease.signatureUrl}: ${ `Failed to verify signature for Temurin version ${javaRelease.version} from ${javaRelease.signatureUrl}: ${
(error as Error).message (error as Error).message
}` }`,
{cause: error}
); );
} }
} }

View File

@@ -5,20 +5,20 @@ import path from 'path';
import fs from 'fs'; import fs from 'fs';
import semver from 'semver'; import semver from 'semver';
import {JavaBase} from '../base-installer'; import {JavaBase} from '../base-installer.js';
import {IZuluVersions} from './models'; import {IZuluVersions} from './models.js';
import { import {
extractJdkFile, extractJdkFile,
getDownloadArchiveExtension, getDownloadArchiveExtension,
convertVersionToSemver, convertVersionToSemver,
isVersionSatisfies, isVersionSatisfies,
renameWinArchive renameWinArchive
} from '../../util'; } from '../../util.js';
import { import {
JavaDownloadRelease, JavaDownloadRelease,
JavaInstallerOptions, JavaInstallerOptions,
JavaInstallerResults JavaInstallerResults
} from '../base-models'; } from '../base-models.js';
export class ZuluDistribution extends JavaBase { export class ZuluDistribution extends JavaBase {
constructor(installerOptions: JavaInstallerOptions) { constructor(installerOptions: JavaInstallerOptions) {

View File

@@ -3,8 +3,8 @@ import * as path from 'path';
import * as io from '@actions/io'; import * as io from '@actions/io';
import * as exec from '@actions/exec'; import * as exec from '@actions/exec';
import * as tc from '@actions/tool-cache'; import * as tc from '@actions/tool-cache';
import * as util from './util'; import * as util from './util.js';
import {ExecOptions} from '@actions/exec/lib/interfaces'; import {ExecOptions} from '@actions/exec';
export const PRIVATE_KEY_FILE = path.join(util.getTempDir(), 'private-key.asc'); export const PRIVATE_KEY_FILE = path.join(util.getTempDir(), 'private-key.asc');
@@ -86,7 +86,8 @@ export async function verifyPackageSignature(
throw new Error( throw new Error(
`Failed to create temporary GPG home directory for signature verification: ${ `Failed to create temporary GPG home directory for signature verification: ${
(error as Error).message (error as Error).message
}` }`,
{cause: error}
); );
} }
try { try {

View File

@@ -1,11 +1,11 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import {getBooleanInput} from './util'; import {getBooleanInput} from './util.js';
import { import {
INPUT_SHOW_DOWNLOAD_PROGRESS, INPUT_SHOW_DOWNLOAD_PROGRESS,
MAVEN_ARGS_ENV, MAVEN_ARGS_ENV,
MAVEN_NO_TRANSFER_PROGRESS_FLAG, MAVEN_NO_TRANSFER_PROGRESS_FLAG,
MAVEN_NO_TRANSFER_PROGRESS_LONG_FLAG MAVEN_NO_TRANSFER_PROGRESS_LONG_FLAG
} from './constants'; } from './constants.js';
/** /**
* Configures the MAVEN_ARGS environment variable so that Maven suppresses * Configures the MAVEN_ARGS environment variable so that Maven suppresses

View File

@@ -1,18 +1,19 @@
import fs from 'fs'; import fs from 'fs';
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as auth from './auth'; import * as auth from './auth.js';
import { import {
getBooleanInput, getBooleanInput,
isCacheFeatureAvailable, isCacheFeatureAvailable,
getVersionFromFileContent getVersionFromFileContent
} from './util'; } from './util.js';
import * as toolchains from './toolchains'; import * as toolchains from './toolchains.js';
import * as constants from './constants'; import * as constants from './constants.js';
import {restore} from './cache'; import {restore} from './cache.js';
import * as path from 'path'; import * as path from 'path';
import {getJavaDistribution} from './distributions/distribution-factory'; import {fileURLToPath} from 'url';
import {JavaInstallerOptions} from './distributions/base-models'; import {getJavaDistribution} from './distributions/distribution-factory.js';
import {configureMavenArgs} from './maven-args'; import {JavaInstallerOptions} from './distributions/base-models.js';
import {configureMavenArgs} from './maven-args.js';
async function run() { async function run() {
try { try {
@@ -113,7 +114,12 @@ async function run() {
} }
} }
core.endGroup(); core.endGroup();
const matchersPath = path.join(__dirname, '..', '..', '.github'); const matchersPath = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'.github'
);
core.info(`##[add-matcher]${path.join(matchersPath, 'java.json')}`); core.info(`##[add-matcher]${path.join(matchersPath, 'java.json')}`);
await auth.configureAuthentication(); await auth.configureAuthentication();
@@ -180,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

@@ -3,9 +3,9 @@ import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as io from '@actions/io'; import * as io from '@actions/io';
import * as constants from './constants'; import * as constants from './constants.js';
import {getBooleanInput} from './util'; import {getBooleanInput} from './util.js';
import {create as xmlCreate} from 'xmlbuilder2'; import {create as xmlCreate} from 'xmlbuilder2';
interface JdkInfo { interface JdkInfo {

View File

@@ -6,7 +6,11 @@ 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 {INPUT_JOB_STATUS, DISTRIBUTIONS_ONLY_MAJOR_VERSION} from './constants'; import * as httpm from '@actions/http-client';
import {
INPUT_JOB_STATUS,
DISTRIBUTIONS_ONLY_MAJOR_VERSION
} from './constants.js';
import {OutgoingHttpHeaders} from 'http'; import {OutgoingHttpHeaders} from 'http';
export function getTempDir() { export function getTempDir() {
@@ -379,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);
}

View File

@@ -2,8 +2,8 @@
"compilerOptions": { "compilerOptions": {
/* Basic Options */ /* Basic Options */
// "incremental": true, /* Enable incremental compilation */ // "incremental": true, /* Enable incremental compilation */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "target": "ES2022", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ "module": "NodeNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "allowJs": true, /* Allow javascript files to be compiled. */ // "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */ // "checkJs": true, /* Report errors in .js files. */
"resolveJsonModule": true, "resolveJsonModule": true,
@@ -61,5 +61,5 @@
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}, },
"exclude": ["node_modules", "**/*.test.ts"] "exclude": ["node_modules", "**/*.test.ts", "jest.config.ts"]
} }