feat(workspace): add Go workspace (go.work) discovery#495
feat(workspace): add Go workspace (go.work) discovery#495Strum355 wants to merge 3 commits intoguacsec:mainfrom
Conversation
Add `discoverMavenModules()` to discover all pom.xml manifest paths in Maven multi-module projects. Uses `mvn help:evaluate -Dexpression=project.modules` to list declared modules, with recursive traversal for nested aggregators. Supports Maven wrapper (mvnw) via `TRUSTIFY_DA_PREFER_MVNW` preference, reusing the same traversal pattern as provider-level wrapper detection. Adds Maven detection (`pom.xml` presence) to `detectWorkspaceManifests()` between Cargo and JavaScript in the ecosystem detection order. Also adds `**/target/**` to the default workspace discovery ignore patterns. Implements TC-4259 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Assisted-by: Claude Code
Discover subprojects in Gradle multi-project builds using a custom init script that emits structured project listings. Supports Groovy and Kotlin DSL variants, wrapper detection via gradlew traversal, and workspaceDiscoveryIgnore filtering. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add discoverGoWorkspaceModules() for discovering go.mod manifest paths in Go multi-module workspaces via `go work edit -json`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reviewer's GuideAdds first-class multi-module workspace discovery for Maven, Gradle, and Go workspaces (go.work), wires them into workspace detection, and exposes the new discovery helpers via the public API, along with expanded default ignore patterns and comprehensive tests/fixtures. Sequence diagram for discoverGoWorkspaceModules using go work edit -jsonsequenceDiagram
participant Caller
participant Workspace as discoverGoWorkspaceModules
participant FS as FileSystem
participant Tools as tools_getCustomPath
participant Go as go_binary
Caller->>Workspace: discoverGoWorkspaceModules(workspaceRoot, opts)
Workspace->>Workspace: root = path.resolve(workspaceRoot)
Workspace->>Workspace: goWork = path.join(root, go.work)
Workspace->>FS: existsSync(goWork)
alt go.work missing
FS-->>Workspace: false
Workspace-->>Caller: []
else go.work present
FS-->>Workspace: true
Workspace->>Tools: getCustomPath(go, opts)
Tools-->>Workspace: goBin
Workspace->>Go: invokeCommand(goBin, [work, edit, -json, goWork], { cwd: root })
alt command fails
Go-->>Workspace: throw error
Workspace-->>Caller: []
else command succeeds
Go-->>Workspace: stdout JSON
Workspace->>Workspace: JSON.parse(stdout)
alt invalid JSON
Workspace-->>Caller: []
else valid JSON
Workspace->>Workspace: useEntries = workspace.Use or []
alt useEntries empty
Workspace-->>Caller: []
else useEntries nonempty
loop for each entry in useEntries
Workspace->>Workspace: diskPath = entry.DiskPath
alt diskPath defined
Workspace->>Workspace: moduleDir = path.resolve(root, diskPath)
Workspace->>Workspace: goMod = path.join(moduleDir, go.mod)
Workspace->>FS: existsSync(goMod)
alt go.mod exists
FS-->>Workspace: true
Workspace->>Workspace: manifestPaths.push(goMod)
else go.mod missing
FS-->>Workspace: false
end
else diskPath missing
Workspace->>Workspace: skip entry
end
end
Workspace->>Workspace: ignorePatterns = resolveWorkspaceDiscoveryIgnore(opts)
Workspace->>Workspace: filtered = filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
Workspace-->>Caller: filtered
end
end
end
end
Flow diagram for updated detectWorkspaceManifests ecosystem selectionflowchart TD
A[start detectWorkspaceManifests root opts] --> B[Check Cargo.toml and Cargo.lock]
B -->|both exist| C[discoverWorkspaceCrates]
C --> C1[Return ecosystem cargo and manifestPaths]
B -->|missing| D[Check pom.xml]
D -->|exists| E[discoverMavenModules]
E -->|manifestPaths length > 0| E1[Return ecosystem maven and manifestPaths]
E -->|manifestPaths length = 0| F[Check settings.gradle or settings.gradle.kts]
D -->|missing| F
F -->|exists| G[discoverGradleSubprojects]
G -->|manifestPaths length > 0| G1[Return ecosystem gradle and manifestPaths]
G -->|manifestPaths length = 0| H[Check go.work]
F -->|missing| H
H -->|exists| I[discoverGoWorkspaceModules]
I -->|manifestPaths length > 0| I1[Return ecosystem gomodules and manifestPaths]
I -->|manifestPaths length = 0| J[Check JS lock files and package.json]
H -->|missing| J
J --> K[Check for pnpm-lock.yaml, yarn.lock, package-lock.json, or npm-shrinkwrap.json]
K -->|has lock and package.json exists| L[discoverWorkspacePackages]
L --> L1[Return ecosystem javascript and manifestPaths]
K -->|no JS workspace| M[Return ecosystem unknown and empty manifestPaths]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
traverseForWrapper, onlyENOENTis treated as a normal "not found" case while otherfs.accessSyncerrors (e.g.EACCESon a non-executable wrapper) cause an exception that aborts discovery; consider treating non-executable wrappers as "not found" and continuing traversal instead of throwing. - The Gradle init script is written to a deterministic path in the OS temp directory (
da-list-projects-${process.pid}.gradle); to avoid collisions or stale files when multiple runs share a PID (e.g. tests or forks), consider usingfs.mkdtemp/a random suffix or reusing a shared script embedded in the repo.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `traverseForWrapper`, only `ENOENT` is treated as a normal "not found" case while other `fs.accessSync` errors (e.g. `EACCES` on a non-executable wrapper) cause an exception that aborts discovery; consider treating non-executable wrappers as "not found" and continuing traversal instead of throwing.
- The Gradle init script is written to a deterministic path in the OS temp directory (`da-list-projects-${process.pid}.gradle`); to avoid collisions or stale files when multiple runs share a PID (e.g. tests or forks), consider using `fs.mkdtemp`/a random suffix or reusing a shared script embedded in the repo.
## Individual Comments
### Comment 1
<location path="src/workspace.js" line_range="439" />
<code_context>
+ manifestPaths.push(rootBuild)
+ }
+
+ const initScriptPath = path.join(os.tmpdir(), `da-list-projects-${process.pid}.gradle`)
+ try {
+ fs.writeFileSync(initScriptPath, GRADLE_INIT_SCRIPT)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using a predictable temp file path for the Gradle init script can cause collisions and subtle race conditions.
Because the path only uses the PID, multiple Gradle invocations from the same process (or reused PIDs) can overwrite or delete each other’s init scripts. Prefer using `fs.mkdtemp` to create a unique temp directory and place the init script inside it to avoid cross-process interference.
```suggestion
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'da-list-projects-'))
const initScriptPath = path.join(tmpDir, 'init.gradle')
```
</issue_to_address>
### Comment 2
<location path="src/workspace.js" line_range="561" />
<code_context>
+ const goBin = getCustomPath('go', opts)
+ let output
+ try {
+ output = invokeCommand(goBin, ['work', 'edit', '-json', goWork], { cwd: root })
+ } catch {
+ return []
</code_context>
<issue_to_address>
**issue (bug_risk):** The go work edit invocation likely passes the go.work path incorrectly, causing discovery to fail.
`go work edit -json` uses the current directory (or an explicit flag) to locate `go.work`, and treats positional arguments as edit directives (e.g. `use ./dir`), not as a filename. Passing `goWork` positionally will likely be ignored or misinterpreted, so discovery may always return an empty list. Consider running `go work edit -json` with `cwd: root` and no extra `go.work` argument, or use the proper flag to target a specific file.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| manifestPaths.push(rootBuild) | ||
| } | ||
|
|
||
| const initScriptPath = path.join(os.tmpdir(), `da-list-projects-${process.pid}.gradle`) |
There was a problem hiding this comment.
suggestion (bug_risk): Using a predictable temp file path for the Gradle init script can cause collisions and subtle race conditions.
Because the path only uses the PID, multiple Gradle invocations from the same process (or reused PIDs) can overwrite or delete each other’s init scripts. Prefer using fs.mkdtemp to create a unique temp directory and place the init script inside it to avoid cross-process interference.
| const initScriptPath = path.join(os.tmpdir(), `da-list-projects-${process.pid}.gradle`) | |
| const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'da-list-projects-')) | |
| const initScriptPath = path.join(tmpDir, 'init.gradle') |
| const goBin = getCustomPath('go', opts) | ||
| let output | ||
| try { | ||
| output = invokeCommand(goBin, ['work', 'edit', '-json', goWork], { cwd: root }) |
There was a problem hiding this comment.
issue (bug_risk): The go work edit invocation likely passes the go.work path incorrectly, causing discovery to fail.
go work edit -json uses the current directory (or an explicit flag) to locate go.work, and treats positional arguments as edit directives (e.g. use ./dir), not as a filename. Passing goWork positionally will likely be ignored or misinterpreted, so discovery may always return an empty list. Consider running go work edit -json with cwd: root and no extra go.work argument, or use the proper flag to target a specific file.
Summary
discoverGoWorkspaceModules()for discoveringgo.modmanifest paths in Go multi-module workspaces viago work edit -jsondetectWorkspaceManifests()(between Gradle and JavaScript in the detection order)discoverGoWorkspaceModulesfrom the public APITest plan
discoverGoWorkspaceModulessuite covering: no go.work, multi-module, nested dirs, single module, missing module dirs, command failure, invalid JSON, empty Use array, ignore pattern filteringJira
TC-4263
🤖 Generated with Claude Code
Summary by Sourcery
Add multi-ecosystem workspace discovery for Maven, Gradle, and Go workspaces and integrate them into workspace manifest detection.
New Features:
Enhancements:
Tests: