Conversation
Discover subprojects in Gradle multi-project builds using a custom init script that emits structured project listings. Supports Groovy and Kotlin DSL variants, gradlew wrapper detection, and workspaceDiscoveryIgnore filtering. Adds **/build/** and **/.gradle/** to default ignore patterns. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reviewer's GuideAdds Gradle multi-project workspace discovery to ExhortApi using a custom init script and Gradle wrapper detection, extends workspace ignore patterns, and introduces comprehensive tests for parsing Gradle output and manifest discovery behavior. Sequence diagram for Gradle multi-project manifest discoverysequenceDiagram
actor User
participant WorkspaceDiscovery
participant ExhortApi
participant FileSystem
participant Operations
participant JavaMavenProvider
participant WorkspaceUtils
User ->> WorkspaceDiscovery: discoverWorkspaceManifests(workspaceDir)
WorkspaceDiscovery ->> ExhortApi: discoverWorkspaceManifests(workspaceDir, ignorePatterns)
ExhortApi ->> FileSystem: check settings.gradle / settings.gradle.kts
FileSystem -->> ExhortApi: hasGradleSettings = true
ExhortApi ->> ExhortApi: discoverGradleSubprojects(workspaceDir, ignorePatterns)
ExhortApi ->> ExhortApi: resolveGradleBinary(workspaceDir)
ExhortApi ->> Operations: getWrapperPreference("gradle")
Operations -->> ExhortApi: wrapperPreferred
alt wrapperPreferred
ExhortApi ->> Operations: isWindows()
Operations -->> ExhortApi: isWindows
ExhortApi ->> JavaMavenProvider: traverseForMvnw(wrapperName, buildGradlePath)
JavaMavenProvider -->> ExhortApi: wrapperPath or null
alt wrapperFound
ExhortApi ->> ExhortApi: use wrapperPath as gradleBin
else noWrapper
ExhortApi ->> Operations: getCustomPathOrElse("gradle")
Operations -->> ExhortApi: gradleBin
end
else wrapperNotPreferred
ExhortApi ->> Operations: getCustomPathOrElse("gradle")
Operations -->> ExhortApi: gradleBin
end
ExhortApi ->> FileSystem: createTempFile(initScript)
FileSystem -->> ExhortApi: initScriptPath
ExhortApi ->> FileSystem: writeString(initScriptPath, GRADLE_INIT_SCRIPT)
ExhortApi ->> Operations: runProcessGetFullOutput(workspaceDir, [gradleBin, -q, --no-daemon, --init-script, initScriptPath, daListProjects], null)
Operations -->> ExhortApi: ProcessExecOutput
alt exitCode != 0
ExhortApi ->> WorkspaceUtils: filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns)
WorkspaceUtils -->> ExhortApi: filteredPaths
ExhortApi -->> WorkspaceDiscovery: filteredPaths
else exitCode == 0
ExhortApi ->> ExhortApi: parseGradleInitScriptOutput(output)
ExhortApi ->> FileSystem: resolve build.gradle[.kts] for each subproject
FileSystem -->> ExhortApi: manifestPaths
ExhortApi ->> WorkspaceUtils: filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns)
WorkspaceUtils -->> ExhortApi: filteredPaths
ExhortApi -->> WorkspaceDiscovery: filteredPaths
end
ExhortApi ->> FileSystem: deleteIfExists(initScriptPath)
Class diagram for Gradle multi-project workspace discovery in ExhortApiclassDiagram
class ExhortApi {
-Set~String~ DEFAULT_WORKSPACE_DISCOVERY_IGNORE
-String GRADLE_INIT_SCRIPT
+Set~String~ resolveIgnorePatterns(Set~String~ callerPatterns)
+List~Path~ discoverWorkspaceManifests(Path workspaceDir, Set~String~ ignorePatterns)
-String resolveGradleBinary(Path startDir)
-List~Path~ discoverGradleSubprojects(Path workspaceDir, Set~String~ ignorePatterns)
+List~GradleProject~ parseGradleInitScriptOutput(String raw)
}
class GradleProject {
+String path
+String dir
}
class Operations {
+boolean getWrapperPreference(String tool)
+boolean isWindows()
+String getCustomPathOrElse(String tool)
+ProcessExecOutput runProcessGetFullOutput(Path workDir, String[] command, Map~String,String~ env)
}
class JavaMavenProvider {
+String traverseForMvnw(String wrapperName, String referencePath)
}
class WorkspaceUtils {
+List~Path~ filterByIgnorePatterns(Path workspaceDir, List~Path~ manifestPaths, Set~String~ ignorePatterns)
}
class ProcessExecOutput {
+int getExitCode()
+String getOutput()
}
ExhortApi *-- GradleProject
ExhortApi ..> Operations
ExhortApi ..> JavaMavenProvider
ExhortApi ..> WorkspaceUtils
Operations o-- ProcessExecOutput
Flow diagram for parsing Gradle init script outputflowchart TD
A[Start parseGradleInitScriptOutput] --> B[Check if raw is null or blank]
B -->|yes| C[Return empty list]
B -->|no| D[Split raw by newline into lines]
D --> E[Initialize empty list projects]
E --> F[For each line in lines]
F --> G{line startsWith ::DA_PROJECT::}
G -->|no| F
G -->|yes| H[Split line by :: into parts]
H --> I[Initialize nonEmpty list]
I --> J[For each part in parts]
J --> K{part is not empty}
K -->|yes| L[Add part to nonEmpty]
K -->|no| J
L --> J
J -->|done| M{nonEmpty size >= 3}
M -->|no| F
M -->|yes| N["Create GradleProject(nonEmpty[1], nonEmpty[2])"]
N --> O[Add GradleProject to projects]
O --> F
F -->|done| P[Return projects]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Test Results0 tests 0 ✅ 0s ⏱️ Results for commit eaae875. |
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
parseGradleInitScriptOutput, splitting lines only on"\n"can leave trailing\ron Windows output and potentially corrupt parsed paths; consider usingsplit("\\R")or trimming each line before processing. - The
discoverGradleSubprojectscatch block logs onlye.getMessage(); usingLOG.log(Level.WARNING, "Failed to discover Gradle subprojects", e)(or equivalent) would make debugging Gradle execution failures much easier by preserving the stack trace.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `parseGradleInitScriptOutput`, splitting lines only on `"\n"` can leave trailing `\r` on Windows output and potentially corrupt parsed paths; consider using `split("\\R")` or trimming each line before processing.
- The `discoverGradleSubprojects` catch block logs only `e.getMessage()`; using `LOG.log(Level.WARNING, "Failed to discover Gradle subprojects", e)` (or equivalent) would make debugging Gradle execution failures much easier by preserving the stack trace.
## Individual Comments
### Comment 1
<location path="src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java" line_range="991-1000" />
<code_context>
+ initScriptPath = Files.createTempFile("da-list-projects-", ".gradle");
+ Files.writeString(initScriptPath, GRADLE_INIT_SCRIPT);
+
+ Operations.ProcessExecOutput output =
+ Operations.runProcessGetFullOutput(
+ workspaceDir,
+ new String[] {
+ gradleBin,
+ "-q",
+ "--no-daemon",
+ "--init-script",
+ initScriptPath.toString(),
+ "daListProjects"
+ },
+ null);
+
+ if (output.getExitCode() != 0) {
+ LOG.warning("gradle daListProjects failed with exit code " + output.getExitCode());
+ return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
</code_context>
<issue_to_address>
**suggestion:** Include stderr (and possibly stdout) in the warning when the Gradle task fails to aid diagnosability.
Right now the warning only includes the exit code. Please also log `output.getErrorOutput()` (and possibly a truncated `getOutput()`) to make remote diagnosis easier (e.g., wrong Gradle version, missing wrapper, init script issues). In the catch block, prefer logging the full exception (e.g., `LOG.log(Level.WARNING, msg, e)`) instead of only `e.getMessage()` so the stack trace is preserved.
Suggested implementation:
```java
if (output.getExitCode() != 0) {
StringBuilder msg =
new StringBuilder("gradle daListProjects failed with exit code ")
.append(output.getExitCode());
String stderr = output.getErrorOutput();
if (stderr != null && !stderr.isBlank()) {
msg.append(", stderr: ").append(truncateForLog(stderr));
}
String stdout = output.getOutput();
if (stdout != null && !stdout.isBlank()) {
msg.append(", stdout: ").append(truncateForLog(stdout));
}
LOG.warning(msg.toString());
return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
}
```
```java
public class ExhortApi {
private static String truncateForLog(String text) {
if (text == null) {
return "";
}
final int max = 4000;
String normalized = text.strip();
if (normalized.length() <= max) {
return normalized;
}
return normalized.substring(0, max) + "...[truncated]";
}
```
To fully implement your review comment, you should also update the `catch` block that surrounds this `try` to log the full exception instead of only `e.getMessage()`. For example, change any pattern like:
- `LOG.warning("...: " + e.getMessage());`
to:
- `LOG.log(Level.WARNING, "...", e);`
You may need to ensure `java.util.logging.Level` is imported if it is not already (`import java.util.logging.Level;` near the top of the file).
</issue_to_address>
### Comment 2
<location path="src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java" line_range="1039-1048" />
<code_context>
+
+ record GradleProject(String path, String dir) {}
+
+ static List<GradleProject> parseGradleInitScriptOutput(String raw) {
+ if (raw == null || raw.isBlank()) {
+ return List.of();
+ }
+ List<GradleProject> projects = new ArrayList<>();
+ for (String line : raw.split("\n")) {
+ if (!line.startsWith("::DA_PROJECT::")) {
+ continue;
+ }
+ String[] parts = line.split("::");
+ List<String> nonEmpty = new ArrayList<>();
+ for (String part : parts) {
+ if (!part.isEmpty()) {
+ nonEmpty.add(part);
+ }
+ }
+ if (nonEmpty.size() >= 3) {
+ projects.add(new GradleProject(nonEmpty.get(1), nonEmpty.get(2)));
+ }
</code_context>
<issue_to_address>
**issue (bug_risk):** Handle Windows-style newlines and trailing CR characters when parsing Gradle output.
Because `raw.split("\n")` doesn’t remove `\r` from Windows `\r\n` line endings, the last token can include a trailing `\r`. That can end up in `proj.dir()` / `proj.path()` (e.g. `"C:\\proj\\subproj\r"`) and break `Path` resolution. Please normalize line endings or trim lines/parts (e.g. use `raw.lines()`, `line.strip()`, and `part.strip()`) before parsing.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| Operations.ProcessExecOutput output = | ||
| Operations.runProcessGetFullOutput( | ||
| workspaceDir, | ||
| new String[] { | ||
| gradleBin, | ||
| "-q", | ||
| "--no-daemon", | ||
| "--init-script", | ||
| initScriptPath.toString(), | ||
| "daListProjects" |
There was a problem hiding this comment.
suggestion: Include stderr (and possibly stdout) in the warning when the Gradle task fails to aid diagnosability.
Right now the warning only includes the exit code. Please also log output.getErrorOutput() (and possibly a truncated getOutput()) to make remote diagnosis easier (e.g., wrong Gradle version, missing wrapper, init script issues). In the catch block, prefer logging the full exception (e.g., LOG.log(Level.WARNING, msg, e)) instead of only e.getMessage() so the stack trace is preserved.
Suggested implementation:
if (output.getExitCode() != 0) {
StringBuilder msg =
new StringBuilder("gradle daListProjects failed with exit code ")
.append(output.getExitCode());
String stderr = output.getErrorOutput();
if (stderr != null && !stderr.isBlank()) {
msg.append(", stderr: ").append(truncateForLog(stderr));
}
String stdout = output.getOutput();
if (stdout != null && !stdout.isBlank()) {
msg.append(", stdout: ").append(truncateForLog(stdout));
}
LOG.warning(msg.toString());
return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
}public class ExhortApi {
private static String truncateForLog(String text) {
if (text == null) {
return "";
}
final int max = 4000;
String normalized = text.strip();
if (normalized.length() <= max) {
return normalized;
}
return normalized.substring(0, max) + "...[truncated]";
}To fully implement your review comment, you should also update the catch block that surrounds this try to log the full exception instead of only e.getMessage(). For example, change any pattern like:
LOG.warning("...: " + e.getMessage());
to:LOG.log(Level.WARNING, "...", e);
You may need to ensure java.util.logging.Level is imported if it is not already (import java.util.logging.Level; near the top of the file).
| static List<GradleProject> parseGradleInitScriptOutput(String raw) { | ||
| if (raw == null || raw.isBlank()) { | ||
| return List.of(); | ||
| } | ||
| List<GradleProject> projects = new ArrayList<>(); | ||
| for (String line : raw.split("\n")) { | ||
| if (!line.startsWith("::DA_PROJECT::")) { | ||
| continue; | ||
| } | ||
| String[] parts = line.split("::"); |
There was a problem hiding this comment.
issue (bug_risk): Handle Windows-style newlines and trailing CR characters when parsing Gradle output.
Because raw.split("\n") doesn’t remove \r from Windows \r\n line endings, the last token can include a trailing \r. That can end up in proj.dir() / proj.path() (e.g. "C:\\proj\\subproj\r") and break Path resolution. Please normalize line endings or trim lines/parts (e.g. use raw.lines(), line.strip(), and part.strip()) before parsing.
Summary
daListProjects) that emits structured project listingsbuild.gradle) and Kotlin DSL (build.gradle.kts) variants with automatic detectiongradlewwrapper detection viatraverseForMvnw()reuse, controlled byTRUSTIFY_DA_PREFER_GRADLEWworkspaceDiscoveryIgnorefiltering to discovered subproject paths**/build/**and**/.gradle/**to default ignore patternsdiscoverWorkspaceManifests()between Cargo and JavaScriptTest plan
Jira: TC-4262
🤖 Generated with Claude Code
Summary by Sourcery
Add Gradle multi-project workspace discovery and integrate it into generic workspace manifest detection.
New Features:
Enhancements:
Tests: