skip to content

You wrote a custom task that packages a distribution. How do you make `gradle build` run it, and where should it hook?

level: middleimportance: must knowfreq 55%

answer

  1. artifact -> assemble
  2. verification -> check
  3. build = assemble + check
  4. use tasks.named (lazy)
  5. archives configuration also wires assemble

basics

~10 s

Make assemble depend on your packaging task: tasks.named("assemble") { dependsOn(myTask) }. Since build depends on assemble, gradle build will then run it.

solid answer

~40 s

Hook it into the correct lifecycle task by **type of work**. A packaging/artifact task produces output, so wire it into `assemble`: `tasks.named("assemble") { dependsOn(packageDist) }`. Because `build` depends on `assemble`, running `gradle build` (or `gradle assemble`) now runs your task. Conversely, a *verification* task (a custom lint, a schema validator) should hook into `check`, not `assemble`. The key rule is: never hardcode `myTask.dependsOn` chains that bypass the lifecycle — always attach to the standard lifecycle task so your work participates in the conventional `build` invocation. Prefer `tasks.named(...)` (lazy, configuration-avoidance) over `tasks.getByName(...)` so the lifecycle task isn't eagerly realized. For artifact tasks specifically you can alternatively publish to the `archives` configuration via `artifacts { archives(packageDist) }`, which also wires it into `assemble`.

code

kotlin · 7 lines
kotlin
val packageDist by tasks.registering(Zip::class) {
    from(layout.buildDirectory.dir("staged"))
    archiveFileName.set("dist.zip")
}

// Hook the producer into the lifecycle, lazily:
tasks.named("assemble") { dependsOn(packageDist) }

go deeper

for a junior

Knowing tasks.named("assemble") { dependsOn(myTask) } makes build run it is enough.

for a middle

Distinguish assemble vs check by work type and use lazy named. Mention build = assemble + check.

for a senior

Bring up configuration avoidance, the archives-configuration alternative, and why convention participation matters for CI.

for a principal

Frame lifecycle hooking as the contract that keeps gradle build uniform org-wide; design custom plugins to attach correctly rather than expose bespoke entry points.

## The decision: which lifecycle task? The base/lifecycle-base plugins give you three umbrella tasks to attach to. Pick by the nature of the work: - Produces an artifact/output (zip, jar, docker image, generated site) -> **`assemble`**. - Verifies something (tests, linters, format checks, schema validation) -> **`check`**. - Need it for both -> attach to both, or rely on `build` (which depends on both). Do **not** invent your own top-level task and tell users to run it; participate in the convention so `gradle build` Just Works. ## The idiomatic wiring ```kotlin val packageDist by tasks.registering(Zip::class) { from(layout.buildDirectory.dir("staged")) archiveFileName.set("dist.zip") destinationDirectory.set(layout.buildDirectory.dir("distributions")) } tasks.named("assemble") { dependsOn(packageDist) } ``` `tasks.named` returns a `TaskProvider` and does **not** realize the task until it's needed — this is *configuration avoidance*, important for build performance. Using `tasks.getByName("assemble")` would eagerly create/configure the task at configuration time. ## Alternative: the `archives` configuration The `base` plugin's `archives` configuration is the legacy hook. Adding an artifact to it also makes `assemble` depend on the producing task: ```kotlin artifacts { add("archives", packageDist) } ``` Modern builds usually prefer the explicit `dependsOn` on `assemble`, but the `archives` route additionally exposes the artifact for consumption/publishing. ## Verification example ```kotlin val validateSchema by tasks.registering { /* ... */ } tasks.named("check") { dependsOn(validateSchema) } ``` Now `gradle check` and `gradle build` both run your validation. ## Why this matters Consistent hooking is what lets CI pipelines just call `gradle build` regardless of project specifics. A task that's only runnable by its own name is invisible to the convention and frequently gets skipped in CI.

  • Why prefer `tasks.named` over `tasks.getByName`?
    `named` returns a TaskProvider and defers realization (configuration avoidance), so the lifecycle task isn't eagerly configured. `getByName` forces it at configuration time, hurting performance.
  • Where would you hook a custom code-coverage verification task?
    Into `check`, since coverage gating is verification, not artifact production. Then `gradle build` runs it via build -> check.
  • What does adding the task to the `archives` configuration give you beyond `dependsOn`?
    It also exposes the produced artifact for consumption/publishing, while still making `assemble` depend on the producing task.

saying these in an interview costs you the question

  • Hooking an artifact task into `check` (or a verification task into `assemble`).
  • Using `tasks.getByName` and realizing tasks eagerly when `named` would do.
  • Creating a standalone top-level task that bypasses the lifecycle entirely.

context