Skip to content

feat(workspace): add Go workspace (go.work) discovery#495

Open
Strum355 wants to merge 3 commits intoguacsec:mainfrom
Strum355:TC-4263
Open

feat(workspace): add Go workspace (go.work) discovery#495
Strum355 wants to merge 3 commits intoguacsec:mainfrom
Strum355:TC-4263

Conversation

@Strum355
Copy link
Copy Markdown
Member

@Strum355 Strum355 commented Apr 28, 2026

Summary

  • Add discoverGoWorkspaceModules() for discovering go.mod manifest paths in Go multi-module workspaces via go work edit -json
  • Integrate Go workspace detection into detectWorkspaceManifests() (between Gradle and JavaScript in the detection order)
  • Re-export discoverGoWorkspaceModules from the public API

Test plan

  • 9 new tests in discoverGoWorkspaceModules suite covering: no go.work, multi-module, nested dirs, single module, missing module dirs, command failure, invalid JSON, empty Use array, ignore pattern filtering
  • All 34 workspace tests pass
  • ESLint clean

Jira

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:

  • Add discovery of Maven multi-module pom.xml manifests with wrapper-aware Maven binary resolution.
  • Add discovery of Gradle multi-project build manifests with wrapper-aware Gradle binary resolution and init-script based project listing.
  • Add discovery of Go multi-module workspaces via go.work to enumerate go.mod manifests.
  • Expose Maven, Gradle, and Go workspace discovery helpers through the public API.

Enhancements:

  • Extend default workspace discovery ignore patterns to skip common build output directories and metadata folders.
  • Broaden workspace manifest detection to recognize Maven, Gradle, and Go workspace roots alongside existing Cargo and JavaScript detection.
  • Improve the no-manifest error message in batch analysis to mention all supported workspace types.

Tests:

  • Add comprehensive unit tests and test fixtures for Maven, Gradle, and Go workspace discovery covering multi-module layouts, nested structures, failures, and ignore pattern filtering.

Strum355 and others added 3 commits April 28, 2026 11:40
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>
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 28, 2026

Reviewer's Guide

Adds 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 -json

sequenceDiagram
    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
Loading

Flow diagram for updated detectWorkspaceManifests ecosystem selection

flowchart 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]
Loading

File-Level Changes

Change Details Files
Implement Maven multi-module discovery with wrapper support and integrate with workspace ignore filtering.
  • Add resolveMavenBinary to prefer mvnw wrapper based on getWrapperPreference and fall back to getCustomPath
  • Introduce traverseForWrapper utility to walk up to git root or filesystem root when searching for a wrapper
  • Implement discoverMavenModules and recursive collectMavenModules using mvn help:evaluate to read project.modules
  • Parse Maven module list output via parseMavenModuleList and filter manifest paths through resolveWorkspaceDiscoveryIgnore and filterManifestPathsByDiscoveryIgnore
  • Add Maven multi-module, nested aggregator, no-modules, failure, and ignore-pattern tests plus minimal pom.xml fixtures
src/workspace.js
test/providers/workspace.test.js
test/providers/tst_manifests/maven/**
Implement Gradle multi-project discovery using an init script, with wrapper support and ignore filtering.
  • Define GRADLE_INIT_SCRIPT and resolveGradleBinary to support gradlew preference via getWrapperPreference and traverseForWrapper
  • Implement discoverGradleSubprojects to detect settings.gradle[.kts], run gradle with the init script, and collect build.gradle[.kts] manifests
  • Handle command failure by falling back to just the root build file and always applying workspace discovery ignore patterns
  • Add parseGradleInitScriptOutput to read structured ::DA_PROJECT:: lines from Gradle output
  • Add Gradle test fixtures and tests covering multi-project, nested subprojects, mixed Groovy/Kotlin builds, no subprojects, failures, and ignore patterns
src/workspace.js
test/providers/workspace.test.js
test/providers/tst_manifests/gradle/**
Add Go workspace (go.work) discovery that enumerates go.mod files from go work edit -json.
  • Implement discoverGoWorkspaceModules that requires go.work at the root, invokes go work edit -json via getCustomPath('go'), and parses the Use entries
  • Resolve each Use.DiskPath to a module directory, require an existing go.mod, and accumulate manifest paths
  • Gracefully handle missing go.work, command failure, invalid JSON, empty Use arrays, and apply workspace discovery ignore patterns
  • Add go workspace fixtures (multi-module, nested, single, missing module) and tests for happy paths, error cases, and ignore filtering
src/workspace.js
test/providers/workspace.test.js
test/providers/tst_manifests/golang/**
Extend workspace detection to support Maven, Gradle, and Go workspaces and expose new helpers in the public API.
  • Update detectWorkspaceManifests to check Maven (pom.xml), then Gradle (settings.gradle[.kts]), then Go (go.work), before JavaScript lockfiles
  • Return new ecosystem tags 'maven', 'gradle', and 'gomodules' alongside existing ecosystems
  • Broaden the stackAnalysisBatch error message to mention all supported workspace root types
  • Re-export discoverGoWorkspaceModules, discoverGradleSubprojects, and discoverMavenModules from src/index.js
src/index.js
Tighten and extend default ignore patterns for workspace discovery.
  • Expand DEFAULT_WORKSPACE_DISCOVERY_IGNORE to also skip target, build, and .gradle directories by default
  • Ensure new discovery functions (Maven, Gradle, Go) reuse resolveWorkspaceDiscoveryIgnore and filterManifestPathsByDiscoveryIgnore for consistent behavior
src/workspace.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/workspace.js
manifestPaths.push(rootBuild)
}

const initScriptPath = path.join(os.tmpdir(), `da-list-projects-${process.pid}.gradle`)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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')

Comment thread src/workspace.js
const goBin = getCustomPath('go', opts)
let output
try {
output = invokeCommand(goBin, ['work', 'edit', '-json', goWork], { cwd: root })
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant