A task keeps re-running every build even though nothing seems to change. How do you diagnose why it's never UP-TO-DATE?
answer
- --info prints the change reason
- build scan 'why did this run'
- timestamp/UUID in @Input
- output dir overlap/churn
- doNotTrackState as last resort
basics
~20 sFind which declared input or output changes each run. Use --info to see Gradle's reason for re-running, or a build scan's timeline. Common causes: a timestamp/absolute path baked into an input, or an output written outside the declared location.
solid answer
~50 sWhen a task is never UP-TO-DATE, an input or output fingerprint differs every run. The fastest path is `--info`, which logs a line like "Task ':x' is not up-to-date because: Input property 'y' has changed" or "Output property 'z' file ... has been removed." A **build scan** shows the same under the task's timeline with the exact changed property. Typical culprits: an `@Input` value that embeds a wall-clock timestamp or build-number, an `@InputFiles` set that includes generated files with changing content, absolute paths that vary by machine (a path-sensitivity concern), or a task whose output directory is also another task's working area so files appear/disappear. Once you know which property churns, you either make it deterministic, move it to `@Internal` if it shouldn't affect output, or fix the producing task. If non-determinism is unavoidable, `doNotTrackState("reason")` opts the task out of up-to-date checks entirely, but that means it always runs.
code
bash · 3 lines$ ./gradlew generateConfig --info | grep -A2 'not up-to-date'
Task ':generateConfig' is not up-to-date because:
Input property 'buildTimestamp' has changed.go deeper
Know that --info shows why a task ran and that changing inputs cause re-runs.
Use --info / build scan to find the churning property and name common causes like timestamps in inputs.
Reason about output overlap, CI vs local differences, and choose between fixing determinism and doNotTrackState.
Drive build-correctness standards: forbid non-deterministic tracked inputs, audit doNotTrackState usage, and standardize scan-based diagnosis in CI.
## Symptom: the task always executes A correctly declared task should be UP-TO-DATE on a no-op rebuild. If it isn't, by definition some declared input or output fingerprint changed between runs. Diagnosis is about finding **which** one. ## Step 1 — ask Gradle why Run with `--info`. Before each non-skipped task, Gradle prints the reason it decided to execute: ``` > Task :generateConfig Task ':generateConfig' is not up-to-date because: Input property 'buildTimestamp' has changed. ``` This names the exact property. Other reasons include "No history is available" (first run / cache cleaned), "Output property '...' file ... has changed", and "...has been removed." ## Step 2 — use a build scan for richer data `./gradlew build --scan` produces a timeline where each task's "Why did this task run?" panel lists the changed inputs/outputs and even diffs input property values. This is the best tool for intermittent or CI-only churn. ## Common root causes 1. **Non-deterministic input value.** An `@Input` holding `System.currentTimeMillis()`, a UUID, or a git short-hash changes every build. Fix: derive it from a stable source or move volatile-but-irrelevant values to `@Internal`. 2. **Changing input file content.** An upstream generator emits files with embedded timestamps; downstream tasks then re-run forever. Fix the generator's determinism. 3. **Output overlap / churn.** Two tasks write into the same directory, so each sees the other's files appear and disappear, breaking the output snapshot. Give each task a distinct output location. 4. **Absolute paths in inputs.** Encoding machine-specific paths makes fingerprints machine-specific (handled by path-sensitivity normalization, a sibling topic, but it shows up here as churn). 5. **Missing history.** A clean build or a wiped `.gradle` has no prior snapshot, so the first run always executes — expected, not a bug. ## Step 3 — the escape hatch If an input is legitimately non-deterministic and you cannot make it stable (e.g., a task that probes a live system), declaring it untrackable is honest: ```kotlin tasks.register("probe") { doNotTrackState("Reads a live external service; results are never reproducible") doLast { /* always run */ } } ``` This tells Gradle the task has no reliable up-to-date state, so it always runs (and is never cached). Use it sparingly — it disables the optimization rather than fixing it. ## Mental model The up-to-date decision is deterministic: same declared inputs ⇒ skip. "Always runs" therefore always reduces to "some declared fingerprint isn't stable." Find it, make it stable, or explicitly opt the task out.
- Why might a task be out of date only on CI but UP-TO-DATE locally?CI often does a clean checkout (no .gradle history) or uses machine-specific absolute paths; the first means 'no history available', the second means input fingerprints differ per agent. A shared build cache plus path normalization addresses both.
- When is doNotTrackState the right answer versus a workaround?It's right when the task's result is genuinely non-reproducible (probing live state). It's a workaround — and a smell — when used to mask a fixable non-deterministic input you could stabilize instead.
- What does 'No history is available' as a reason mean?Gradle has no recorded snapshot from a previous run (first build, or .gradle state was cleaned), so it must execute and record fresh history.
saying these in an interview costs you the question
- Reaching for doNotTrackState() before diagnosing the actual churning property.
- Blaming the build cache when the issue is local up-to-date churn from a non-deterministic input.
- Embedding build timestamps/UUIDs in tracked @Input properties and expecting UP-TO-DATE.