Skip to content

Oracle and GraalVM floating downloads do not use the actual JDK version #1212

Description

@jdubois

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:

  1. The documented version output is not the actual installed Java version; it is only the requested major.
  2. 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:

  • Ubuntu
  • macOS
  • Windows

Runner type:

  • Hosted
  • Self-hosted

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions