Description:
distribution: oracle and Oracle distribution: graalvm resolve major-only requests through mutable /latest/ download URLs, but both report the requested major as the resolved release version. For example, a request for Java 21 produces JavaDownloadRelease.version === "21" even though the downloaded archive contains a concrete patch/build such as 21.0.x+build.
Oracle JDK constructs the floating URL and returns version: range here:
|
const isOnlyMajorProvided = !range.includes('.'); |
|
const major = isOnlyMajorProvided ? range : range.split('.')[0]; |
|
|
|
const possibleUrls: string[] = []; |
|
|
|
/** |
|
* NOTE |
|
* If only major version was provided we will check it under /latest first |
|
* in order to retrieve the latest possible version if possible, |
|
* otherwise we will fall back to /archive where we are guaranteed to |
|
* find any version if it exists |
|
*/ |
|
if (isOnlyMajorProvided) { |
|
possibleUrls.push( |
|
`${ORACLE_DL_BASE}/${major}/latest/jdk-${major}_${platform}-${arch}_bin.${extension}` |
|
); |
|
} |
|
const floatingUrl = isOnlyMajorProvided ? possibleUrls[0] : undefined; |
|
|
|
possibleUrls.push( |
|
`${ORACLE_DL_BASE}/${major}/archive/jdk-${range}_${platform}-${arch}_bin.${extension}` |
|
); |
|
|
|
if (parseInt(major) < 17) { |
|
throw new Error('Oracle JDK is only supported for JDK 17 and later'); |
|
} |
|
|
|
for (const url of possibleUrls) { |
|
const response = await this.http.head(url); |
|
|
|
if (response.message.statusCode === HttpCodes.OK) { |
|
return { |
|
url, |
|
version: range, |
|
checksum: await this.fetchChecksum(`${url}.sha256`, 'sha256'), |
|
floating: url === floatingUrl |
|
}; |
Oracle GraalVM has the same behavior here:
|
// 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 fileUrl = this.constructFileUrl( |
|
range, |
|
major, |
|
platform, |
|
arch, |
|
extension |
|
); |
|
|
|
const response = await this.http.head(fileUrl); |
|
this.handleHttpResponse(response, range); |
|
|
|
return { |
|
url: fileUrl, |
|
version: range, |
|
checksum: await this.fetchChecksum(`${fileUrl}.sha256`, 'sha256'), |
|
// A major-only range resolves to the vendor's `/latest/` path, whose |
|
// contents change when a new build is published. |
|
floating: !range.includes('.') |
|
}; |
The base installer then compares the existing tool-cache version with that major-only value:
|
} else { |
|
core.info('Trying to resolve the latest version from remote'); |
|
try { |
|
const javaRelease = await this.resolveJavaRelease(); |
|
core.info(`Resolved latest version as ${javaRelease.version}`); |
|
if (!this.forceDownload && foundJava?.version === javaRelease.version) { |
|
core.info(`Resolved Java ${foundJava.version} from tool-cache`); |
|
} else { |
|
let jdkCache: JdkCache | undefined; |
|
if (this.cacheJdk) { |
|
const {getJdkVerificationIdentity} = |
|
await import('../jdk-cache.js'); |
|
jdkCache = { |
|
distribution: this.distribution, |
|
packageType: this.packageType, |
|
architecture: this.architecture, |
|
version: javaRelease.version, |
|
source: this.getJdkReleaseIdentity(javaRelease), |
|
verification: getJdkVerificationIdentity( |
|
this.verifySignature, |
|
this.verifySignaturePublicKey |
|
), |
|
path: this.getJdkCachePath(javaRelease.version) |
|
}; |
|
} |
|
if (!this.forceDownload && jdkCache) { |
|
const {restoreJdk} = await import('../jdk-cache.js'); |
|
const restored = await restoreJdk(jdkCache); |
|
if (restored) { |
|
const restoredPath = this.getRestoredJdkPath(javaRelease.version); |
|
if (restoredPath) { |
|
foundJava = { |
|
version: javaRelease.version, |
|
path: restoredPath |
|
}; |
|
} |
|
} |
|
} |
|
if (!foundJava || foundJava.version !== javaRelease.version) { |
|
core.info('Trying to download...'); |
|
foundJava = await this.downloadTool(javaRelease); |
|
core.info(`Java ${foundJava.version} was downloaded`); |
|
if (jdkCache) { |
|
// Register after the installation exists so its identity is |
This has two observable consequences:
- The documented
version output is not the actual installed Java version; it is only the requested major.
check-latest: true and the latest alias can reuse a stale tool-cache installation after the vendor publishes a newer patch. The remote /latest/ result is still represented as 21, which equals the existing tool-cache entry named 21, so setup-java skips the download even though the floating URL now serves different bytes.
The floating flag prevents these releases from entering the resolution cache, but it does not correct the tool-cache comparison or the reported output.
This applies to Oracle JDK and Oracle GraalVM (distribution: graalvm). It does not apply to graalvm-community, which resolves concrete versions from GitHub release metadata.
Task version:
actions/setup-java@ab597f914a6894678251803485c09f413263b889 (main, unreleased v6 development)
Platform:
Runner type:
Repro steps:
The output mismatch can be reproduced immediately:
jobs:
actual-version:
runs-on: ubuntu-latest
strategy:
matrix:
distribution: [oracle, graalvm]
steps:
- id: setup
uses: actions/setup-java@ab597f914a6894678251803485c09f413263b889
with:
distribution: ${{ matrix.distribution }}
java-version: '21'
check-latest: true
force-download: true
- name: Compare reported and installed versions
shell: bash
env:
REPORTED_VERSION: ${{ steps.setup.outputs.version }}
run: |
ACTUAL_VERSION=$(sed -n 's/^JAVA_VERSION="\(.*\)"/\1/p' "$JAVA_HOME/release")
echo "setup-java output: $REPORTED_VERSION"
echo "JDK release file: $ACTUAL_VERSION"
test "$REPORTED_VERSION" = "$ACTUAL_VERSION"
The comparison fails because setup-java reports 21, while $JAVA_HOME/release contains the concrete installed version.
The stale check-latest path can be covered deterministically in a unit test by resolving two different /latest/ artifacts for the same major. After the first is stored in the tool cache as 21, the second resolution is also represented as 21, and the equality check reuses the first installation.
Expected behavior:
- The
version output identifies the actual installed patch/build.
- Tool-cache entries use an immutable, concrete version.
check-latest: true and latest install a newer patch when the vendor's floating artifact changes.
Actual behavior:
- The action reports and caches only the major version for these floating downloads.
- A cached installation with that major can be accepted as current without comparing the actual patch/build or artifact identity.
Suggested fix:
- Determine the concrete version from authoritative immutable metadata or from the extracted JDK
release file.
- Return and expose that concrete version after installation.
- Store the tool-cache installation under the concrete normalized version.
- Ensure current-version checks for floating URLs compare the concrete version or immutable artifact identity/checksum rather than only the requested major.
- Add tests that simulate two different
/latest/ artifacts for the same major and verify the second one replaces the first.
Description:
distribution: oracleand Oracledistribution: graalvmresolve major-only requests through mutable/latest/download URLs, but both report the requested major as the resolved release version. For example, a request for Java21producesJavaDownloadRelease.version === "21"even though the downloaded archive contains a concrete patch/build such as21.0.x+build.Oracle JDK constructs the floating URL and returns
version: rangehere:setup-java/src/distributions/oracle/installer.ts
Lines 85 to 121 in ab597f9
Oracle GraalVM has the same behavior here:
setup-java/src/distributions/graalvm/installer.ts
Lines 129 to 156 in ab597f9
The base installer then compares the existing tool-cache version with that major-only value:
setup-java/src/distributions/base-installer.ts
Lines 178 to 221 in ab597f9
This has two observable consequences:
versionoutput is not the actual installed Java version; it is only the requested major.check-latest: trueand thelatestalias can reuse a stale tool-cache installation after the vendor publishes a newer patch. The remote/latest/result is still represented as21, which equals the existing tool-cache entry named21, so setup-java skips the download even though the floating URL now serves different bytes.The
floatingflag prevents these releases from entering the resolution cache, but it does not correct the tool-cache comparison or the reported output.This applies to Oracle JDK and Oracle GraalVM (
distribution: graalvm). It does not apply tograalvm-community, which resolves concrete versions from GitHub release metadata.Task version:
actions/setup-java@ab597f914a6894678251803485c09f413263b889(main, unreleased v6 development)Platform:
Runner type:
Repro steps:
The output mismatch can be reproduced immediately:
The comparison fails because setup-java reports
21, while$JAVA_HOME/releasecontains the concrete installed version.The stale
check-latestpath can be covered deterministically in a unit test by resolving two different/latest/artifacts for the same major. After the first is stored in the tool cache as21, the second resolution is also represented as21, and the equality check reuses the first installation.Expected behavior:
versionoutput identifies the actual installed patch/build.check-latest: trueandlatestinstall a newer patch when the vendor's floating artifact changes.Actual behavior:
Suggested fix:
releasefile./latest/artifacts for the same major and verify the second one replaces the first.