skip to content

When would you use a build-completion hook (buildFinished / FlowAction) versus task finalizedBy? Contrast their semantics.

level: middleimportance: should knowfreq 25%

answer

  1. finalizedBy = per-task, runs even if task fails
  2. completion hook = whole build, once, on any outcome
  3. finalizer only runs if finalized task ran
  4. completion hook not tied to task graph
  5. doLast is not a finalizer

basics

~20 s

Build-completion hooks run once at the end of the whole build, on success or failure — good for build-global teardown/reporting. finalizedBy attaches a finalizer task to a specific task and runs when that task runs — good for per-task cleanup like collecting reports after a test task.

solid answer

~50 s

They operate at different granularities. **`finalizedBy`** is a **task-graph** relationship: `test.finalizedBy(collectReports)`. The finalizer runs whenever its finalized task runs, even if that task fails — but only if the finalized task actually executes (and is in the graph). It's per-task, ordered, and part of normal task scheduling, so it's configuration-cache friendly and integrates with up-to-date checks. **Build-completion hooks** (`gradle.buildFinished`, or a `FlowAction` via `flowScope.always`) fire **once for the entire build**, after all execution, regardless of which tasks ran or failed. They're for build-global concerns: overall reporting, notifications, tearing down a resource shared across the whole build. Rule of thumb: 'after *this task*, do X' → `finalizedBy`. 'after the *whole build*, do X' → completion hook (prefer the Flow API for cache-safety). For a shared resource owned by tasks, a `BuildService` with `AutoCloseable.close()` is often cleaner than either.

code

kotlin · 7 lines
kotlin
// per-task finalizer
tasks.named("test") { finalizedBy("aggregateReports") }

// build-global completion hook (legacy form)
gradle.buildFinished { result ->
    println("build done, failed=${result.failure != null}")
}

go deeper

for a junior

Know finalizedBy is per-task and completion hooks are whole-build.

for a middle

Contrast their firing conditions, failure behavior, and cache-safety; pick correctly per scenario.

for a senior

Add the BuildService AutoCloseable option and reason about configuration-cache implications of each.

for a principal

Define team conventions for teardown/finalization patterns and when each mechanism is mandated.

## finalizedBy — a per-task relationship `taskA.finalizedBy(taskB)` declares that whenever `taskA` is scheduled to run, `taskB` is added as its finalizer. Semantics: - The finalizer runs **after** the finalized task, **even if the finalized task fails**. - It runs only if the finalized task is itself in the execution graph and actually executes. - It is a normal task, so it has inputs/outputs, up-to-date checks, and works with the configuration cache. - Classic use: a `test` task `finalizedBy` an `aggregateTestReports` task, so reports are gathered whether or not tests passed. ## Build-completion hooks — build-global `gradle.buildFinished { ... }` (legacy) and `FlowScope.always { ... }` (modern) run **once at the very end of the build**: - They fire on success and failure of the build as a whole. - They are not tied to any particular task being in the graph. - `buildFinished` is configuration-cache-incompatible; the `FlowAction` form is the cache-safe replacement. ## Side-by-side | Aspect | finalizedBy | Completion hook | |---|---|---| | Granularity | Per finalized task | Whole build, once | | Fires if task didn't run? | No | Yes (build-level) | | Runs on failure? | Yes (if task ran) | Yes | | Config-cache safe? | Yes | Only the FlowAction form | | Typical use | Collect reports, release a per-task lock | Global teardown, notifications, telemetry | ## Choosing ```kotlin // Per-task: gather reports after tests, pass or fail tasks.named("test") { finalizedBy("collectReports") } // Build-global: notify CI at the very end (cache-safe form preferred) flowScope.always(NotifyAction::class.java) { parameters.failed.set(flowProviders.buildWorkResult.map { it.failure.isPresent }) } ``` ## Don't confuse with doLast `doLast` is the *last action of a single task* and only runs if that task runs **and** doesn't fail before reaching it. It is neither a failure-safe per-task finalizer nor a build-global hook.

  • If the finalized task is never scheduled, does its finalizer run?
    No. A finalizer only runs when its finalized task is in the execution graph and runs; otherwise it doesn't fire — unlike a build-completion hook.
  • Which of the two is configuration-cache safe out of the box?
    finalizedBy (it's an ordinary task relationship). Among completion hooks, only the FlowAction form is cache-safe; buildFinished is not.

saying these in an interview costs you the question

  • Saying finalizedBy runs even if the finalized task wasn't scheduled — it doesn't.
  • Using a build-completion hook for per-task report collection.
  • Treating doLast as a failure-safe finalizer.

context