CI build output caching — what this branch does

The one-sentence version

Right now every push rebuilds all 45 modules from scratch. This branch makes CI remember the output of work it has already done, so a push that touches three modules only rebuilds those three. Nothing about the code being built changes — only how much of the build is repeated.

There are two build systems in this repo (Java/Maven and Node/nx), so there are two halves to the change, plus one shared trick with GitHub’s cache.

Background: the three tools involved

Skip whichever ones you already know.

nx is the task runner sitting under Lerna for the TypeScript side. Its one useful idea: before running a task, it hashes everything the task depends on (source files, config, dependencies) into a single fingerprint. If it has seen that fingerprint before, it does not run the task — it replays the stored output from .nx/cache. Correctness depends entirely on the hash covering everything that can change the output. Miss an input, and nx replays a stale build.

Maven build cache extension is the same idea for Maven, made by the Maven project itself. A “Maven extension” is a plugin that loads before the build starts, so it can intercept module builds. It hashes each module’s inputs, and on a hit it restores the module’s target/ and installed jar instead of compiling. It is off by default and is opted into per invocation.

actions/cache is GitHub’s step for carrying a directory between workflow runs. You give it a key. On a hit it restores the directory; at job end it uploads the directory under that key. Two rules matter here:

The combination of those two rules produces the standard pattern used in this branch: make the exact key unique per run, and restore by prefix. Every run starts from the newest compatible snapshot and saves a fresh one.

Half one: the Node side (nx.json)

The build target’s declared inputs went from just default to:

"inputs": [
  "default",
  "^default",
  { "env": "P8_PRODUCT_VARIATION" },
  { "env": "P8_SOLUTION_VARIATION" },
  { "runtime": "node --version" }
]

Each addition closes a way the old hash could have lied:

The workflow’s restore-keys also scope the cache per variant, so this is belt-and-braces. That is the right call for a caching layer: a wrong cache hit produces a build that is green and wrong, which is far more expensive than a cache miss.

Half two: the Java side

Three pieces, because Maven needs more assembly.

.github/maven/extensions.xml declares the cache extension. Maven only reads extensions from .mvn/extensions.xml in the repo root, and it cannot be enabled by a command-line flag — hence the copy-in described below.

.github/maven/build-cache-config.xml configures it, with two deliberate choices:

The workflow step is the fiddly bit:

./scripts/build.sh -c prepare_build -pv ... -sv ...
cp .github/maven/extensions.xml .mvn/extensions.xml
trap 'rm -f .mvn/extensions.xml' EXIT
./scripts/build.sh --command "mvn_package" --threaded -cs "clean install ... -Dmaven.build.cache.enabled=true ..."
rm -f .mvn/extensions.xml
trap - EXIT

Reading it in order:

  1. prepare_build is run first, on its own. This is not redundant. That function creates and deletes .mvn as part of switching variants, so copying the extension in before it ran would have the file wiped. Running it up front settles the variant; the second prepare_build (which mvn_package always invokes) then sees the variant unchanged and leaves .mvn alone.
  2. cp installs the extension.
  3. trap ... EXIT guarantees removal even if the build dies, so a crashed run cannot leave the extension behind for the test pass.
  4. The build runs with the cache enabled and pointed at the config file. restoreOnDiskArtifacts=true means restore the jars from cache rather than re-resolving them.
  5. Explicit rm plus trap - clears the trap, so tests and static analysis run on a stock Maven.

The cache keys

Both new actions/cache steps follow the same shape. Reading the Maven one:

{os}-{arch}-maven-build-v1-runner-2026-08-12.4-{ProductVariant}-{SolutionVariant}-{hash of poms, .github/maven, scripts, variation-values}-{run_id}-{run_attempt}

Everything before the hash is a scope: OS, CPU arch, runner image version, and variant. Two builds in different scopes must never share a cache. The hash segment invalidates the whole cache when the build definition itself changes. The trailing run_id-run_attempt is the uniqueness trick from earlier — it makes the save key new every time, while restore-keys (identical string minus the run id) restores the newest snapshot from the same scope.

The nx step is the same idea with the inputs that matter for TypeScript: nx.json, lerna.json, scripts/**, every tsconfig*.json, the rspack configs, and the variant’s package-lock.json.

What to look at if you review it