Skip to content

Commit 5a81505

Browse files
committed
feat(workspace): add Go workspace (go.work) discovery
1 parent 58200d2 commit 5a81505

13 files changed

Lines changed: 260 additions & 2 deletions

File tree

src/index.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import fs from 'node:fs'
88
import { getCustom } from "./tools.js";
99
import { resolveBatchMetadata, resolveContinueOnError } from './batch_opts.js'
1010
import {
11+
discoverGoWorkspaceModules,
1112
discoverWorkspaceCrates,
1213
discoverWorkspacePackages,
1314
filterManifestPathsByDiscoveryIgnore,
@@ -23,6 +24,7 @@ export { getProjectLicense, findLicenseFilePath, identifyLicense, getLicenseDeta
2324

2425
export default { componentAnalysis, stackAnalysis, stackAnalysisBatch, imageAnalysis, validateToken, generateSbom }
2526
export {
27+
discoverGoWorkspaceModules,
2628
discoverWorkspacePackages,
2729
discoverWorkspaceCrates,
2830
validatePackageJson,
@@ -319,7 +321,7 @@ async function generateOneSbom(manifestPath, workspaceOpts) {
319321
*
320322
* @param {string} root - Resolved workspace root
321323
* @param {Options} opts
322-
* @returns {Promise<{ ecosystem: 'javascript' | 'cargo' | 'unknown', manifestPaths: string[] }>}
324+
* @returns {Promise<{ ecosystem: 'javascript' | 'cargo' | 'gomodules' | 'unknown', manifestPaths: string[] }>}
323325
* @private
324326
*/
325327
async function detectWorkspaceManifests(root, opts) {
@@ -331,6 +333,13 @@ async function detectWorkspaceManifests(root, opts) {
331333
return { ecosystem: 'cargo', manifestPaths: await discoverWorkspaceCrates(root, opts) }
332334
}
333335

336+
if (fs.existsSync(path.join(root, 'go.work'))) {
337+
const manifestPaths = await discoverGoWorkspaceModules(root, opts)
338+
if (manifestPaths.length > 0) {
339+
return { ecosystem: 'gomodules', manifestPaths }
340+
}
341+
}
342+
334343
const hasJsLock = fs.existsSync(path.join(root, 'pnpm-lock.yaml'))
335344
|| fs.existsSync(path.join(root, 'yarn.lock'))
336345
|| fs.existsSync(path.join(root, 'package-lock.json'))
@@ -489,7 +498,7 @@ async function stackAnalysisBatch(workspaceRoot, html = false, opts = {}) {
489498
}
490499

491500
if (manifestPaths.length === 0) {
492-
throw new Error(`No workspace manifests found at ${root}. Ensure Cargo.toml+Cargo.lock or package.json+lock file exist.`)
501+
throw new Error(`No workspace manifests found at ${root}. Ensure a supported workspace root exists (Cargo.toml+Cargo.lock, go.work, or package.json+lock file).`)
493502
}
494503

495504
const workspaceOpts = { ...opts, TRUSTIFY_DA_WORKSPACE_DIR: root }

src/workspace.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,56 @@ export async function discoverWorkspaceCrates(workspaceRoot, opts = {}) {
269269
const ignorePatterns = resolveWorkspaceDiscoveryIgnore(opts)
270270
return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
271271
}
272+
273+
/**
274+
* Discover all go.mod manifest paths in a Go workspace.
275+
* Uses `go work edit -json` to get workspace members.
276+
*
277+
* @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain go.work)
278+
* @param {import('./index.js').Options} [opts={}]
279+
* @returns {Promise<string[]>} Paths to go.mod files (absolute)
280+
*/
281+
export async function discoverGoWorkspaceModules(workspaceRoot, opts = {}) {
282+
const root = path.resolve(workspaceRoot)
283+
const goWork = path.join(root, 'go.work')
284+
285+
if (!fs.existsSync(goWork)) {
286+
return []
287+
}
288+
289+
const goBin = getCustomPath('go', opts)
290+
let output
291+
try {
292+
output = invokeCommand(goBin, ['work', 'edit', '-json', goWork], { cwd: root })
293+
} catch {
294+
return []
295+
}
296+
297+
let workspace
298+
try {
299+
workspace = JSON.parse(output.toString().trim())
300+
} catch {
301+
return []
302+
}
303+
304+
const useEntries = workspace.Use || []
305+
if (useEntries.length === 0) {
306+
return []
307+
}
308+
309+
const manifestPaths = []
310+
for (const entry of useEntries) {
311+
const diskPath = entry.DiskPath
312+
if (!diskPath) {
313+
continue
314+
}
315+
const moduleDir = path.resolve(root, diskPath)
316+
const goMod = path.join(moduleDir, 'go.mod')
317+
if (fs.existsSync(goMod)) {
318+
manifestPaths.push(goMod)
319+
}
320+
}
321+
322+
const ignorePatterns = resolveWorkspaceDiscoveryIgnore(opts)
323+
return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
324+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
go 1.22
2+
3+
use (
4+
./module-a
5+
./module-b
6+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module example.com/module-a
2+
3+
go 1.22
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module example.com/module-b
2+
3+
go 1.22
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module example.com/existing
2+
3+
go 1.22
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
go 1.22
2+
3+
use (
4+
./existing
5+
./nonexistent
6+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
go 1.22
2+
3+
use (
4+
./libs/core
5+
./libs/util
6+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module example.com/libs/core
2+
3+
go 1.22
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module example.com/libs/util
2+
3+
go 1.22

0 commit comments

Comments
 (0)