skip to content

A teammate adds --rerun-tasks to the CI build because a task 'keeps producing stale output'. How would you diagnose and respond?

level: seniorimportance: should knowfreq 35%

answer

  1. CI rerun = symptom not fix
  2. root cause = under-declared inputs/outputs
  3. diagnose with -i / --scan
  4. localize with upToDateWhen{false}+@DisableCaching
  5. global flag kills cache/incremental org-wide

basics

~20 s

Forcing reruns in CI hides the real bug and kills incremental/cache speed. Find why the task skips when it shouldn't — usually missing or wrong input/output declarations — and fix the declaration so up-to-date checking is correct.

solid answer

~50 s

I'd treat `--rerun-tasks` in CI as a symptom, not a fix. Stale-but-skipped output almost always means the task's **inputs or outputs are under-declared**: an input file or property Gradle isn't tracking changes, so up-to-date checking concludes nothing changed and skips (`UP-TO-DATE`), or the outputs aren't declared so cache keys/state are wrong. Diagnosis: run with `--info`/`-i` (or `--build-cache` + a build scan) to see *why* the task was up-to-date or `FROM-CACHE`, and inspect the `@Input*`/`@Output*` annotations or the script's `inputs`/`outputs` wiring. If the staleness is intrinsic (external non-snapshotable state), the right localized control is `outputs.upToDateWhen { false }` plus `@DisableCachingByDefault` on that one task — not a global `--rerun-tasks` that re-runs *everything* and throws away all incremental and cache benefits org-wide. Use `--rerun`/`--rerun-tasks` only for ad-hoc local debugging. The durable fix is correct declarations checked into the build.

code

kotlin · 7 lines
kotlin
// Instead of CI-wide --rerun-tasks, fix the declaration so it reruns correctly:
tasks.register("renderTemplate") {
    inputs.file("src/template.mustache")          // was untracked -> caused stale skips
    inputs.property("version", project.version)
    outputs.file(layout.buildDirectory.file("out.txt"))
    doLast { /* render */ }
}

go deeper

for a junior

Recognize forcing rerun is a workaround and the real issue is the task being skipped wrongly.

for a middle

Know to check input/output declarations and use -i to see the up-to-date reason.

for a senior

Drive root-cause to under-declared inputs/outputs; prefer localized upToDateWhen{false}+@DisableCachingByDefault over global flags; weigh cache/perf cost.

for a principal

Set org policy: no blanket --rerun-tasks in pipelines; enforce correct task declarations, protect remote-cache integrity, and document non-incremental tasks with a rationale.

## Reframe the request `--rerun-tasks` (or repeatedly `--rerun` on a task) forces re-execution by bypassing up-to-date and cache. As a **permanent CI setting** it's almost always wrong: it slows every pipeline, nullifies the build cache's hit-rate, and masks a real correctness bug rather than fixing it. The interview-grade answer is to diagnose root cause. ## Why a task is wrongly UP-TO-DATE / FROM-CACHE Gradle decides up-to-dateness purely from **declared** inputs and outputs. Common failure modes: - An input the task actually reads (a config file, an env-derived property, a generated source dir) is **not declared** as an `@InputFile`/`@InputDirectory`/`@Input`/`@InputFiles`. Gradle can't see it change, so it skips. - Outputs aren't declared, so Gradle's snapshot is incomplete and cache keys are wrong. - The task mutates files **outside** its declared outputs, so another task sees stale state. - A `@CacheableTask` with non-relocatable or non-reproducible outputs gets a stale `FROM-CACHE` hit. ## Diagnosis steps 1. Run the task alone with `-i`/`--info`: Gradle logs the reason it was up to date or out of date. 2. Use a **build scan** (`--scan`) or the cacheability report to see input snapshots and the cache key. 3. Inspect the task type's annotations or the script's `inputs {}` / `outputs {}` wiring against what the task *actually* reads and writes. ## The fix ladder - **Best:** declare the missing inputs/outputs correctly. Then normal up-to-date checking works, the task reruns exactly when it should, and it stays incremental + cacheable. - **If outputs are genuinely non-deterministic / externally driven:** localize the force to that one task with `outputs.upToDateWhen { false }` and `@DisableCachingByDefault(because = "...")`. This is targeted and documented, unlike a global CLI flag. - **Debugging only:** `--rerun` (one task) or `--rerun-tasks` (whole build) for a manual local run. ## Why localized beats global ```kotlin // Targeted: only this task is non-incremental, with a recorded reason tasks.named("generateBuildInfo") { outputs.upToDateWhen { false } } ``` A global `--rerun-tasks` re-executes compile, test, packaging, everything — every CI run. The localized override re-runs just the one problematic node and leaves the rest of the graph fast and cacheable. And ideally even that node gets proper input declarations so it can stay incremental. ## Talking points for the interview - Distinguish *forcing* (a workaround) from *fixing declarations* (the cure). - Name the cost dimension: cache hit-rate and pipeline time across many builds. - Show you know the localized programmatic controls, not just CLI flags.

  • How do you find out why Gradle considered a task up to date?
    Run with --info/-i (it logs the up-to-date reason per task) or use a build scan (--scan) which shows input snapshots and the cache key. That reveals the missing or unchanged input.
  • If the output truly can't be made reproducible, what's the localized fix?
    Apply outputs.upToDateWhen { false } and @DisableCachingByDefault(because = ...) to that single task, instead of a global --rerun-tasks, so only that node is non-incremental and it's documented.
  • Why is global --rerun-tasks costly in CI specifically?
    It re-executes the entire graph every run and bypasses the build cache, so you lose incremental builds and remote-cache hits across every pipeline, lengthening every build.

saying these in an interview costs you the question

  • Accepting --rerun-tasks as a permanent CI fix.
  • Not investigating missing input/output declarations as the root cause.
  • Confusing a stale FROM-CACHE hit with an up-to-date skip when diagnosing.

context