Skip to content

Commit d4e17c7

Browse files
committed
feat(bazel): add bounded verbose diagnostics
1 parent d463160 commit d4e17c7

7 files changed

Lines changed: 134 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
99
### Added
1010
- **`socket manifest bazel [beta]`** — Generate Bazel JVM SBOM manifests by running `bazel query` against discovered Maven repos in a Bazel workspace. Closes the inline-Maven-declaration gap that lockfile-only parsing misses for repos like envoy, ray, tensorflow, tink-java, and or-tools. Auto-detects Bzlmod and legacy `WORKSPACE`.
1111
- **`socket scan create --auto-manifest`** now covers Bazel workspaces in addition to Gradle/Scala/Kotlin/Conda. Repos with `MODULE.bazel`, `WORKSPACE`, or `WORKSPACE.bazel` are detected automatically and their Maven dependencies extracted as part of the standard scan-create flow.
12-
- **Bazel PyPI extraction**`socket manifest bazel` now generates `requirements.txt` for Python Bazel workspaces via the new repeatable `--ecosystem pypi` flag, or via auto-detection when no `--ecosystem` flag is supplied. Discovers custom `rules_python` pip hub names, queries `py_library` / `py_binary` / `py_test` dependencies, resolves canonical pinned versions from `requirements_lock.txt`, and emits PEP 503-normalized `name==version` lines. Supports both Bzlmod (`pip.parse`) and legacy `WORKSPACE` (`pip_parse` / `pip_install`) configurations. `socket scan create --auto-manifest` picks up the generated PyPI manifest alongside Maven.
12+
- **Bazel PyPI extraction**`socket manifest bazel --ecosystem pypi` now generates `requirements.txt` for Python Bazel workspaces. Discovers custom `rules_python` pip hub names with Bazel command output first, queries `py_library` / `py_binary` / `py_test` dependencies, resolves canonical pinned versions from `requirements_lock.txt`, and emits PEP 503-normalized `name==version` lines. Supports both Bzlmod (`pip.parse`) and legacy `WORKSPACE` (`pip_parse` / `pip_install`) configurations. PyPI remains explicit opt-in for `socket scan create --auto-manifest` until real-world no-lockfile recovery is validated.
13+
14+
### Changed
15+
- **Bazel diagnostics**`socket manifest bazel --verbose` now emits bounded subprocess traces with argv, cwd, duration, exit status, output sizes, and failure stderr tails to make customer log-only triage safer and faster.
1316

1417
## [1.1.100](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.100) - 2026-05-21
1518

src/commands/manifest/README.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,20 @@ socket manifest bazel [options] [DIR=.]
3737
- `--bazel-rc <path>` — path to additional `.bazelrc` fragments forwarded to bazel.
3838
- `--bazel-flags <str>` — flags forwarded to every bazel invocation (single quoted string).
3939
- `--bazel-output-base <dir>` — Bazel `--output_base` for read-only-cache CI environments.
40-
- `--ecosystem <name>` — ecosystem(s) to extract; repeatable. Supported values: `maven`, `pypi`. When omitted, every detected supported ecosystem is generated automatically.
40+
- `--ecosystem <name>` — ecosystem(s) to extract; repeatable. Supported values: `maven`, `pypi`. When omitted, Maven is generated by default; PyPI is explicit opt-in.
4141
- `--out <dir>` — output directory; default `./.socket/bazel-manifests/`.
4242
- `--dry-run`, `--verbose` — standard diagnostic flags.
4343

4444
> **Upload**: This subcommand only generates manifests. To generate and
4545
> upload in one step, use `socket scan create --auto-manifest .` — it
46-
> detects the workspace, runs the same extraction this subcommand performs,
47-
> and uploads the result.
46+
> detects the workspace, generates Bazel Maven manifests by default, and
47+
> uploads the result. Bazel PyPI auto-manifest generation requires an explicit
48+
> `defaults.manifest.bazel.ecosystem` config value that includes `pypi`.
4849
4950
### Examples
5051

5152
```bash
52-
# Auto-detect and generate every supported ecosystem from the current
53-
# Bazel workspace (Maven and/or PyPI).
53+
# Generate the default Bazel Maven manifest from the current workspace.
5454
socket manifest bazel .
5555

5656
# Generate only the PyPI manifest.
@@ -65,10 +65,9 @@ socket manifest bazel --bazel=/usr/local/bin/bazelisk .
6565

6666
### Python/PyPI Extraction
6767

68-
When `--ecosystem pypi` is selected (or PyPI rules are auto-detected), the
69-
command:
68+
When `--ecosystem pypi` is selected, the command:
7069

71-
1. Discovers `rules_python` pip hubs from `MODULE.bazel` (`pip.parse(hub_name = "...")`) and legacy `WORKSPACE` (`pip_parse(name = "...")` / `pip_install(name = "...")`). Hub names are never hardcoded; custom names like `my_pypi` are detected automatically.
70+
1. Discovers `rules_python` pip hubs from Bazel's `mod show_extension` output when available, with bounded static parsing of `MODULE.bazel` (`pip.parse(hub_name = "...")`) and legacy `WORKSPACE` (`pip_parse(name = "...")` / `pip_install(name = "...")`) retained as fallback. Hub names are never hardcoded; custom names like `my_pypi` are detected automatically.
7271
2. Validates each candidate hub by probing it with `bazel query` for `:pkg` targets / `alias(` rules. Invalid candidates are dropped.
7372
3. Runs `bazel query 'deps(kind("py_library|py_binary|py_test", //...))'` to determine which PyPI packages are actually reached by Python rules in the repo (test dependencies included for whole-repo scope).
7473
4. Reads `requirements_lock.txt` (the path discovered from `pip.parse(requirements_lock = "...")`) for canonical pinned versions. When the lockfile is unavailable, falls back to parsing `pypi_name=` and `pypi_version=` tags from the spoke `py_library` rules in the hub-and-spoke architecture.

src/commands/manifest/bazel/bazel-query-runner.mts

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export type BazelQueryResult = {
2525
// Default per-invocation timeout for bazel queries. Bazel cold-cache starts
2626
// can take several minutes; 10 minutes is generous while still bounding CI hangs.
2727
const BAZEL_QUERY_TIMEOUT_MS = 600_000
28+
const STDERR_TAIL_BYTES = 4_096
29+
const STDOUT_EXCERPT_BYTES = 1_024
2830

2931
// Splits the user-supplied --bazel-flags string on whitespace.
3032
// Empty / undefined returns []. No shell parsing — quoted args with embedded
@@ -111,6 +113,58 @@ function numericExitCode(value: unknown): number | undefined {
111113
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
112114
}
113115

116+
function byteLength(value: string): number {
117+
return Buffer.byteLength(value, 'utf8')
118+
}
119+
120+
function excerpt(value: string, maxBytes: number): string {
121+
if (byteLength(value) <= maxBytes) {
122+
return value
123+
}
124+
return value.slice(0, maxBytes) + '\n[truncated]'
125+
}
126+
127+
function logBazelTrace({
128+
argv,
129+
durationMs,
130+
opts,
131+
result,
132+
step,
133+
}: {
134+
argv: string[]
135+
durationMs: number
136+
opts: BazelQueryOptions
137+
result: BazelQueryResult
138+
step: string
139+
}): void {
140+
if (!opts.verbose) {
141+
return
142+
}
143+
const stderrBytes = byteLength(result.stderr)
144+
const stdoutBytes = byteLength(result.stdout)
145+
const category = result.code === 0 ? 'ok' : 'bazel-query-failed'
146+
logger.log('[VERBOSE] bazel subprocess trace:', `category=${category}`, {
147+
argv,
148+
category,
149+
code: result.code,
150+
cwd: opts.cwd,
151+
durationMs,
152+
stderrBytes,
153+
stdoutBytes,
154+
step,
155+
timedOut: false,
156+
timeoutMs: BAZEL_QUERY_TIMEOUT_MS,
157+
})
158+
if (result.code !== 0 && result.stderr) {
159+
logger.log(
160+
'[VERBOSE] bazel stderr tail:',
161+
excerpt(result.stderr.slice(-STDERR_TAIL_BYTES), STDERR_TAIL_BYTES),
162+
)
163+
} else if (result.stdout && stdoutBytes <= STDOUT_EXCERPT_BYTES) {
164+
logger.log('[VERBOSE] bazel stdout excerpt:', result.stdout)
165+
}
166+
}
167+
114168
function normalizeSpawnError(error: unknown): BazelQueryResult {
115169
const e = error as {
116170
code?: unknown
@@ -140,6 +194,7 @@ export async function runBazelQuery(
140194
if (opts.verbose) {
141195
logger.log('[VERBOSE] Executing:', opts.bin, ', args:', argv)
142196
}
197+
const startedAt = Date.now()
143198
const { spinner } = constants
144199
let result: BazelQueryResult | undefined
145200
try {
@@ -162,6 +217,15 @@ export async function runBazelQuery(
162217
} else {
163218
spinner.failAndStop(`bazel query failed (${truncated}).`)
164219
}
220+
if (result) {
221+
logBazelTrace({
222+
argv,
223+
durationMs: Date.now() - startedAt,
224+
opts,
225+
result,
226+
step: `bazel query ${truncated}`,
227+
})
228+
}
165229
}
166230
}
167231

@@ -177,17 +241,27 @@ export async function runBazelModShowVisibleRepos(
177241
if (opts.verbose) {
178242
logger.log('[VERBOSE] Executing:', opts.bin, ', args:', argv)
179243
}
244+
const startedAt = Date.now()
245+
let result: BazelQueryResult
180246
try {
181247
const output = await spawn(opts.bin, argv, {
182248
cwd: opts.cwd,
183249
timeout: BAZEL_QUERY_TIMEOUT_MS,
184250
...(opts.env ? { env: opts.env } : {}),
185251
})
186252
const { code, stderr, stdout } = output
187-
return { code, stdout, stderr }
253+
result = { code, stdout, stderr }
188254
} catch (e) {
189-
return normalizeSpawnError(e)
255+
result = normalizeSpawnError(e)
190256
}
257+
logBazelTrace({
258+
argv,
259+
durationMs: Date.now() - startedAt,
260+
opts,
261+
result,
262+
step: 'bazel mod dump_repo_mapping',
263+
})
264+
return result
191265
}
192266

193267
/**
@@ -202,17 +276,27 @@ export async function runBazelModShowPipExtension(
202276
if (opts.verbose) {
203277
logger.log('[VERBOSE] Executing:', opts.bin, ', args:', argv)
204278
}
279+
const startedAt = Date.now()
280+
let result: BazelQueryResult
205281
try {
206282
const output = await spawn(opts.bin, argv, {
207283
cwd: opts.cwd,
208284
timeout: BAZEL_QUERY_TIMEOUT_MS,
209285
...(opts.env ? { env: opts.env } : {}),
210286
})
211287
const { code, stderr, stdout } = output
212-
return { code, stdout, stderr }
288+
result = { code, stdout, stderr }
213289
} catch (e) {
214-
return normalizeSpawnError(e)
290+
result = normalizeSpawnError(e)
215291
}
292+
logBazelTrace({
293+
argv,
294+
durationMs: Date.now() - startedAt,
295+
opts,
296+
result,
297+
step: 'bazel mod show_extension rules_python pip',
298+
})
299+
return result
216300
}
217301

218302
/**

src/commands/manifest/bazel/bazel-query-runner.test.mts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ vi.mock('../../../constants.mts', () => ({
1515
},
1616
}))
1717

18+
import { logger } from '@socketsecurity/registry/lib/logger'
1819
import { spawn } from '@socketsecurity/registry/lib/spawn'
1920

2021
import {
@@ -192,6 +193,28 @@ describe('runBazelQuery', () => {
192193
})
193194
expect(r).toEqual({ code: -1, stdout: '', stderr: 'missing bazel' })
194195
})
196+
197+
it('emits bounded subprocess trace when verbose is true', async () => {
198+
const logSpy = vi.spyOn(logger, 'log').mockImplementation(() => logger)
199+
try {
200+
// @ts-ignore — narrow return shape for the test's purposes.
201+
mocked.mockResolvedValueOnce({ code: 7, stdout: 'OUT', stderr: 'ERR' })
202+
await runBazelQuery('q', {
203+
bin: 'bazel',
204+
cwd: '/r',
205+
invocationFlags: [],
206+
verbose: true,
207+
})
208+
const text = logSpy.mock.calls
209+
.map(args => args.map(a => String(a)).join(' '))
210+
.join('\n')
211+
expect(text).toContain('bazel subprocess trace')
212+
expect(text).toContain('bazel stderr tail')
213+
expect(text).toContain('bazel-query-failed')
214+
} finally {
215+
logSpy.mockRestore()
216+
}
217+
})
195218
})
196219

197220
describe('runBazelModShowVisibleRepos', () => {

src/commands/manifest/bazel/cmd-manifest-bazel.mts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ const config: CliCommandConfig = {
5757
},
5858
verbose: {
5959
type: 'boolean',
60-
description: 'Stream bazel stdout/stderr',
60+
description:
61+
'Emit bounded Bazel diagnostics with argv, duration, exit status, and output sizes',
6162
},
6263
},
6364
help: (command, config) => `

src/commands/manifest/bazel/extract_bazel_to_maven.mts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,9 @@ export async function extractBazelToMaven(
463463
if (!allArtifacts.length) {
464464
if (!repos.size) {
465465
if (verbose) {
466-
logger.info('No Maven artifacts extracted.')
466+
logger.info(
467+
'No Maven artifacts extracted. failureCategory=no-supported-ecosystem',
468+
)
467469
}
468470
return {
469471
artifactCount: 0,
@@ -473,7 +475,7 @@ export async function extractBazelToMaven(
473475
}
474476
}
475477
logger.fail(
476-
`Discovered Maven repo(s) ${repoNames.join(', ')} but extracted zero artifacts.`,
478+
`Discovered Maven repo(s) ${repoNames.join(', ')} but extracted zero artifacts. failureCategory=ecosystem-detected-but-empty`,
477479
)
478480
return {
479481
artifactCount: 0,

src/commands/manifest/bazel/extract_bazel_to_pypi.mts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,9 @@ export async function extractBazelToPypi(
175175

176176
if (!hubs.size) {
177177
if (verbose) {
178-
logger.info('No PyPI hubs discovered.')
178+
logger.info(
179+
'No PyPI hubs discovered. failureCategory=no-supported-ecosystem',
180+
)
179181
}
180182
return {
181183
artifactCount: 0,
@@ -270,7 +272,9 @@ export async function extractBazelToPypi(
270272
}
271273

272274
if (!allLines.length) {
273-
logger.fail('No PyPI packages extracted. See warnings above.')
275+
logger.fail(
276+
'No PyPI packages extracted. failureCategory=ecosystem-detected-but-empty. See warnings above.',
277+
)
274278
return { artifactCount: 0, manifestPath, ok: false }
275279
}
276280
logger.success(

0 commit comments

Comments
 (0)