skip to content

How would you iterate over all included builds and wire a common task across each of them, for example an aggregate `cleanAll`?

level: seniorimportance: should knowfreq 25%

answer

  1. gradle.includedBuilds collection
  2. map { it.task(":clean") }
  3. dependsOn accepts a list of references
  4. no automatic cascade → aggregate task
  5. task must exist at graph time

basics

~10 s

Iterate gradle.includedBuilds, and for each handle call .task(":clean"), adding it to an aggregate task: tasks.register("cleanAll") { dependsOn(gradle.includedBuilds.map { it.task(":clean") }) }.

solid answer

~40 s

`gradle.includedBuilds` is a collection of `IncludedBuild` handles. You can build an aggregate task that fans out to a same-named task in every included build by mapping each handle to a `TaskReference`: ```kotlin tasks.register("cleanAll") { dependsOn(gradle.includedBuilds.map { it.task(":clean") }) } ``` Each `it.task(":clean")` is rooted in that included build's root project and resolved lazily. This is the canonical pattern for composite-wide aggregate tasks (`cleanAll`, `publishAll`, `checkAll`) because there is no automatic cascade — a root task touches included builds only when it explicitly depends on their tasks. Caveat: this assumes every included build actually *has* a `:clean` task; if one might not, guard it, because `.task()` for a missing task fails when the graph resolves. For `clean` specifically the base/lifecycle plugin guarantees it across typical builds.

code

kotlin · 6 lines
kotlin
tasks.register("cleanAll") {
    group = "build"
    description = "clean root + all included builds"
    dependsOn(tasks.named("clean"))
    dependsOn(gradle.includedBuilds.map { it.task(":clean") })
}

go deeper

for a junior

Recall that gradle.includedBuilds exists and can be iterated.

for a middle

Write the aggregate-task loop and explain the lazy reference.

for a senior

Handle caveats — missing tasks, no transitive traversal, configuration-cache compatibility — and justify aggregate tasks.

for a principal

Encapsulate aggregate wiring in a convention/settings plugin so all composites get consistent *All tasks.

## The collection of handles The `Gradle` object exposes `includedBuilds` — a `Collection<IncludedBuild>` of every build wired in via `includeBuild`. Each handle lets you address a task inside that build with `.task(path)`, where `path` is absolute within *that* build. ## Building an aggregate task Because included-build tasks never run automatically with the root lifecycle, teams create explicit aggregate tasks: ```kotlin // build.gradle.kts of the root build tasks.register("cleanAll") { group = "build" description = "Runs clean in the root build and all included builds" dependsOn(tasks.named("clean")) // root build's own clean dependsOn(gradle.includedBuilds.map { it.task(":clean") }) } ``` Key mechanics: - `gradle.includedBuilds.map { ... }` produces a `List<TaskReference>`; `dependsOn` accepts a collection, so all of them are wired. - Each `TaskReference` is **lazy** — Gradle resolves it when assembling the composite task graph, sidestepping cross-build configuration ordering. - The path `":clean"` is rooted at each included build's **root project**. If you want clean across *its* subprojects too, that build's own `clean` lifecycle already aggregates them. ## Same pattern, other tasks ```kotlin tasks.register("publishAll") { dependsOn(gradle.includedBuilds.map { it.task(":publishToMavenLocal") }) } ``` ## Caveats 1. **Task must exist.** `.task(":foo")` resolves to a real task at graph time; if an included build lacks `:foo`, resolution fails. Filter handles or ensure the task exists everywhere first. 2. **No transitive include traversal by default.** `gradle.includedBuilds` lists the builds *this* build includes. If an included build itself includes further builds, those aren't in your collection — address them from where they're included or rely on each build's own aggregates. 3. **Configuration cache.** Capturing `gradle.includedBuilds` at configuration time and mapping to references is compatible with the configuration cache because the references are declared as task dependencies, not executed eagerly. ## Why not a plugin convention In large composites you'd typically encapsulate this in a convention/settings plugin so every root build gets `cleanAll`/`checkAll` for free rather than copy-pasting the loop.

  • Why is `dependsOn(gradle.includedBuilds.map { ... })` safe with respect to configuration ordering?
    Because `.task()` returns a lazy `TaskReference`; nothing in the other build is configured or executed at the point you declare the dependency — it's resolved when the composite graph is built.
  • Will an included build that itself includes another build show that nested build in `gradle.includedBuilds`?
    No. `gradle.includedBuilds` lists only the builds the current build directly includes. Nested inclusions are addressed within the build that declares them.
  • What breaks if one included build lacks the task you map to?
    Resolution of that `TaskReference` fails when the task graph is assembled. You must ensure the task exists in every targeted build or filter the handles.

saying these in an interview costs you the question

  • Assuming a root lifecycle task automatically cascades into included builds.
  • Eagerly calling `.task(...).get()` or configuring included-build tasks at configuration time.
  • Mapping to a task that doesn't exist in every included build.

context