Skip to content

feat: support JS/TS monorepo workspace batch analysis#423

Merged
ruromero merged 6 commits intoguacsec:mainfrom
ruromero:feat/js-monorepos
Mar 24, 2026
Merged

feat: support JS/TS monorepo workspace batch analysis#423
ruromero merged 6 commits intoguacsec:mainfrom
ruromero:feat/js-monorepos

Conversation

@ruromero
Copy link
Copy Markdown
Collaborator

Summary

  • Add stackAnalysisBatch() API for workspace-level batch stack analysis
  • Discover JS/TS workspace packages from pnpm-workspace.yaml and package.json workspaces
  • Generate SBOMs in parallel with configurable concurrency and error handling (continue-on-error / fail-fast)
  • Propagate workspaceDir so sub-packages find the root lock file
  • Add CLI stack-batch command with --concurrency, --ignore, --metadata, --fail-fast options

Related

Implements TC-3862
Parent feature: TC-3767

Test plan

  • Integration tests for batch analysis with mocked providers and HTTP backend (6 tests)
  • Unit tests for batch option resolvers (6 tests)
  • Workspace discovery tests for pnpm and npm/yarn workspaces
  • Manual test with a real monorepo workspace

🤖 Generated with Claude Code

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add monorepo workspace batch analysis with concurrent SBOM generation

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add stackAnalysisBatch() API for workspace-level batch stack analysis
  - Discovers JS/TS workspace packages from pnpm-workspace.yaml and package.json workspaces
  - Discovers Cargo workspace crates via cargo metadata
  - Generates SBOMs in parallel with configurable concurrency (default 10)
• Support workspaceDir option to locate lock files at workspace root
  - Enables monorepo analysis where lock file is separate from manifest
  - Propagates through provider matching and validation
• Add CLI stack-batch command with options for concurrency, discovery ignores, metadata, and
  fail-fast modes
• Implement robust workspace discovery with negation pattern handling and configurable ignore globs
Diagram
flowchart LR
  A["Workspace Root"] -->|detect ecosystem| B["Cargo or JS/TS"]
  B -->|discover manifests| C["Manifest Paths"]
  C -->|validate JS packages| D["Valid Manifests"]
  D -->|generate SBOMs| E["SBOM Map"]
  E -->|batch request| F["Backend Analysis"]
  F -->|optional metadata| G["Analysis + Metadata"]
Loading

Grey Divider

File Changes

1. src/analysis.js ✨ Enhancement +49/-1

Add requestStackBatch function for batch analysis

src/analysis.js


2. src/batch_opts.js ✨ Enhancement +37/-0

New module for batch option resolvers

src/batch_opts.js


3. src/cli.js ✨ Enhancement +120/-4

Add stack-batch command and workspaceDir option

src/cli.js


View more (12)
4. src/index.js ✨ Enhancement +311/-4

Add stackAnalysisBatch function and workspace utilities

src/index.js


5. src/provider.js ✨ Enhancement +5/-4

Update match function to accept workspaceDir option

src/provider.js


6. src/providers/base_javascript.js ✨ Enhancement +10/-5

Support workspaceDir for lock file location

src/providers/base_javascript.js


7. src/providers/rust_cargo.js ✨ Enhancement +11/-2

Support workspaceDir for Cargo.lock location

src/providers/rust_cargo.js


8. src/workspace.js ✨ Enhancement +271/-0

New workspace discovery and validation module

src/workspace.js


9. test/batch_opts.test.js 🧪 Tests +51/-0

Unit tests for batch option resolvers

test/batch_opts.test.js


10. test/providers/javascript.test.js 🧪 Tests +15/-0

Add tests for workspaceDir option matching

test/providers/javascript.test.js


11. test/providers/rust_cargo.test.js 🧪 Tests +17/-0

Add tests for workspaceDir option matching

test/providers/rust_cargo.test.js


12. test/providers/workspace.test.js 🧪 Tests +189/-0

Integration tests for workspace discovery

test/providers/workspace.test.js


13. test/stack_analysis_batch.test.js 🧪 Tests +289/-0

Integration tests for batch analysis with mocked providers

test/stack_analysis_batch.test.js


14. README.md 📝 Documentation +54/-2

Document monorepo support and batch analysis API

README.md


15. package.json Dependencies +4/-0

Add dependencies for workspace discovery

package.json


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Mar 23, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 📐 Spec deviations (0)

Grey Divider


Action required

1. Async provideStack not awaited🐞 Bug ✓ Correctness
Description
stackAnalysisBatch generates SBOMs by calling provider.provideStack() synchronously and
JSON.parse()ing provided.content, which breaks providers whose provideStack returns a Promise (e.g.,
python_pip), causing batch analysis to throw and produce no results.
Code

src/index.js[R287-295]

+function generateOneSbom(manifestPath, workspaceOpts) {
+	const provider = match(manifestPath, availableProviders, workspaceOpts)
+	const provided = provider.provideStack(manifestPath, workspaceOpts)
+	const sbom = JSON.parse(provided.content)
+	const purl = sbom?.metadata?.component?.purl || sbom?.metadata?.component?.['bom-ref']
+	if (!purl) {
+		return { ok: false, manifestPath, reason: 'missing purl in SBOM' }
+	}
+	return { ok: true, purl, sbom }
Evidence
In the new batch flow, generateOneSbom() does not await provideStack and immediately parses
provided.content. The Provider typedef explicitly allows provideStack to return a Promise, and the
python provider is implemented as async, so provided will be a Promise and provided.content will be
undefined (or JSON.parse will fail).

src/index.js[279-296]
src/provider.js[13-29]
src/providers/python_pip.js[48-60]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`stackAnalysisBatch()` SBOM generation assumes `provider.provideStack()` is synchronous. Some providers (e.g. python pip) implement it as `async`, so batch analysis crashes when it tries to `JSON.parse(provided.content)`.
### Issue Context
The Provider typedef allows `provideStack` to return `Provided | Promise<Provided>`. Batch path must support both.
### Fix Focus Areas
- src/index.js[279-413]
### Implementation notes
- Make `generateOneSbom` `async` and `await provider.provideStack(...)`.
- Update `generateSboms` to await `generateOneSbom` in both fail-fast and continue-on-error branches.
- Ensure the try/catch in the concurrency branch still catches async rejections (wrap `await generateOneSbom` inside the try).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. JS monorepo commands run in wrong dir🐞 Bug ⛯ Reliability
Description
Passing workspaceDir enables lockfile validation at the workspace root, but JavaScript providers
still execute npm/pnpm/yarn with cwd defaulting to the package’s manifest directory, which can fail
to resolve workspace root context/lockfiles in real monorepos.
Code

src/providers/base_javascript.js[R122-127]

+	validateLockFile(manifestDir, opts = {}) {
+		const workspaceDir = getCustom('TRUSTIFY_DA_WORKSPACE_DIR', null, opts) ?? opts.workspaceDir
+		const dirToCheck = workspaceDir ? path.resolve(workspaceDir) : manifestDir
+		const lock = path.join(dirToCheck, this._lockFileName())
+		return fs.existsSync(lock)
}
Evidence
validateLockFile now checks the lock at opts.workspaceDir, but Base_javascript’s dependency-tree
build and command invocation default cwd to the manifest directory and do not use workspaceDir.
stackAnalysisBatch forces workspaceDir=root for all subpackages, so lock validation succeeds but
command execution may still run in the wrong directory for workspace-aware package managers.

src/providers/base_javascript.js[114-127]
src/providers/base_javascript.js[187-195]
src/providers/base_javascript.js[355-360]
src/index.js[443-481]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
JS monorepo support currently updates only lockfile *detection*. The actual npm/pnpm/yarn command execution still runs with `cwd` defaulting to the package directory, which can break workspace installs/listing when the lockfile is at the workspace root.
### Issue Context
`stackAnalysisBatch()` sets `{ ...opts, workspaceDir: root }` and then calls `provider.provideStack(manifestPath, workspaceOpts)` for each package. The JS providers validate the lock in `workspaceDir`, but `_buildDependencyTree()` / command invocation still use the manifest directory.
### Fix Focus Areas
- src/providers/base_javascript.js[182-215]
- src/providers/base_javascript.js[327-416]
### Implementation notes
- Decide on desired behavior:
- If `workspaceDir` is set, run package-manager commands with `cwd = workspaceDir` (workspace root) and/or pass appropriate flags (e.g., `--workspace-root` where supported).
- Ensure lockfile creation/update uses workspace root when in workspace mode.
- Add/extend tests covering pnpm/yarn workspace where lockfile only exists at root and commands must run from root.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. CLI prints object for HTML+metadata🐞 Bug ✓ Correctness
Description
The stack-batch CLI prints res directly when --html is used; if --metadata is also set, the
API returns an object {analysis, metadata} and the CLI prints an object instead of the HTML
string.
Code

src/cli.js[R252-284]

+		let res = await client.stackAnalysisBatch(workspaceRoot, html, opts)
+		const batchAnalysis =
+			res && typeof res === 'object' && res != null && 'analysis' in res ? res.analysis : res
+		if (summary && !html && typeof batchAnalysis === 'object') {
+			const summaries = {}
+			for (const [purl, report] of Object.entries(batchAnalysis)) {
+				if (report?.providers) {
+					for (const provider of Object.keys(report.providers)) {
+						const sources = report.providers[provider]?.sources
+						if (sources) {
+							for (const [source, data] of Object.entries(sources)) {
+								if (data?.summary) {
+									if (!summaries[purl]) {
+										summaries[purl] = {}
+									}
+									if (!summaries[purl][provider]) {
+										summaries[purl][provider] = {}
+									}
+									summaries[purl][provider][source] = data.summary
+								}
+							}
+						}
+					}
+				}
+			}
+			if (res && typeof res === 'object' && res != null && 'metadata' in res) {
+				res = { analysis: summaries, metadata: res.metadata }
+			} else {
+				res = summaries
+			}
+		}
+		console.log(html ? res : JSON.stringify(res, null, 2))
+	}
Evidence
stackAnalysisBatch returns { analysis: analysisResult, metadata } when batchMetadata is true,
regardless of the html flag. The CLI uses console.log(html ? res : JSON.stringify(res, null, 2)),
so with --html --metadata it logs the wrapper object instead of res.analysis.

src/cli.js[252-284]
src/index.js[495-503]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When `trustify-da-javascript-client stack-batch --html --metadata` is used, output should be HTML (or a well-defined JSON wrapper), but the CLI currently prints the wrapper object.
### Fix Focus Areas
- src/cli.js[252-284]
### Implementation notes
- If `html` is true and `res` has an `analysis` field, print `res.analysis` (HTML string).
- Alternatively, disallow combining `--html` with `--metadata` via yargs `conflicts`, but that changes UX; printing `analysis` is likely preferable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@ruromero ruromero requested a review from Strum355 March 23, 2026 15:30
ruromero and others added 3 commits March 23, 2026 18:08
Signed-off-by: Ruben Romero Montes <rromerom@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Implements TC-3862
…aintainability

- Replace hand-rolled pnpm-workspace.yaml parser with js-yaml
- Fix negation pattern handling in workspace discovery (e.g. !**/test/**)
- Refactor stackAnalysisBatch into focused helpers, eliminating duplicated
  SBOM generation logic between fail-fast and continue-on-error paths
- Add integration tests for stackAnalysisBatch with mocked providers and
  HTTP backend

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements TC-3862
- Make generateOneSbom async and await provider.provideStack() to support
  async providers (e.g. python_pip)
- Propagate workspaceDir as cwd for package manager commands so npm/pnpm/yarn
  run from workspace root in monorepos
- Fix CLI --html --metadata printing wrapper object instead of HTML string

Implements TC-3862

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ruromero ruromero force-pushed the feat/js-monorepos branch from efc1054 to 01b45a5 Compare March 23, 2026 17:10
ruromero and others added 3 commits March 23, 2026 21:25
Remove the opts.workspaceDir fallback pattern and use only the
TRUSTIFY_DA_WORKSPACE_DIR key through getCustom(), keeping the
existing single-convention pattern for option propagation.

TC-3862

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use named import for js-yaml load function and fix import ordering
in stack_analysis_batch.test.js.

TC-3862

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ruromero ruromero merged commit 2ea1d77 into guacsec:main Mar 24, 2026
4 checks passed
@ruromero ruromero deleted the feat/js-monorepos branch March 24, 2026 13:05
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