skip to content

How would you use gradle.taskGraph.whenReady to fail a build fast based on the set of tasks about to run?

level: seniorimportance: should knowfreq 32%

answer

  1. scan allTasks / hasTask, then throw GradleException
  2. aborts before execution = fast feedback
  3. sees transitive closure, not just typed tasks
  4. keep cheap, no side effects, no mutation
  5. onlyIf skips one task; whenReady vetoes whole run

basics

~10 s

Register whenReady, inspect graph.allTasks/hasTask, and throw a GradleException if an invalid or risky combination is detected — so the build aborts before any task action runs.

solid answer

~40 s

Because `whenReady` fires after the graph is built but before execution, it's the ideal place to **validate the plan and fail fast**. Inside the closure, scan `graph.allTasks` or call `graph.hasTask(path)`, and `throw GradleException("...")` (or `require(...)`) when something is wrong — e.g. a publish task scheduled without credentials, two mutually exclusive tasks both requested, or a deploy task pointed at production from a feature branch. Throwing here stops the build immediately, before any actual work runs, giving fast, clear feedback instead of failing midway. Keep the logic cheap and side-effect-free (it runs in every build), and prefer task paths in `hasTask`. This is more robust than checking command-line input because it sees the resolved transitive plan, catching cases where a dangerous task was pulled in as a dependency rather than typed explicitly.

code

kotlin · 9 lines
kotlin
gradle.taskGraph.whenReady { graph ->
    val danger = listOf(":app:deployProd", ":db:dropSchema")
    val scheduled = danger.filter { graph.hasTask(it) }
    if (scheduled.isNotEmpty() && !project.hasProperty("confirm")) {
        throw GradleException(
            "Dangerous tasks ${scheduled} scheduled. Re-run with -Pconfirm to proceed."
        )
    }
}

go deeper

for a junior

Know you can throw a GradleException in whenReady to stop the build before tasks run.

for a middle

Show the allTasks/hasTask scan plus throw, and keep the check cheap.

for a senior

Explain why the resolved closure beats command-line inspection and contrast with onlyIf.

for a principal

Position such guards as governance/policy gates, weigh configuration-cache friendliness, and prefer lazy value sources where the veto can be modeled declaratively.

## Why whenReady is the right place for guards Validation that depends on **what the build is about to do** must happen after the graph exists (configuration is done) but before execution wastes time. `whenReady` is exactly that window. ## The pattern ```kotlin gradle.taskGraph.whenReady { graph -> val willPublish = graph.allTasks.any { it.name.contains("publish", ignoreCase = true) } if (willPublish && System.getenv("PUBLISH_TOKEN").isNullOrBlank()) { throw GradleException("A publish task is scheduled but PUBLISH_TOKEN is not set.") } if (graph.hasTask(":app:deployProd") && currentGitBranch() != "main") { throw GradleException("deployProd may only run from main.") } } ``` ## Why this beats checking the command line `gradle.startParameter.taskNames` only contains what the user typed. A dangerous task can enter the run as a **transitive dependency** of something innocuous. `allTasks`/`hasTask` reflect the **resolved closure**, so the guard catches it regardless of how the task got scheduled. ## Guidelines - **Throw `GradleException`** (or `IllegalStateException`) to abort; the message is shown to the user. - **Keep it cheap** — this closure runs on *every* invocation; avoid network/IO unless gated by a relevant `hasTask` check. - **No mutation** of the graph here; it's finalized. Use it to validate, log, or set a flag, not to add dependencies. - **Prefer paths** in `hasTask` for unambiguous matching in multi-project builds. ## Failing fast vs onlyIf `onlyIf` *skips* an individual task at execution time; `whenReady` can *abort the whole build* before execution. Use `onlyIf` to conditionally skip one task, `whenReady` to veto the entire run based on the global plan. ## Configuration cache note Reading environment/branch state inside the callback can interact with the configuration cache. Where the decision can be modeled with a lazy `Provider`/value source, that is more cache-friendly; use the `whenReady` guard when a true graph-level veto is what you need.

  • Why is checking allTasks better than checking startParameter.taskNames for a safety guard?
    allTasks reflects the resolved transitive plan, so it catches dangerous tasks pulled in as dependencies, whereas taskNames only holds what the user explicitly typed.
  • How does this differ from using onlyIf?
    onlyIf conditionally skips a single task at execution time; whenReady can abort the entire build before execution based on the whole plan.
  • What should you avoid doing inside the whenReady guard?
    Heavy IO on every build, mutating the already-finalized graph, and assuming a bare name matches in multi-project builds.

saying these in an interview costs you the question

  • Validating only startParameter.taskNames, missing tasks added as dependencies.
  • Doing expensive network calls unconditionally in the callback.
  • Trying to remove or re-wire tasks in whenReady instead of just vetoing or flagging.

context