skip to content

As a build platform owner, how do you leverage the base/lifecycle-base convention so heterogeneous modules (JVM, JS, docs) all respond uniformly to `gradle build` in CI?

level: seniorimportance: should knowfreq 30%

answer

  1. lifecycle = language-agnostic contract
  2. centralize in convention plugin
  3. assemble=artifacts, check=verification, even for JS/docs
  4. CI: build on main, check on PR
  5. one dependsOn propagates org-wide

basics

~10 s

Ensure every module applies a plugin in the base family so it has assemble/check/build, then hook each module's real work into assemble (artifacts) or check (verification). CI then just runs gradle build everywhere.

solid answer

~50 s

The base/lifecycle-base lifecycle is a cross-language *contract*: any module with these tasks responds to `gradle build` predictably. As platform owner you exploit that by encoding it in a **convention plugin**. Make your convention plugin apply `lifecycle-base` (or `base`) so the umbrella tasks exist, then wire each technology's work consistently — JVM jars and JS bundles into `assemble`, tests/linters/format-checks into `check`. For non-JVM tools (a webpack build, a docs generator) you write small tasks and `dependsOn` them from `assemble`/`check`. CI then calls a single `gradle build` (or `gradle check` for fast PR gates) at the root, and every module — regardless of language — does the right thing. The lifecycle's action-less aggregation means you can add a new verification (e.g. a security scan) org-wide by hooking it into `check` in the convention plugin, and it instantly participates in every existing pipeline without touching CI YAML.

code

kotlin · 8 lines
kotlin
// my.lifecycle-conventions.gradle.kts — applied by every module
plugins { base }

val sbom = tasks.register("generateSbom") { /* ... */ }
tasks.named("assemble") { dependsOn(sbom) }   // artifact-side, org-wide

val depScan = tasks.register("dependencyScan") { /* ... */ }
tasks.named("check") { dependsOn(depScan) }   // verification, org-wide

go deeper

for a junior

Likely beyond junior scope; at most, note that every module getting assemble/check lets CI run one command.

for a middle

Explain hooking each language's work into assemble/check so gradle build is uniform; mention convention plugins.

for a senior

Design the convention-plugin centralization, the PR-check vs main-build CI mapping, and configuration-avoidance at scale.

for a principal

Treat the lifecycle as a governance contract: a single hook point to roll out org-wide controls, with explicit attention to PR latency, caching, and avoiding CI-invisible bespoke tasks.

## The lifecycle as an organizational contract The value of `assemble`/`check`/`build` is that they're a **language-agnostic vocabulary**. A CI system that knows only `gradle build` can drive a JVM service, a TypeScript frontend wrapped in Gradle, and a documentation module — because each has applied something in the base family and hooked its work into the right umbrella task. ## Encode it in a convention plugin Don't scatter lifecycle wiring across modules. Centralize it in a precompiled script plugin or binary convention plugin under `buildSrc`/an included build: ```kotlin // my.lifecycle-conventions.gradle.kts plugins { base } // lifecycle + archives for every consumer // Org-wide verification hooked once, applies everywhere this plugin is applied: val licenseCheck = tasks.register("licenseCheck") { /* ... */ } tasks.named("check") { dependsOn(licenseCheck) } ``` Every module applies `my.lifecycle-conventions`, so they uniformly gain the lifecycle plus the org verifications. ## Wiring non-JVM work For a JS module driven through Gradle: ```kotlin val buildFrontend = tasks.register<Exec>("buildFrontend") { commandLine("pnpm", "build") } val lintFrontend = tasks.register<Exec>("lintFrontend") { commandLine("pnpm", "lint") } tasks.named("assemble") { dependsOn(buildFrontend) } // artifact tasks.named("check") { dependsOn(lintFrontend) } // verification ``` Now `gradle build` builds and lints the frontend with no CI changes. ## CI strategy that falls out of it - **PR gate (fast):** `gradle check` — runs all verification across every module, no artifact production. - **Main/release:** `gradle build` — assemble + check everywhere. - **Caching:** because tasks declare inputs/outputs and hook the lifecycle, the build cache and incremental build apply uniformly; CI just calls one command. ## Governance leverage Adding a new org-wide control (SBOM generation, dependency-vulnerability scan) is a one-line `dependsOn` from `check`/`assemble` in the convention plugin. It propagates to every module instantly — a strong centralization point, but also a place to be careful: hooking a slow task into `check` slows every PR, so gate expensive scans behind separate lifecycle-attached tasks or a dedicated `verification`-style task you opt modules into. ## Pitfalls - Modules that define bespoke top-level tasks instead of hooking the lifecycle become CI-invisible. - Eagerly realizing lifecycle tasks (`getByName`) across many modules hurts configuration time at scale; prefer `named`/configuration avoidance. - Over-stuffing `check` makes PRs slow; balance coverage against feedback latency.

  • Why hook an org-wide security scan into `check` rather than a custom task?
    Because `check` (and thus `build`) is what CI already runs everywhere. Hooking into it makes the scan participate in every pipeline with zero CI changes — though you must watch PR latency.
  • What CI commands map to `assemble` vs `check`?
    Fast PR gates run `gradle check` (verification only, no artifacts). Main/release runs `gradle build` (assemble + check), producing artifacts and verifying.
  • How do you keep a slow scan from slowing every PR?
    Don't hook it into `check`. Attach it to a separate lifecycle-attached task (or opt-in convention) run only on a nightly/release pipeline, keeping the PR `check` fast.

The lifecycle is like a universal power outlet standard: any appliance (language) that fits the plug works with the same wall socket (gradle build) — CI is the socket, not the appliance.

saying these in an interview costs you the question

  • Letting modules define bespoke top-level tasks that bypass the lifecycle (CI-invisible).
  • Stuffing slow scans into `check` and tanking PR feedback time.
  • Duplicating lifecycle wiring per-module instead of centralizing in a convention plugin.

context