A teammate added `outputs.upToDateWhen { false }` to a deploy task and the build is now slow and reruns it constantly even in CI cache scenarios. How do you reason about whether this is correct, and what alternatives exist?
answer
- trackable vs untrackable side effect
- deploy must always run
- cost is graph wiring, not predicate
- split build (cacheable) from publish (always-run)
- onlyIf gating + non-cacheable
basics
~20 sFor a genuine deploy with untrackable remote side effects, upToDateWhen { false } is correct — deploys should always run. The fix isn't to remove it but to ensure the task is non-cacheable and isn't wired as a dependency of fast inner-loop tasks.
solid answer
~50 sFirst decide whether the side effect is *trackable*. A deploy mutates a remote server Gradle can't fingerprint, so always rerunning is the right semantics — `upToDateWhen { false }` is appropriate. The slowness usually isn't the predicate itself; it's that the always-rerun task got wired into the graph of common builds (e.g., a `check` or `assemble` depends on it), so it executes on every invocation. Reasoning checklist: 1. Is the effect trackable? If yes (it produces files deterministically), replace the predicate with proper `inputs`/`outputs` so up-to-date and the build cache work. 2. If no (real deploy), keep `upToDateWhen { false }`, ensure it's **not** `@CacheableTask` (don't let stale outputs come `FROM-CACHE`), and audit its dependents so it only runs when explicitly requested. 3. Consider gating: `onlyIf { project.hasProperty("deploy") }` so it's skipped unless asked. 4. Split: a fast cacheable 'build artifact' task plus a thin always-run 'publish/deploy' task that depends on it. So the predicate is fine; the wiring and caching policy are what to fix.
code
kotlin · 11 linesval buildArtifact = tasks.register<Jar>("buildArtifact") {
// deterministic, file-producing -> cacheable & incremental
outputs.cacheIf { true }
}
tasks.register("deploy") {
dependsOn(buildArtifact)
onlyIf { project.hasProperty("deploy") } // skip unless explicitly asked
outputs.upToDateWhen { false } // the remote push always runs
doLast { /* upload buildArtifact's output */ }
}go deeper
Recognize that deploys should always run and upToDateWhen { false } does that.
Separate the trackable-vs-untrackable question and know onlyIf gates execution.
Diagnose graph wiring, split cacheable build from always-run publish, and keep the deploy non-cacheable for correctness.
Establish conventions: side-effecting tasks are thin, gated, non-cacheable, and never depended on by inner-loop builds; cacheable work is isolated and relocatable.
## Is the side effect trackable? Gradle's incremental engine works by fingerprinting declared **inputs** and **outputs**. It can only skip work it can model as files/values. A deploy that pushes bytes to a remote host changes state Gradle cannot see, so there is no honest fingerprint that says 'already deployed.' For that class of task, `outputs.upToDateWhen { false }` (always run) is the *correct* semantics — not a smell. ## Why the build feels slow The predicate runs in microseconds. The real cost is **graph wiring**: if the always-run task is a dependency (or finalizer) of routine commands, it executes every time those run. Diagnose with `--dry-run` or a build scan to see what pulls the task in. ## Fix options ### 1. Make it trackable (preferred when possible) If the task actually produces local files deterministically, declare them: ```kotlin tasks.named<MyTask>("buildBundle") { inputs.dir("src") outputs.dir(layout.buildDirectory.dir("bundle")) outputs.cacheIf { true } // now cacheable across machines } ``` Now remove `upToDateWhen { false }` entirely. ### 2. Keep always-run, fix policy (for true deploys) - Keep `outputs.upToDateWhen { false }`. - Do **not** annotate `@CacheableTask` / `cacheIf { true }` — otherwise a stale 'deploy' could be served `FROM-CACHE`, which is dangerous. - Gate it: `onlyIf { gradle.startParameter.taskNames.contains("deploy") || project.hasProperty("deploy") }`. ### 3. Split fast core from thin side-effect Separate a cacheable artifact-producing task from a lightweight publisher: ```kotlin val buildArtifact = tasks.register<Jar>("buildArtifact") { /* cacheable */ } tasks.register("deploy") { dependsOn(buildArtifact) outputs.upToDateWhen { false } // the push always runs doLast { upload(buildArtifact.get().archiveFile.get().asFile) } } ``` The expensive build is incremental/cacheable; only the cheap upload reruns. ## CI-cache nuance `upToDateWhen { false }` does not disable the build cache by itself, but for a deploy you specifically want it non-cacheable so stale remote pushes are impossible. The right mental model: cacheable, deterministic, file-producing work vs. always-run, untrackable side effects — keep them in separate tasks.
- Why is making a deploy task `@CacheableTask` dangerous?A cache hit would report FROM-CACHE and skip the actual upload, leaving the remote in a stale state even though the build claims success. Deploys have remote side effects the cache can't represent, so they must stay non-cacheable.
- How would you confirm what's pulling the deploy task into every build?Run with `--dry-run` to print the task graph without executing, or open a build scan to inspect task dependencies and why the task is requested; then break the offending dependsOn/finalizedBy wiring.
- What's the difference between `onlyIf` and `upToDateWhen { false }` here?`onlyIf` decides whether the task runs at all (skipped/excluded if false); `upToDateWhen` decides whether a task that WILL run can be considered already-done. They're complementary: gate execution with `onlyIf`, force real work with `upToDateWhen { false }`.
saying these in an interview costs you the question
- Blanket-removing `upToDateWhen { false }` from a real deploy, which would let a 'deployed' state be falsely skipped.
- Making the deploy cacheable to speed things up — risks serving a stale deploy FROM-CACHE.
- Assuming the predicate itself is the performance cost rather than the task graph wiring.