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.
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:
restore-keys is a list of prefixes used as fallback when the exact key
misses. GitHub picks the newest entry whose key starts with the prefix.
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.
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:
^default — the caret means “my dependencies’ files too”. Without it, changing
a library did not change the fingerprint of the app that imports it, so the app
would replay a build made against the old library.
env entries — this repo builds seven variants from the same sources,
and the variant is selected by environment variable. Without these, a crypto
build and a prediction build hash identically and can be served each other’s
output. This is the entry that turns caching from a speedup into a correctness
question, and it is the reason it is listed.
node --version — output compiled by a different Node major should not be
reused.
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.
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:
<enabled>false</enabled> — the extension defaults to off, and the reactor
command turns it on with -Dmaven.build.cache.enabled=true. So only the one
compile-and-install pass is cached; the later test and SpotBugs/Sonar passes
run for real. You do not want a cached “tests passed”.
<input> block adds non-Java files (src, plus a * glob) to the hash.
The extension’s default input set is Java-centric; this repo’s modules also
build from resources and variant config files, and a module whose only change
was a .conf file must not be treated as unchanged.
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:
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.
cp installs the extension.
trap ... EXIT guarantees removal even if the build dies, so a crashed run
cannot leave the extension behind for the test pass.
restoreOnDiskArtifacts=true means restore the jars from cache rather than
re-resolving them.
rm plus trap - clears the trap, so tests and static analysis run
on a stock Maven.
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.
.nx/cache grows unbounded across runs, since each run restores the
previous snapshot and adds to it. Maven’s side is capped
(maxBuildsCached: 3); nx’s is not. Worth watching the cache size and GitHub’s
10 GB per-repo cache limit — eviction is least-recently-used, so a bloated nx
cache can push out the npm and .m2 caches and make things slower.
* glob in the Maven input config is broad. Broad is the safe
direction (over-hashing causes misses, under-hashing causes wrong hits), but it
means any stray file in a module busts its cache.
env inputs were wrong,
the symptom is one variant building another’s artifacts — green CI, wrong jar.
That is the failure mode to deliberately look for before trusting it.