Skip to content

Cache resolved JDK releases to remove the vendor API from warm jobs - #1208

Merged
brunoborges merged 7 commits into
mainfrom
brunoborges-urban-telegram
Aug 5, 2026
Merged

Cache resolved JDK releases to remove the vendor API from warm jobs#1208
brunoborges merged 7 commits into
mainfrom
brunoborges-urban-telegram

Conversation

@brunoborges

@brunoborges brunoborges commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Description:

Only Temurin is preinstalled in the runner tool cache. For every other distribution (Zulu, Corretto, Liberica, SapMachine, Microsoft, Semeru, ...) findInToolcache() misses on essentially every job, so setup-java must call the distribution's metadata API to learn which release satisfies java-version before it can even compute the JDK cache key. That makes the vendor a hard per-job dependency even when the JDK bytes are already in the Actions cache, and turns a vendor 403, 429, or outage into a job failure.

This stores the resolved release itself in a small companion cache entry, keyed only on inputs known before any network call: runner OS, architecture, distribution, package type, requested version, and stability. A job that finds a current entry installs the JDK without contacting the metadata API at all.

Why the key and path are shaped this way

@actions/cache derives a cache version by hashing the requested paths (getCacheVersion in cacheUtils), and getCacheEntry(keys, paths) matches on it, so save and restore paths must be identical. That rules out any scheme keyed on the resolved version, since the path would have to contain a version we do not know yet. Instead the entry uses a path that excludes the freshness window while the key includes it:

path:        $RUNNER_TEMP/setup-java-jdk-resolution/<identityDigest>
primary key: setup-java-jdkres-v1-<os>-<arch>-<identityDigest>-<windowStart>
restoreKeys: setup-java-jdkres-v1-<os>-<arch>-<identityDigest>-

An entry from an earlier window is deliberately not used directly. setup-java still queries the metadata API, so a floating request such as java-version: 21 keeps picking up new releases; the older entry is used only when that query fails, which is what keeps a job working through an outage or rate limit. Because the entry also carries the download URL and checksum, that fallback works even when the JDK itself is not cached and still has to be downloaded. The fallback is reported with a warning.

The window is seven days. GitHub removes cache entries that have not been accessed for seven days, so anything longer would leave the previous entry evicted by the time the window rolls over, removing the fallback exactly when it is most likely to be needed. Anything shorter penalizes low-frequency repositories, which are the ones least likely to have a warm tool cache: a daily window would give no benefit at all to a workflow that runs weekly. Seven days also comfortably covers JDK release cadence, which is monthly at its fastest and usually quarterly.

Things worth a careful look

  • Releases whose URL is not content-addressed are never stored. Oracle JDK and Oracle GraalVM build a /latest/ URL when java-version names only a major version, and the bytes behind it change whenever a new build is published, so its URL and its .sha256 sibling are only consistent with each other at the moment they are resolved. Caching that pair could produce a checksum mismatch that looks like tampering. Those releases are marked floating and skipped. A more specific version resolves an archived URL and is stored normally.
  • The restored payload drives the download, so it is validated as untrusted input: HTTPS URLs only, supported checksum algorithms only, unknown fields dropped. This does not widen the trust model, since the existing JDK cache already restores an executable JDK from the same cache scope under a derivable key.
  • The post-job save rewrites the payload the key was computed for rather than uploading whatever is on disk. A restore performed by a later step in the same job targets the same path, so without this a stale payload could be promoted into the current window's key.
  • Gating: the cache is bypassed entirely for check-latest: true, java-version: latest, and force-download: true, and it is coupled to cache-jdk so nothing is written unless the user opted into caching.

Every cache interaction degrades to today's behavior on failure.

Related issue:

N/A

Check list:

  • Ran npm run check locally (format, lint, build, test) and all checks pass.
  • Mark if documentation changes are required.
  • Mark if tests were added or updated to cover the changes.

Only Temurin is preinstalled in the runner tool cache, so for every other
distribution `findInToolcache()` misses on essentially every job. That
forces a call to the distribution's metadata API before the JDK cache key
can even be computed, which makes the vendor a hard per-job dependency
even when the JDK bytes are already cached, and turns a vendor 403, 429,
or outage into a job failure.

Store the resolved release in a small companion cache entry keyed only on
inputs known before any network call: runner OS, architecture,
distribution, package type, requested version, and stability. A job that
finds a current entry installs the JDK without contacting the metadata API
at all.

`@actions/cache` derives a cache version by hashing the requested paths, so
save and restore paths must match. The entry therefore uses a path that
excludes the date bucket while the key includes it, which lets restore keys
fall back to an older bucket. An entry older than the current day is not
used directly: the metadata API is still queried so floating requests such
as `java-version: 21` keep picking up new releases, and the older entry is
used only when that query fails. Because the entry also carries the
download URL and checksum, that fallback works even when the JDK itself is
not cached.

