Skip to content

chore(usersrole): hash the JDK and stop caching Gradle state - #160

Merged
jdwillmsen merged 2 commits into
mainfrom
fix/usersrole-build-cache
Aug 2, 2026
Merged

chore(usersrole): hash the JDK and stop caching Gradle state#160
jdwillmsen merged 2 commits into
mainfrom
fix/usersrole-build-cache

Conversation

@jdwillmsen

Copy link
Copy Markdown
Member

Closes JDWLABS-282's sibling, JDWLABS-280.

What was wrong

usersrole:build had inputs unset, so it inherited ["default", "^default"]. That is why the JDWLABS-276 guard never flagged it — the guard checks that declared inputs are dependency-aware, and cannot see inputs that are absent from the hash entirely.

The JDK was not in the hash. build.gradle.kts set sourceCompatibility/targetCompatibility = 21 but declared no toolchain, so Gradle compiled with whatever JVM launched it. CI pins temurin 21; a workstation may be on 23 or 25. Same sources, same Nx hash, different bytecode — across machines that is a wrong artifact, not a cache miss.

The outputs were the whole build/ directory, which carries machine-specific state:

build/tmp/compileJava/previous-compilation-data.bin   79KB of Gradle incremental state, keyed to the producing machine
build/reports/tests, build/test-results
build/bootBuildInfo/META-INF/build-info.properties    build.time=2026-07-29T02:02:36.217098300Z

That last file was also stamped build.version=1.0.2 while VERSION reads 1.0.4 — stale output sitting inside a cached directory.

A Docker-dependent task was cached as pure. gradlew build runs checktest, which starts real Postgres containers via Testcontainers.

What changed

  • toolchain { languageVersion = JavaLanguageVersion.of(21) } — makes the build deterministic rather than merely making Nx notice the difference. Preferred over an Nx-side runtime input for that reason.
  • outputs["{projectRoot}/build/libs"] — the actual artifact, nothing else.
  • gradlew buildgradlew assemble — skips the test lifecycle.

Why dropping test from build is not a coverage loss

@nx/gradle already infers a test target for this project:

test   cache=true   outputs=[build/test-results/test/binary, build/reports/tests/test, build/test-results/test]

and CI runs nx affected -t lint test before nx affected -t build. So the Testcontainers suite was running twice per pipeline. assemble removes the duplicate, not the coverage.

Verification

Toolchain, proven not argued — built with a JDK 25 launcher, then read the class-file major version out of the produced jar:

$ bash gradlew clean assemble -Dorg.gradle.java.home=<JDK 25>
BUILD SUCCESSFUL
$ od -An -tu1 -j6 -N2 com/jdw/usersrole/configs/CustomAccessDeniedHandler.class
   0  65          # 65 = Java 21  (69 would be Java 25)

Cache correctness:

run 3  unchanged        1/1 hit (100%)
run 4  source edited    0/1 hit          hash changes
run 5  edit reverted    1/1 hit (100%)   deterministic

Restore contains only the artifact — deleted build/ entirely, then re-ran:

restored: build/libs/{usersrole-1.0.4.jar, usersrole-1.0.4-plain.jar}
absent:   tmp/compileJava/previous-compilation-data.bin, reports,
          test-results, bootBuildInfo, classes

Notes

chore rather than fix on purpose. A fix would trigger a usersrole release, and JDWLABS-275 has releases stalling unmerged right now — this is a build-config change with no runtime effect (CI already compiled on 21), so it should not push a release into a known-broken delivery path.

One claim on the ticket no longer holds: bootJar's inferred output now reads usersrole-1.0.4.jar, matching VERSION, not the 1.0.1 recorded there. The host-local Gradle model snapshot has since refreshed. The underlying fragility is unchanged — those paths are still Windows-shaped ({projectRoot}\build\libs\…) and drift per machine — but the stale-filename defect is not currently present, so nothing was changed for it.

Gradle dependency resolution is still unpinned (Spring Boot BOM versions resolve from Maven Central at build time), so identical hashes can still resolve different jars. Fixing that needs Gradle dependency locking and is deliberately not bundled here.

sourceCompatibility/targetCompatibility only assert the language level;
Gradle still compiles with whatever JVM launched it. CI pins temurin 21, a
workstation may be on 23 or 25, and both produce the same Nx hash from the
same sources -- across machines that is a wrong artifact, not a cache miss.

Verified: with a JDK 25 launcher the emitted class files are major version
65 (Java 21), not 69.
outputs was the whole build directory, which carries machine-specific
state: Gradle's previous-compilation-data.bin incremental cache, test
reports, and a build-info.properties stamped with build.time. Restoring
another machine's incremental state is a known way to confuse Gradle's
up-to-date checks.

The command also ran the full build lifecycle, so every cached run
executed the Testcontainers suite -- a Docker-dependent, side-effecting
task cached as if it were pure. Gradle's test task is already inferred by
the plugin and already runs in the pipeline's test step, so assemble drops
duplicate work rather than coverage.
@jdwillmsen
jdwillmsen force-pushed the fix/usersrole-build-cache branch from 2615c70 to 05c9aae Compare August 2, 2026 05:58
@jdwillmsen
jdwillmsen merged commit 318e5bc into main Aug 2, 2026
17 checks passed
@jdwillmsen
jdwillmsen deleted the fix/usersrole-build-cache branch August 2, 2026 06:01
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.

1 participant