fix(gradle): use object format for dependsOn instead of shorthand strings#34715
fix(gradle): use object format for dependsOn instead of shorthand strings#34715FrozenPandaz merged 12 commits intomasterfrom
Conversation
✅ Deploy Preview for nx-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for nx-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
View your CI Pipeline Execution ↗ for commit 945693f
☁️ Nx Cloud last updated this comment at |
69fa893 to
ee7bc98
Compare
3713b54 to
bc16580
Compare
bc16580 to
9b5dd8c
Compare
a7e652b to
3aff1d4
Compare
…-not-found errors Same-project dependencies are now excluded from the Nx target's dependsOn since Gradle handles internal task ordering automatically. Including them caused Nx to schedule them as separate tasks via the batch executor, which failed when tasks like compileGroovy didn't exist at execution time. CI targets (check-ci, build-ci) now compute same-project dependencies directly from Gradle's task graph instead of reading from the target's dependsOn.
3aff1d4 to
2730c96
Compare
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
Cross-project dependencies now resolve correctly in object format, which causes bootJar to run in batch mode with its dependencies. Instead of checking for Gradle-specific output text (:app:bootJar) which is not captured in batch mode, check for the Nx success message which works regardless of execution mode.
3c34bb2 to
ca21b82
Compare
Same-project dependencies are now included as { "target": "taskName" }
(without a "projects" field). Cross-project dependencies remain grouped
as { "target": "taskName", "projects": [...] }.
…or dependsOn Replace raw Map<String, Any> with typed DependsOnEntry data class for dependsOn entries. Add DependsOnParams enum (FORWARD, IGNORE) for the params field. Rename internal DepEntry to ResolvedTaskDep. Build CI replacement maps as named variables for clarity.
…data class Remove ResolvedTaskDep by building sameProjectDependsOn and crossProjectByTarget inline during the loop.
The executor's get-exclude-task.ts assumed dependsOn entries were always
strings like "project:target". With the new object format
({ target: "name", projects?: [...] }), same-project deps need the
owning project prepended to form a valid Nx task ID.
| if (dep.projects) { | ||
| const projectList = Array.isArray(dep.projects) | ||
| ? dep.projects | ||
| : [dep.projects]; | ||
| // For cross-project deps, use the first project | ||
| return projectList[0] !== 'self' | ||
| ? `${projectList[0]}:${target}` | ||
| : `${owningProject}:${target}`; |
There was a problem hiding this comment.
Critical Bug: Only first project is resolved from multi-project dependencies
When a DependsOnEntry contains multiple projects (e.g., { target: "jar", projects: [": lib1", ":lib2"] }), this function only returns the first project's task ID (:lib1:jar), completely ignoring the remaining projects (:lib2:jar).
This breaks the core feature of the PR - grouping multiple project dependencies. The Kotlin code in TaskUtils.kt lines 329-332 explicitly groups multiple projects into a single entry, but this TypeScript function discards all but the first.
Fix: The function needs to return an array of task IDs, and calling code must iterate over all results:
function resolveDepToTaskIds(
dep: string | { target?: string; projects?: string | string[] },
owningProject: string
): string[] {
if (typeof dep === 'string') {
return [dep];
}
const target = dep?.target;
if (!target) {
return [];
}
if (dep.projects) {
const projectList = Array.isArray(dep.projects)
? dep.projects
: [dep.projects];
return projectList.map(proj =>
proj !== 'self' ? `${proj}:${target}` : `${owningProject}:${target}`
);
}
return [`${owningProject}:${target}`];
}Then update callers to iterate: for (const taskId of resolveDepToTaskIds(dep, project)) { ... }
| if (dep.projects) { | |
| const projectList = Array.isArray(dep.projects) | |
| ? dep.projects | |
| : [dep.projects]; | |
| // For cross-project deps, use the first project | |
| return projectList[0] !== 'self' | |
| ? `${projectList[0]}:${target}` | |
| : `${owningProject}:${target}`; | |
| if (dep.projects) { | |
| const projectList = Array.isArray(dep.projects) | |
| ? dep.projects | |
| : [dep.projects]; | |
| // For cross-project deps, map all projects to task IDs | |
| return projectList.map(proj => | |
| proj !== 'self' | |
| ? `${proj}:${target}` | |
| : `${owningProject}:${target}` | |
| ); | |
| } | |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
There was a problem hiding this comment.
Important
At least one additional CI pipeline execution has run since the conclusion below was written and it may no longer be applicable.
Nx Cloud is proposing a fix for your failed CI:
We've fixed the issue by skipping same-project dependencies (object-format entries without a projects field) in the task exclusion logic. This prevents the Nx batch executor from attempting to schedule same-project tasks that Gradle manages internally, resolving the "task not found" errors that occurred when building Gradle projects with object-format dependsOn entries.
Warning
❌ We could not verify this fix.
Suggested Fix changes
diff --git a/packages/gradle/src/executors/gradle/get-exclude-task.spec.ts b/packages/gradle/src/executors/gradle/get-exclude-task.spec.ts
index d712f94326..ad0068bcac 100644
--- a/packages/gradle/src/executors/gradle/get-exclude-task.spec.ts
+++ b/packages/gradle/src/executors/gradle/get-exclude-task.spec.ts
@@ -148,7 +148,8 @@ describe('getExcludeTasks', () => {
const targets = new Set<string>(['app1:build']);
const runningTaskIds = new Set<string>(['app1:build']);
const excludes = getExcludeTasks(targets, objectNodes, runningTaskIds);
- expect(excludes).toEqual(new Set([':app1:compileJava', ':app2:build']));
+ // Same-project deps (without 'projects' field) are skipped, only cross-project deps are excluded
+ expect(excludes).toEqual(new Set([':app2:build']));
});
});
@@ -239,6 +240,7 @@ describe('getAllDependsOn', () => {
},
};
const dependencies = getAllDependsOn(objectNodes, 'app', 'build');
- expect(dependencies).toEqual(new Set(['app:compileJava', 'lib:jar']));
+ // Same-project deps (without 'projects' field) are skipped, only cross-project deps are included
+ expect(dependencies).toEqual(new Set(['lib:jar']));
});
});
diff --git a/packages/gradle/src/executors/gradle/get-exclude-task.ts b/packages/gradle/src/executors/gradle/get-exclude-task.ts
index 9624b67639..fa06996e03 100644
--- a/packages/gradle/src/executors/gradle/get-exclude-task.ts
+++ b/packages/gradle/src/executors/gradle/get-exclude-task.ts
@@ -59,6 +59,12 @@ export function getExcludeTasks(
const taskDeps = nodes[project]?.data?.targets?.[target]?.dependsOn ?? [];
for (const dep of taskDeps) {
+ // Skip same-project dependencies (object format without 'projects' field)
+ // Gradle handles same-project task ordering automatically
+ if (typeof dep === 'object' && !dep.projects) {
+ continue;
+ }
+
const depTaskId = resolveDepToTaskId(dep, project);
if (depTaskId && !runningTaskIds.has(depTaskId)) {
const gradleTaskName = getGradleTaskNameWithNxTaskId(depTaskId, nodes);
@@ -101,6 +107,12 @@ export function getAllDependsOn(
?.dependsOn ?? [];
for (const dep of directDependencies) {
+ // Skip same-project dependencies (object format without 'projects' field)
+ // Gradle handles same-project task ordering automatically
+ if (typeof dep === 'object' && !dep.projects) {
+ continue;
+ }
+
const depTaskId = resolveDepToTaskId(dep, currentProjectName);
if (depTaskId && !allDependsOn.has(depTaskId)) {
stack.push(depTaskId);
🔔 Heads up, your workspace has pending recommendations ↗ to auto-apply fixes for similar failures.
Or Apply changes locally with:
npx nx-cloud apply-locally CfkM-4C44
Apply fix locally with your editor ↗ View interactive diff ↗
🎓 Learn more about Self-Healing CI on nx.dev
Each test now creates its own workspace to avoid name collisions between kotlin and groovy gradle subprojects that share the same buildTreePath (e.g., :list, :app, :utilities).
|
This pull request has already been merged/closed. If you experience issues related to these changes, please open a new issue referencing this pull request. |
Current Behavior
The Gradle plugin generates
dependsOnentries using the shorthand string format (e.g.,"projectName:taskName"). This doesn't leverage the full object syntax that Nx supports.Expected Behavior
dependsOnentries now use the object format:{ "target": "taskName" }{ "target": "taskName", "projects": ["proj1", "proj2"] }with projects grouped by target nameThis is more explicit, consistent with the CI targets code (which already used object format), and enables the
projectsarray for grouping multiple project dependencies under a single target.Related Issue(s)
N/A - internal improvement