Releases whose URL is not content-addressed are never stored. Oracle JDK
and Oracle GraalVM build a `/latest/` URL for a major-only version, and its
bytes change when a new build is published, so the URL and checksum are
only consistent at the moment they are resolved. Mark those releases
floating and skip recording them.

Restored payloads are validated as untrusted input, and the post-job save
rewrites the payload the key was computed for rather than uploading
whatever is on disk, since a restore in a later step targets the same path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b76d8cb0-f629-46e1-bf9a-ffde06948644
@brunoborges
brunoborges requested a review from a team as a code owner August 5, 2026 03:38
Copilot AI lite review requested due to automatic review settings August 5, 2026 03:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a companion cache for “resolved JDK release metadata” so setup-java can avoid calling vendor metadata APIs on warm runs (when JDK caching is enabled), and can fall back to a previously-resolved release during vendor outages/rate limiting.

Changes:

  • Introduces a new jdk-resolution-cache module to restore/register/save resolved JavaDownloadRelease payloads keyed by inputs available pre-network.
  • Wires resolution-cache restore/register into JavaBase resolution flow, and saves the companion cache in the post-job cleanup step.
  • Marks vendor /latest/ style releases as floating (Oracle JDK, Oracle GraalVM) so they are never stored as reusable cached resolutions; updates docs and tests accordingly.
Show a summary per file
File Description
src/jdk-resolution-cache.ts New module implementing restore/register/save of resolved JDK release metadata with validation and date-bucket logic.
src/distributions/oracle/installer.ts Marks major-only /latest/ Oracle JDK URL resolutions as floating.
src/distributions/graalvm/installer.ts Marks GraalVM /latest/ URL resolutions as floating for major-only requests.
src/distributions/base-models.ts Extends JavaDownloadRelease with optional floating flag.
src/distributions/base-installer.ts Adds resolveJavaRelease() to prefer resolution-cache, register non-floating resolutions, and fall back to stale cached resolution on vendor API failure.
src/cleanup-java.ts Adds post-job save of JDK resolution caches when cache-jdk is enabled.
docs/advanced-usage.md Documents “Caching release resolution” behavior, gating, validation, and floating-release exclusions.
tests/jdk-resolution-cache.test.ts Adds unit tests for restore/register/save behavior and payload validation.
tests/distributors/oracle-installer.test.ts Verifies floating is set only for Oracle /latest/ URLs.
tests/distributors/graalvm-installer.test.ts Updates expected releases to include floating and validates /latest/ behavior.
tests/distributors/base-installer.test.ts Adds coverage for resolution-cache integration in JavaBase.setupJava().
dist/setup/968.index.js Compiled output updates for GraalVM floating flag.
dist/setup/348.index.js Compiled output for new resolution-cache chunk (setup side).
dist/setup/242.index.js Compiled output updates for JavaBase.resolveJavaRelease() integration.
dist/setup/182.index.js Compiled output updates for Oracle floating flag.
dist/cleanup/index.js Compiled output updates to save resolution cache in cleanup step.
dist/cleanup/348.index.js Compiled output for new resolution-cache chunk (cleanup side).

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 11/17 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/distributions/base-installer.ts
brunoborges and others added 6 commits August 4, 2026 23:43
A daily window gives no benefit to the repositories that need it most.
A repository whose workflows run once a day would re-resolve on every job,
and one running weekly would never see a current entry at all, yet those
are exactly the repositories with nothing warm in the tool cache.

Seven days is also the ceiling. GitHub removes cache entries that have not
been accessed for seven days, so a longer window would leave the previous
entry evicted by the time the window rolls over, removing the stale
fallback at the moment it is most likely to be needed. It comfortably
covers JDK release cadence, which is monthly at its fastest and usually
quarterly.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b76d8cb0-f629-46e1-bf9a-ffde06948644
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
The pre-commit hook runs `eslint --fix` after `npm run check` has already
built `dist/`, so the fix it applied to the resolution fallback warning in
`base-installer.ts` never reached the bundle.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b76d8cb0-f629-46e1-bf9a-ffde06948644
The autofix accepted on the pull request edited the resolution fallback
warning in `base-installer.ts` through the GitHub UI, which does not run
`npm run build`, so `dist/` still carried the pre-fix bundle.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b76d8cb0-f629-46e1-bf9a-ffde06948644
@brunoborges
brunoborges merged commit ab597f9 into main Aug 5, 2026
85 checks passed
@brunoborges
brunoborges deleted the brunoborges-urban-telegram branch August 5, 2026 03:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants