Skip to content

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

Merged
Strum355 merged 3 commits into
guacsec:mainfrom
Strum355:TC-4263
May 7, 2026
Merged

feat(workspace): add Go workspace (go.work) discovery#495
Strum355 merged 3 commits into
guacsec:mainfrom
Strum355:TC-4263

Conversation

@Strum355

@Strum355 Strum355 commented Apr 28, 2026

Copy link
Copy Markdown
Member

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.

@sourcery-ai

sourcery-ai Bot commented Apr 28, 2026

Copy link
Copy Markdown

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

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 Outdated
Comment thread src/workspace.js Outdated
@Strum355

Strum355 commented May 5, 2026

Copy link
Copy Markdown
Member Author

Verification Report -- TC-4263

1. Review Feedback

Thread File Reviewer Classification Status
Gradle temp file collision src/workspace.js:439 sourcery-ai[bot] out-of-scope Targets Gradle code from a prior PR, not Go workspace changes
go work edit positional arg src/workspace.js:561 sourcery-ai[bot] incorrect go work edit [flags] [go.work] accepts a positional filepath per Go docs

Overall review feedback assessment: Both Sourcery comments are either out-of-scope or factually incorrect. No human reviewer comments. No blocking feedback.

2. Scope Containment

Changed files (13):

Category Files
Production code src/index.js, src/providers/golang_gomodules.js
Test code test/providers/workspace.test.js
Test fixtures 10 fixture files under test/providers/tst_manifests/golang/go_workspace*

Verdict: PASS -- All changes are directly related to Go workspace discovery. Minor whitespace fixes in src/index.js (indentation correction on lines 114-117) are cosmetic and harmless.

3. Commit Traceability

SHA Message Conventional?
5a81505 feat(workspace): add Go workspace (go.work) discovery Yes
41fa1a5 refactor: move Go discovery logic from workspace.js to golang_gomodules.js Yes

Verdict: PASS -- 2 commits, both use conventional commit format, logically separated (feature then refactor to proper location).

4. Sensitive Patterns

Scanned diff for passwords, secrets, tokens, API keys, credentials, private keys, and auth headers.

Verdict: PASS -- No sensitive patterns detected.

5. CI Status

Check Status
Lint and test project (22) PASS
Lint and test project (24) PASS
Validate PR title PASS
Validate commit messages PASS
Sourcery review PASS

Verdict: PASS -- All 5 CI checks passed.

6. Acceptance Criteria

# Criterion Status Evidence
1 stackAnalysisBatch("/path/to/go-workspace") discovers all workspace member go.mod files PASS detectWorkspaceManifests() in src/index.js:336-341 checks for go.work and calls discoverGoWorkspaceModules()
2 Discovery uses go work edit -json for structured parsing PASS src/providers/golang_gomodules.js:451 invokes invokeCommand(goBin, ['work', 'edit', '-json', goWork])
3 Relative paths in Use entries correctly resolved to absolute paths PASS src/providers/golang_gomodules.js:474 uses path.resolve(root, diskPath)
4 Members pointing to non-existent directories skipped gracefully PASS src/providers/golang_gomodules.js:476 checks fs.existsSync(goMod) before pushing; test "skips modules whose directory does not exist" confirms
5 TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE patterns are respected PASS src/providers/golang_gomodules.js:481-482 applies resolveWorkspaceDiscoveryIgnore + filterManifestPathsByDiscoveryIgnore; test "applies ignore patterns" confirms
6 If go is not available, returns [] PASS src/providers/golang_gomodules.js:452-453 catches invokeCommand errors and returns []; test "returns empty when go command fails" confirms

Verdict: PASS -- All 6 acceptance criteria met.

7. Test Quality

9 new tests in discoverGoWorkspaceModules suite:

Test Coverage
No go.work at root Absence path
Two modules Happy path with multi-module
Nested directories Subdirectory resolution
Single module Minimal workspace
Missing module dirs Graceful skip
Command failure Error handling
Invalid JSON Parse error handling
Empty Use array Edge case
Ignore patterns Filter integration

Test results: 21/21 workspace tests pass (12 pre-existing + 9 new).

Missing from Jira spec: The Jira description requested a test for parent-relative paths (use ../sibling) which is not covered. However, path.resolve(root, diskPath) handles ../ correctly by design, and the nested directory test validates relative path resolution. This is a minor gap.

Verdict: PASS -- Comprehensive coverage of happy paths, error cases, edge cases, and filter integration.

8. Overall Verdict

PASS -- PR is ready to merge.

  • All 6 acceptance criteria are satisfied
  • All CI checks pass
  • All 21 workspace tests pass (9 new Go workspace tests)
  • No blocking review feedback
  • Clean scope containment
  • No sensitive patterns
  • Conventional commits with proper traceability

Minor note: The Jira spec mentions a test for parent-relative paths (use ../sibling) which is not explicitly covered, but the underlying path.resolve() handles this correctly by design.


This comment was AI-generated by sdlc-workflow/verify-pr v0.5.11.

Strum355 and others added 3 commits May 6, 2026 16:24
…es.js

Move discoverGoWorkspaceModules to its provider file so workspace.js only
retains generic scaffolding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace esmock invokeCommand mocks with real `go work edit -json`
invocations for happy-path tests. Keep esmock only for error paths
that cannot be reproduced with a real binary (command failure,
invalid JSON output). Add go_workspace_empty fixture for the
null-Use case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Strum355
Strum355 enabled auto-merge (squash) May 6, 2026 15:30
@Strum355
Strum355 merged commit 848421d into guacsec:main May 7, 2026
5 checks passed
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.

2 participants