Incremental Builds, Configuration Cache, Build Cache, Remote Cache, and Reproducibility: Diagnostics, Failure Modes, Security, and Performance
Diagnose stale results, path-sensitive cache misses, configuration-cache problems, untrusted shared cache reuse, and false reproducibility claims without deleting valuable normal caches.
Learning objectives
- Use a fixed evidence-first sequence for cache and reproducibility failures.
- Reproduce an incorrect cache hit caused by an undeclared input and repair it with the narrowest task-model change.
- Diagnose non-relocatable absolute-path inputs without deleting caches.
- Handle Configuration Cache incompatibility and sensitive-state risks using generated reports and lazy providers.
- Explain how untrusted remote cache writers turn performance infrastructure into a software-supply-chain boundary.
1. Diagnostic sequence: preserve evidence before cleaning
- Preserve concise task outcomes, configuration-cache messages, checksums, and the failing command.
-
Confirm
./gradlew --versionand JDK/toolchain identity. - Inspect declared task inputs/outputs and build/settings configuration.
- Inspect task/dependency graph and the exact requested task set.
-
Inspect project
.gradle, build outputs, and the isolated lab cache location. - Inspect compiler/test/plugin errors and configuration-cache HTML reports where generated.
- Apply the least destructive model correction.
- Verify with a controlled rebuild, then independently with clean-room evidence.
Blindly deleting ~/.gradle destroys useful evidence and
can convert a deterministic modeling defect into an intermittent
network/download problem.
2. Broken example: undeclared input creates a stale cached result
Start from the Lesson 2 task, then deliberately make the suffix file
invisible to Gradle by annotating it @Internal even
though the action reads it. This is intentionally incorrect.
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.bundling.AbstractArchiveTask
plugins {
`java-library`
}
group = "dev.academy.cache"
version = "1.0.0"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
tasks.withType<JavaCompile>().configureEach {
options.release.set(17)
}
// These are already Gradle 9+ archive defaults; writing them here makes the
// reproducibility contract visible to the learner and reviewers.
tasks.withType<AbstractArchiveTask>().configureEach {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
}
@CacheableTask
abstract class RenderGreeting : DefaultTask() {
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val templateFile: RegularFileProperty
@get:Input
abstract val audience: Property<String>
// Deliberately wrong: the action reads this file but marks it Internal,
// so it does not participate in up-to-date or build-cache keys.
@get:Internal
abstract val suffixFile: RegularFileProperty
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun render() {
val suffix = suffixFile.get().asFile.readText().trim()
val text = templateFile.get().asFile.readText()
.replace("{{audience}}", audience.get()) + " " + suffix + "\n"
val out = outputFile.get().asFile
out.parentFile.mkdirs()
out.writeText(text)
}
}
tasks.register<RenderGreeting>("renderGreeting") {
templateFile.set(layout.projectDirectory.file("src/greeting/template.txt"))
audience.convention(providers.gradleProperty("audience").orElse("engineers"))
suffixFile.set(layout.projectDirectory.file("src/greeting/suffix.txt"))
outputFile.set(layout.buildDirectory.file("generated/greeting.txt"))
}
# src/greeting/template.txt
printf '%s
' 'Welcome, {{audience}}.' > src/greeting/template.txt
printf '%s
' '[v1]' > src/greeting/suffix.txt
./gradlew clean renderGreeting --build-cache --no-configuration-cache --console=plain | tee broken-v1.log
cat build/generated/greeting.txt
# Change only the hidden/undeclared input.
printf '%s
' '[v2]' > src/greeting/suffix.txt
./gradlew clean renderGreeting --build-cache --no-configuration-cache --console=plain | tee broken-v2.log
cat build/generated/greeting.txt
Because suffix.txt is excluded from task input
identity, Gradle is allowed to reuse the previous cache entry. The
output can still show [v1] after the file changed to
[v2]. That is an
incorrect cache hit caused by an incorrect task model.
3. Repair the model, not the cache
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.bundling.AbstractArchiveTask
plugins {
`java-library`
}
group = "dev.academy.cache"
version = "1.0.0"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
tasks.withType<JavaCompile>().configureEach {
options.release.set(17)
}
// These are already Gradle 9+ archive defaults; writing them here makes the
// reproducibility contract visible to the learner and reviewers.
tasks.withType<AbstractArchiveTask>().configureEach {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
}
@CacheableTask
abstract class RenderGreeting : DefaultTask() {
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val templateFile: RegularFileProperty
@get:Input
abstract val audience: Property<String>
// Correct: suffix content and relative path now participate in task state.
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val suffixFile: RegularFileProperty
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun render() {
val suffix = suffixFile.get().asFile.readText().trim()
val text = templateFile.get().asFile.readText()
.replace("{{audience}}", audience.get()) + " " + suffix + "\n"
val out = outputFile.get().asFile
out.parentFile.mkdirs()
out.writeText(text)
}
}
tasks.register<RenderGreeting>("renderGreeting") {
templateFile.set(layout.projectDirectory.file("src/greeting/template.txt"))
audience.convention(providers.gradleProperty("audience").orElse("engineers"))
suffixFile.set(layout.projectDirectory.file("src/greeting/suffix.txt"))
outputFile.set(layout.buildDirectory.file("generated/greeting.txt"))
}
./gradlew clean renderGreeting --build-cache --no-configuration-cache --console=plain | tee repaired.log
cat build/generated/greeting.txt
# Expected content now ends in [v2].
Changing the task implementation/annotations also changes cache identity, so the stale entry is not a valid match. The repaired suffix input uses relative path sensitivity, allowing its content and project-relative location to matter without making the checkout root part of the key.
4. Failure: absolute paths destroy relocatability
If a repository-relative input is declared with
PathSensitivity.ABSOLUTE, workspace A and B can
calculate different cache keys solely because their checkout roots
differ. This is normally a cache miss, not stale output.
@get:InputFile
@get:PathSensitive(PathSensitivity.ABSOLUTE) // deliberately over-specific
abstract val templateFile: RegularFileProperty
Repair by asking whether absolute location truly changes semantics.
If not, use RELATIVE. Do not weaken path sensitivity
blindly: a task that embeds the absolute path in output genuinely is
not relocatable until the task implementation stops doing that.
5. Failure: Configuration Cache rejects live build-model access
A common anti-pattern is using project or other
build-model objects inside task execution callbacks. The
Configuration Cache needs serializable, isolated task state and can
reject such access.
// Broken pattern for configuration-cache adoption:
tasks.register("whereAmI") {
doLast {
println(project.projectDir) // execution-time access to Project
}
}
./gradlew whereAmI --configuration-cache --console=plain
# Preserve the console path to the generated HTML configuration-cache report.
The repair is to model needed data as task properties/providers
during configuration rather than reaching back into
Project during execution. Use the generated report as
primary evidence; do not switch permanently to warning mode just to
hide the problem.
6. Failure: sensitive values are captured into configuration state
Configuration Cache serializes scheduled task state and encrypts it on disk, but sensitive values should still not be eagerly materialized into task fields or logged. The safe direction is lazy execution-time wiring.
// Better shape: Provider remains lazy and the value is not printed.
val tokenProvider = providers.environmentVariable("LAB_FAKE_TOKEN")
abstract class CallFixture : DefaultTask() {
@get:Input
abstract val endpointName: Property<String>
// Real credentials should use a dedicated credentials/property design and
// should not be emitted to logs or task outputs.
}
For real repository credentials, use protected Gradle properties/credentials APIs and CI secret management. This chapter deliberately does not create a real secret or remote endpoint.
7. Failure: poisoned or untrusted remote cache output
If an attacker or compromised developer can write arbitrary cache entries accepted by trusted CI, a cache restore can introduce generated classes/resources without running the producing task locally. This is why cache authorization is supply-chain policy.
// Illustrative production client policy. Do not execute against example.invalid.
buildCache {
remote<HttpBuildCache> {
url = uri("https://cache.example.invalid/")
isPush = false // developer/read-only posture
// Keep normal TLS validation. Do not set isAllowUntrustedServer=true.
}
}
For a suspected poisoning incident, stop writes, preserve the cache key/build provenance, reproduce with remote cache disabled, compare artifacts, and rotate/rebuild the cache trust domain as an infrastructure incident. Deleting only one developer’s local cache is not a sufficient response.
8. Failure: “same warm workspace” is not reproducibility
Running jar twice in one workspace can yield
UP-TO-DATE. Running after clean with Build
Cache enabled can yield FROM-CACHE. Neither regenerates
independent bytes. A clean-room check uses separate paths and
disables the reuse layers under test.
./gradlew clean jar --no-build-cache --no-configuration-cache
sha256sum build/libs/*.jar
# Repeat from a second checkout/path with the same declared inputs and compare.
9. Performance diagnosis by phase
| Observed delay | Likely layer | Evidence before tuning |
|---|---|---|
| First build spends time resolving artifacts | Dependency resolution/cache |
Repository logs, --info, warm-vs-cold isolated
User Home.
|
| Every invocation spends long before tasks start | Configuration/model | Configuration Cache compatibility/report; build script/plugin cost. |
| Compilation dominates and often misses cache | Task inputs/cache key | Compile task inputs, toolchain, classpath ABI changes, cache debug evidence. |
| Tests dominate | Execution/test model | Suite reports, fork settings, external resources; Chapter 22 evidence. |
| Cache downloads slower than executing task | Remote cache/network | Measure cache transfer and task duration; do not assume more caching is faster. |
Chapter 25 goes deeper into daemon, worker, parallelism, file-system watching, profiling, and memory. Here, performance discussion stays tied to whether reuse is correct and measurable.
Knowledge check
A task restores stale output after only an untracked file changed. What is the first repair?
Declare that file/value as an input with appropriate normalization/path sensitivity; fix the task model rather than deleting caches.
Workspace A and B always miss the cache even with identical bytes. What path-related cause should you inspect?
An input may use absolute path sensitivity or the task may embed absolute paths, making it non-relocatable.
What should you do with a Configuration Cache HTML problem report?
Preserve and read it, identify the unsupported captured state/API, refactor to serializable task properties/providers, then re-run with fail mode.
Why is isAllowUntrustedServer=true a dangerous
“fix”?
It disables normal TLS trust requirements and permits cache-server impersonation.
Why can local cache deletion be the wrong first response to suspected remote-cache poisoning?
It loses evidence and does not address the shared writer/trust boundary that may have produced the malicious entry.
What does an uncached second-workspace checksum add?
Independent evidence that the build can regenerate the same artifact bytes without relying on current workspace or output-cache state.
Official references and version notes
-
Incremental Builds and Build Caching
— task outcome labels including
UP-TO-DATEandFROM-CACHE. -
Build Cache
— enabling local/remote caches, cacheable task outputs, local
DirectoryBuildCache, remote HTTP cache, and push/read controls. - Build Cache concepts — cache keys, stable inputs, repeatable outputs, path sensitivity, relocatability, and overlapping-output risks.
- Debugging Build Cache misses — controlled cache-miss and relocatability diagnosis.
- Configuration Cache — what is cached, configuration inputs, serialization, security considerations, and execution behavior.
- Enabling the Configuration Cache — current opt-in adoption workflow and HTML problem report.
- Configuration Cache debugging — problem reports and supported diagnostic workflow.
- AbstractArchiveTask API — Gradle 9+ reproducible archive defaults: timestamps not preserved and reproducible file ordering enabled.
- Gradle security best practices — reproducible archives and build-security guidance.
Version-sensitive behavior was rechecked against current Gradle primary documentation on 2026-08-24. The mandatory path uses Gradle 9.7.1 through the previously verified Wrapper, JDK 21 as the Gradle runtime/compiler toolchain, Java 17 as the project target, isolated lab state, and no paid service. Both the Build Cache and Configuration Cache are opt-in in Gradle 9.7.1. The Configuration Cache is local-only and cannot currently be shared across developers or CI machines. Remote HTTP build-cache examples are explanatory only; the hands-on cross-workspace exercise uses a disposable shared directory cache so no server or credentials are needed.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.