feat(workspace): add uv workspace (pyproject.toml) discovery#496
feat(workspace): add uv workspace (pyproject.toml) discovery#496Strum355 wants to merge 4 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>
Add discoverUvWorkspaceMembers() for discovering workspace members in uv/Python monorepos. Parses pyproject.toml for [tool.uv.workspace] member globs and resolves them via fast-glob. Requires both pyproject.toml with workspace config and uv.lock to be present. Handles virtual workspaces (no [project] section in root) by excluding the root pyproject.toml from discovered manifests. Respects uv's exclude patterns and the standard workspace discovery ignore mechanism. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reviewer's GuideAdds first-class multi-ecosystem workspace discovery (Maven, Gradle, Go, uv/pyproject) to the workspace analysis pipeline, including CLI wrapper resolution, TOML parsing via smol-toml, enhanced default ignore patterns, and comprehensive tests/fixtures for the new discovery paths and error/ignore handling. Class diagram for new workspace discovery utilities and exportsclassDiagram
class WorkspaceModule {
<<module>>
+discoverMavenModules(workspaceRoot, opts) Promise~string[]~
+discoverGradleSubprojects(workspaceRoot, opts) Promise~string[]~
+discoverGoWorkspaceModules(workspaceRoot, opts) Promise~string[]~
+discoverUvWorkspaceMembers(workspaceRoot, opts) Promise~string[]~
+discoverWorkspaceCrates(workspaceRoot, opts) Promise~string[]~
+discoverWorkspacePackages(workspaceRoot, opts) Promise~string[]~
+resolveMavenBinary(startDir, opts) string
+resolveGradleBinary(startDir, opts) string
+traverseForWrapper(startDir, wrapperName, repoRoot) string
+listMavenModules(dir, mvnBin) string[]
+collectMavenModules(dir, mvnBin, visited, manifestPaths) void
+parseMavenModuleList(raw) string[]
+parseGradleInitScriptOutput(raw) ProjectInfo[]
+hasProjectMetadata(pyprojectPath) boolean
+resolveWorkspaceDiscoveryIgnore(opts) string[]
+buildWorkspaceDiscoveryGlobOptions(root, ignorePatterns) GlobOptions
+filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns) string[]
+toManifestGlobPatterns(memberPatterns, manifestFileName) string[]
}
class ToolsModule {
<<module>>
+getCustom(toolName, opts) string
+getCustomPath(toolName, opts) string
+getGitRootDir(startDir) string
+getWrapperPreference(toolName, opts) boolean
+invokeCommand(binary, args, options) Buffer
}
class IndexModule {
<<module>>
+detectWorkspaceManifests(root, opts) DetectionResult
+stackAnalysisBatch(workspaceRoot, html, opts) Promise~AnalysisResult[]~
+discoverMavenModules(workspaceRoot, opts) Promise~string[]~
+discoverGradleSubprojects(workspaceRoot, opts) Promise~string[]~
+discoverGoWorkspaceModules(workspaceRoot, opts) Promise~string[]~
+discoverUvWorkspaceMembers(workspaceRoot, opts) Promise~string[]~
}
class DetectionResult {
+ecosystem : string
+manifestPaths : string[]
}
class ProjectInfo {
+path : string
+dir : string
}
class GlobOptions {
+cwd : string
+ignore : string[]
+absolute : boolean
}
class AnalysisResult {
+workspaceRoot : string
+ecosystem : string
+total : number
+successful : number
+failed : number
}
WorkspaceModule ..> ToolsModule : uses
IndexModule ..> WorkspaceModule : calls
IndexModule ..> DetectionResult : returns
WorkspaceModule ..> GlobOptions : constructs
WorkspaceModule ..> ProjectInfo : returns
IndexModule ..> AnalysisResult : returns
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 1 issue, and left some high level feedback:
- In
discoverGradleSubprojects, the init script is written to a fixed path inos.tmpdir()keyed only byprocess.pid, which can collide if multiple calls run in the same process (or overlap across threads); consider usingfs.mkdtempor a random suffix per call and cleaning up that directory. - In
discoverUvWorkspaceMembers,hasProjectMetadatareparses the rootpyproject.tomlthat was already parsed earlier; you could pass the parsed object into this check (or inline the logic) to avoid a second read/parse and keep behavior centralized.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `discoverGradleSubprojects`, the init script is written to a fixed path in `os.tmpdir()` keyed only by `process.pid`, which can collide if multiple calls run in the same process (or overlap across threads); consider using `fs.mkdtemp` or a random suffix per call and cleaning up that directory.
- In `discoverUvWorkspaceMembers`, `hasProjectMetadata` reparses the root `pyproject.toml` that was already parsed earlier; you could pass the parsed object into this check (or inline the logic) to avoid a second read/parse and keep behavior centralized.
## Individual Comments
### Comment 1
<location path="src/workspace.js" line_range="442" />
<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):** Temp init script name based only on PID can cause collisions within the same process.
Because the filename only varies by PID, all Gradle detections in the same Node.js process will share this temp file. Concurrent or overlapping calls can clobber each other’s init scripts or delete the file while another call still needs it. Consider adding extra uniqueness (e.g. a counter, timestamp, `crypto.randomUUID()`) or using a temp-file helper to avoid intra-process races.
```suggestion
const initScriptPath = path.join(
os.tmpdir(),
`da-list-projects-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.gradle`
)
```
</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): Temp init script name based only on PID can cause collisions within the same process.
Because the filename only varies by PID, all Gradle detections in the same Node.js process will share this temp file. Concurrent or overlapping calls can clobber each other’s init scripts or delete the file while another call still needs it. Consider adding extra uniqueness (e.g. a counter, timestamp, crypto.randomUUID()) or using a temp-file helper to avoid intra-process races.
| const initScriptPath = path.join(os.tmpdir(), `da-list-projects-${process.pid}.gradle`) | |
| const initScriptPath = path.join( | |
| os.tmpdir(), | |
| `da-list-projects-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.gradle` | |
| ) |
Summary
discoverUvWorkspaceMembers()for discovering workspace members in uv/Python monorepospyproject.tomlfor[tool.uv.workspace]member globs and resolves viafast-glob— nouvCLI needed[project]in root → exclude root) vs root-package workspaces (include root)excludepatterns andTRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE__pycache__and.venvto default ignore patternsTest plan
Jira
TC-4265
🤖 Generated with Claude Code
Summary by Sourcery
Extend workspace manifest discovery to support additional ecosystems and uv-based Python monorepos.
New Features:
Enhancements:
Tests: