Hello, we have a Gradle 9.5.1 multi-project build using Kotlin DSL and migrating to Isolated Projects.
Some subprojects have:
taskA: scans local annotation metadata.taskB: depends ontaskAand is the usual partial-build entry point.
The root project has:
taskAAggregator: merges thetaskAoutputs from the modules involved in the current invocation.taskBAggregator: consumes the merged A output and performs the next cross-project processing step.
For example:
./gradlew :module1:taskB :module2:taskB
should effectively run:
:module1:taskA
:module2:taskA
:taskAAggregator
:module1:taskB
:module2:taskB
:taskBAggregator
The important constraint is that this must remain a partial build. Running :module1:taskB should not force taskA in every other module.
Desired semantics
Conceptually, this is what we want:
taskA.finalizedBy(taskAAggregator)
taskAAggregator.mustRunAfter(taskA)
taskB.finalizedBy(taskBAggregator)
taskBAggregator.mustRunAfter(taskB)
taskBAggregator.dependsOn(taskAAggregator)
The mustRunAfter is important because the aggregator should run after producers that are already in the task graph, but should not pull in producers from unrelated modules.
However, direct cross-project finalizedBy / mustRunAfter wiring is not allowed under Isolated Projects because it requires accessing another project’s task container.
Patterns considered
Artifact/variant-based aggregation is IP-safe and gives proper tracked inputs/outputs, but it forces all producer tasks to run when the aggregate is requested. That does not fit our partial-build requirement.
An AutoCloseable build service can collect only the producers that actually ran, but the resulting aggregate is not a tracked task output and cannot be used cleanly as an input to taskBAggregator.
Possible workaround: local marker/gate tasks
We are considering replacing direct cross-project task references with artifact-based proxy tasks.
In each subproject:
taskAMustRunAfter.mustRunAfter(taskA)
taskA.finalizedBy(taskAFinalizedBy)
taskAMustRunAfteris a lightweight marker task that publishes a dummy artifact.taskAFinalizedByis a lightweight finalizer/gate task that depends on the root aggregator via an artifact dependency.
In the root project:
taskAAggregatorresolves the marker artifacts from all subprojects.taskAAggregatorpublishes its own marker artifact.- subproject
taskAFinalizedBytasks consume that root marker artifact.
This avoids direct cross-project task access. All cross-project relationships go through Gradle’s artifact/variant system.
The intended ordering is:
selected taskA tasks
→ marker tasks
→ taskAAggregator
→ finalizer/gate tasks
The same pattern would be repeated for taskB / taskBAggregator, with taskBAggregator depending on taskAAggregator.
Questions
-
Is there an Isolated Projects-compatible way to express this directly:
“run an aggregator after whichever producer tasks are already in the task graph, without causing all producers to run”? -
Is there a supported pattern for this when the first aggregator must produce a real tracked output that another aggregator consumes as an input?
-
Is the marker/gate task approach valid under Isolated Projects?
In particular:
- Is a subproject-to-root artifact dependency supported under IP?
- Will Gradle resolve the combination of
finalizedByplus transitive artifact-backeddependsOnordering correctly? - Does this approach really avoid running heavy producer tasks in modules that were not part of the original invocation?
-
If this is not the right approach, what workflow would Gradle recommend for partial cross-project aggregation with tracked outputs under Isolated Projects and Configuration Cache?
Any examples from existing plugins or builds with similar partial aggregation behavior would be very helpful.