Same-named tasks not run on children in an included build

I have a project that consists of multiple sub-projects. The project’s root directory is called “tools”.

Multi-Project Build Basics says:

Finally, you can build and test everything in all projects. Any task you run in the root project folder will cause that same-named task to be run on all the children.

That works. From the root project directory I can run a task (like “test”) and that task is run on all the children.

Now I includeBuild this project in another project, such that “:tools” is the reference to the included project.

If, from the root directory of the other project, I run a task that is a “root” task of the included project it no longer runs on all the child projects in the included project.


tools% ../gradlew test # <-- works, runs "test" in all child projects
tools% cd ..
% ./gradlew :tools:test # <-- only runs "test" in root "tools" project, not child projects
  1. Is this intended behaviour? I can’t find it documented anywhere, and it’s extremely weird that running a task has different results depending on the directory you’re in when you run it.
  2. If this is intended behaviour, is there a straightforward workaround?

I can, for example, ensure that test dependencies are always run with something like this in tools/build.gradle.kts:

tasks.named<Test>("test").configure {
    subprojects.forEach { subproject ->
        subproject.tasks.withType<Test>().forEach {

but this only works for tasks of type Test, and does not match the “Any task you run in the root project folder will cause that same-named task to be run on all the children.” behaviour.

Yes, this is the documented and intended behavior.
If you run a task by path like :test only that task is run, in this case the task called test in the root project.
If you run a task without path like test, then the task is executed in all subprojects that have a task with that name.
When you run :tools:test, then this is with path and so you only run the task test in the tools project and nothing more.

But reaching into subprojects’ models to get the tasks is actually bad idea, especially when using forEach to eagerly evaluate the list which then also only gets the tasks that are already registered at the time you call it. And even if you would use finalizedBy(subproject.tasks.withType<Test>()) it would not be good, as you break task-configuration avoidance for all tasks of type Test then.

Thanks, that makes it a bit clearer. That wildly different behaviour between “task has a path” and “task does not have a path” is pretty surprising.