Skip to content

Commit 5b41564

Browse files
authored
feat(manifest): quieter JVM output + fail-closed manifest generation (1.1.133) (#1392)
* feat(manifest): quiet JVM build-tool output behind a spinner (1.1.133) `socket manifest gradle|sbt|maven` (and `--auto-manifest`) streamed the build tool's full output to the terminal. Capture it by default and show a spinner instead, streaming live only under --verbose. On a build crash the captured output tail is surfaced so failures stay diagnosable without a rebuild. Also fix runNeverThrow: the registry's isSpawnError is unreliable (it never matches), so a non-zero build exit rethrew as an opaque "command failed" instead of returning the exit code. Duck-type the numeric exit code directly. * refactor(manifest): match the --pom path's spawn + error handling Align the JVM facts path with convertGradleToMaven (the `--pom` generator), which is the direct-spawn sibling of these commands rather than the coana dlx wrapper: - spinner + captured output by default, stream on --verbose (unchanged intent). - On failure use process.exitCode = 1 + logger.fail(...) + return instead of throwing, so the facts path behaves identically to the pom path — including under --auto-manifest, where both now set exit 1 and continue the sequence. - runNeverThrow classifies a non-zero build exit by a numeric `code` (the utils/dlx.mts convention), not the registry's isSpawnError, which is broken upstream and never matches. * docs(manifest): correct stale 'throws' in runManifestFacts comment * feat(manifest): fail closed under --auto-manifest on a JVM build failure Under --auto-manifest, a failed manifest generation now aborts the whole run instead of continuing with a partial or empty SBOM (which silently under-reports dependencies). This is enforced uniformly for every JVM path — Gradle, sbt, and Maven, in both Socket-facts and pom mode — in generate_auto_manifest, keying off the exit code each generator already sets. The standalone `socket manifest` commands are unchanged (they exit non-zero, as before). Failures the user opted to tolerate (ignoreUnresolved / --reach-continue-on-install-errors) warn without setting an exit code, so they continue. * fix(manifest): a crashed JVM build always fails, ignoring ignoreUnresolved ignoreUnresolved (and --ignore-unresolved) means 'the build ran but some dependencies could not be resolved; tolerate those'. It must not swallow the build process itself failing (missing JDK/build tool, unparseable project, OOM, plugin crash). Scope it to the blocking-resolution-failure branch only; a crashed build now fails regardless. * refactor(scan): decouple --reach-continue-on-install-errors from manifest ignoreUnresolved --reach-continue-on-install-errors is a Coana concern (it tells Coana to keep going past its own install errors) and is threaded to Coana in perform-reachability-analysis. It should not also decide whether socket-cli's manifest generation tolerates unresolved dependencies — that is a separate concern governed by the manifest's own ignoreUnresolved (socket.json / --ignore-unresolved). Drop the resolveIgnoreUnresolved coupling so the two are independent; the flag still reaches Coana unchanged.
1 parent 48c4d3f commit 5b41564

6 files changed

Lines changed: 155 additions & 46 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

7+
## [1.1.133](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.133) - 2026-07-01
8+
9+
### Changed
10+
- Quieter `socket manifest gradle`, `sbt`, and `maven`: the build tool's output is now hidden behind a progress spinner and shown only if the build fails. Pass `--verbose` to stream it live.
11+
- `--auto-manifest` now fails fast: if a Gradle, sbt, or Maven build fails, the run stops instead of continuing with an incomplete SBOM.
12+
713
## [1.1.132](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.132) - 2026-06-30
814

915
### Changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "socket",
3-
"version": "1.1.132",
3+
"version": "1.1.133",
44
"description": "CLI for Socket.dev",
55
"homepage": "https://github.com/SocketDev/socket-cli",
66
"license": "MIT",

src/commands/manifest/generate_auto_manifest.mts

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { parseBuildToolOpts } from './parse-build-tool-opts.mts'
1313
import { resolveBuildToolBin } from './scripts/build-tool.mts'
1414
import { serializeSidecar } from './scripts/sidecar.mts'
1515
import { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts'
16+
import { InputError } from '../../utils/errors.mts'
1617
import { readOrDefaultSocketJson } from '../../utils/socket-json.mts'
1718

1819
import type { GeneratableManifests } from './detect-manifest-actions.mts'
@@ -28,21 +29,35 @@ export type GenerateAutoManifestResult = {
2829
resolvedPathsSidecar?: ResolvedPathsSidecar | undefined
2930
}
3031

32+
// Under --auto-manifest, a manifest generator that failed — raising the exit
33+
// code above the value captured before it ran — aborts the whole run: a partial
34+
// or empty SBOM silently under-reports dependencies. The generator has already
35+
// logged the specifics. A tolerated resolution failure (ignoreUnresolved) warns
36+
// without touching the exit code, so it passes through here and the run
37+
// continues.
38+
function abortManifestRunIfFailed(
39+
ecosystem: string,
40+
beforeExitCode: string | number | undefined,
41+
): void {
42+
if (process.exitCode && process.exitCode !== beforeExitCode) {
43+
throw new InputError(
44+
`Auto-manifest generation failed for the ${ecosystem} project; aborting (see the errors above).`,
45+
)
46+
}
47+
}
48+
3149
export async function generateAutoManifest({
3250
computeArtifactsSidecar,
3351
cwd,
3452
detected,
3553
outputKind,
36-
reachContinueOnInstallErrors,
3754
verbose,
3855
}: {
3956
// Reachability path: run build tools with files to emit the sidecar.
4057
computeArtifactsSidecar?: boolean | undefined
4158
detected: GeneratableManifests
4259
cwd: string
4360
outputKind: OutputKind
44-
// Reachability install-error gate: tolerate a blocking resolution failure.
45-
reachContinueOnInstallErrors?: boolean | undefined
4661
verbose: boolean
4762
}): Promise<GenerateAutoManifestResult> {
4863
const sockJson = readOrDefaultSocketJson(cwd)
@@ -52,11 +67,6 @@ export async function generateAutoManifest({
5267
const sidecarAcc: SidecarAccumulator | undefined = computeArtifactsSidecar
5368
? new Map()
5469
: undefined
55-
// Reachability: the install-error gate decides abort; manifest path: socket.json.
56-
const resolveIgnoreUnresolved = (configured: boolean): boolean =>
57-
computeArtifactsSidecar
58-
? configured || Boolean(reachContinueOnInstallErrors)
59-
: configured
6070

6171
if (verbose) {
6272
logger.info(`Using this ${SOCKET_JSON} for defaults:`, sockJson)
@@ -77,22 +87,26 @@ export async function generateAutoManifest({
7787
// `defaults.manifest.sbt.facts: false` in socket.json.
7888
if (sockJson.defaults?.manifest?.sbt?.facts !== false) {
7989
logger.log('Detected a Scala sbt build, generating Socket facts...')
90+
const beforeExitCode = process.exitCode
8091
await convertSbtToFacts({
8192
...sbtArgs,
8293
excludeConfigs: sockJson.defaults?.manifest?.sbt?.excludeConfigs ?? '',
83-
ignoreUnresolved: resolveIgnoreUnresolved(
84-
Boolean(sockJson.defaults?.manifest?.sbt?.ignoreUnresolved),
94+
ignoreUnresolved: Boolean(
95+
sockJson.defaults?.manifest?.sbt?.ignoreUnresolved,
8596
),
8697
includeConfigs: sockJson.defaults?.manifest?.sbt?.includeConfigs ?? '',
8798
sidecarAcc,
8899
withFiles: computeArtifactsSidecar,
89100
})
101+
abortManifestRunIfFailed('sbt', beforeExitCode)
90102
} else {
91103
logger.log('Detected a Scala sbt build, generating pom files with sbt...')
104+
const beforeExitCode = process.exitCode
92105
await convertSbtToMaven({
93106
...sbtArgs,
94107
out: sockJson.defaults?.manifest?.sbt?.outfile ?? './pom.xml',
95108
})
109+
abortManifestRunIfFailed('sbt', beforeExitCode)
96110
}
97111
}
98112

@@ -114,37 +128,42 @@ export async function generateAutoManifest({
114128
logger.log(
115129
'Detected a gradle build (Gradle, Kotlin, Scala), generating Socket facts...',
116130
)
131+
const beforeExitCode = process.exitCode
117132
await convertGradleToFacts({
118133
...gradleArgs,
119134
excludeConfigs:
120135
sockJson.defaults?.manifest?.gradle?.excludeConfigs ?? '',
121-
ignoreUnresolved: resolveIgnoreUnresolved(
122-
Boolean(sockJson.defaults?.manifest?.gradle?.ignoreUnresolved),
136+
ignoreUnresolved: Boolean(
137+
sockJson.defaults?.manifest?.gradle?.ignoreUnresolved,
123138
),
124139
includeConfigs:
125140
sockJson.defaults?.manifest?.gradle?.includeConfigs ?? '',
126141
sidecarAcc,
127142
withFiles: computeArtifactsSidecar,
128143
})
144+
abortManifestRunIfFailed('gradle', beforeExitCode)
129145
} else {
130146
logger.log(
131147
'Detected a gradle build (Gradle, Kotlin, Scala), running default gradle generator...',
132148
)
149+
const beforeExitCode = process.exitCode
133150
await convertGradleToMaven(gradleArgs)
151+
abortManifestRunIfFailed('gradle', beforeExitCode)
134152
}
135153
}
136154

137155
if (!sockJson?.defaults?.manifest?.maven?.disabled && detected.maven) {
138156
logger.log('Detected a Maven pom.xml build, generating Socket facts...')
157+
const beforeExitCode = process.exitCode
139158
await convertMavenToFacts({
140159
// Configured bin wins; else prefer ./mvnw, else mvn on PATH.
141160
bin:
142161
sockJson.defaults?.manifest?.maven?.bin ??
143162
resolveBuildToolBin('maven', cwd),
144163
cwd,
145164
excludeConfigs: sockJson.defaults?.manifest?.maven?.excludeConfigs ?? '',
146-
ignoreUnresolved: resolveIgnoreUnresolved(
147-
Boolean(sockJson.defaults?.manifest?.maven?.ignoreUnresolved),
165+
ignoreUnresolved: Boolean(
166+
sockJson.defaults?.manifest?.maven?.ignoreUnresolved,
148167
),
149168
includeConfigs: sockJson.defaults?.manifest?.maven?.includeConfigs ?? '',
150169
mavenOpts: parseBuildToolOpts(
@@ -154,6 +173,7 @@ export async function generateAutoManifest({
154173
verbose: Boolean(sockJson.defaults?.manifest?.maven?.verbose),
155174
withFiles: computeArtifactsSidecar,
156175
})
176+
abortManifestRunIfFailed('maven', beforeExitCode)
157177
}
158178

159179
if (!sockJson?.defaults?.manifest?.conda?.disabled && detected.conda) {

src/commands/manifest/run-manifest-facts.mts

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,29 @@ import { renderResolutionErrorReport } from './scripts/resolution-report-render.
77
import { runManifestScript } from './scripts/run.mts'
88
import { accumulateSidecar } from './scripts/sidecar.mts'
99
import constants from '../../constants.mts'
10-
import { InputError } from '../../utils/errors.mts'
1110

1211
import type { BuildTool } from './scripts/build-tool.mts'
12+
import type { ManifestRunResult } from './scripts/run.mts'
1313
import type { SidecarAccumulator } from './scripts/sidecar.mts'
1414

15+
const MAX_FAILURE_OUTPUT_LINES = 40
16+
17+
// Last N non-empty lines of the captured build output, for diagnosing a crash
18+
// without forcing a --verbose rebuild.
19+
function tailBuildOutput(stdout: string, stderr: string): string {
20+
const combined = [stdout, stderr]
21+
.map(s => s.trimEnd())
22+
.filter(Boolean)
23+
.join('\n')
24+
return combined.split('\n').slice(-MAX_FAILURE_OUTPUT_LINES).join('\n')
25+
}
26+
1527
// Runs the bundled build-tool resolution script for a JVM project and writes
1628
// `.socket.facts.json`. `withFiles` (reachability only) additionally folds
17-
// resolved artifact paths into `sidecarAcc`. A blocking resolution failure — or
18-
// a build that crashes without emitting any facts — throws unless
19-
// `ignoreUnresolved`.
29+
// resolved artifact paths into `sidecarAcc`. A blocking resolution failure sets
30+
// a non-zero exit code and returns (matching the `--pom` generator) unless
31+
// `ignoreUnresolved`; a crashed build — a process failure, not an unresolved
32+
// dependency — always fails.
2033
export async function runManifestFacts({
2134
bin,
2235
buildOpts,
@@ -46,17 +59,53 @@ export async function runManifestFacts({
4659
`Generating Socket facts for the ${ecosystem} project at \`${cwd}\` ...`,
4760
)
4861

49-
const { artifactPaths, code, facts, report } = await runManifestScript(
50-
ecosystem,
51-
{
52-
bin: bin || undefined,
53-
excludeConfigs: excludeConfigs || undefined,
54-
includeConfigs: includeConfigs || undefined,
55-
projectDir: cwd,
56-
toolOpts: buildOpts,
57-
withFiles,
58-
},
59-
)
62+
const scriptOpts = {
63+
bin: bin || undefined,
64+
excludeConfigs: excludeConfigs || undefined,
65+
includeConfigs: includeConfigs || undefined,
66+
projectDir: cwd,
67+
// Stream the build tool's output only when asked; otherwise capture it and
68+
// show a spinner, surfacing the output only if the build crashes.
69+
stdio: verbose ? ('inherit' as const) : ('pipe' as const),
70+
toolOpts: buildOpts,
71+
withFiles,
72+
}
73+
const { spinner } = constants
74+
let result: ManifestRunResult
75+
try {
76+
if (verbose) {
77+
logger.info(
78+
`(Running ${ecosystem} with output streaming; this can take a while.)`,
79+
)
80+
result = await runManifestScript(ecosystem, scriptOpts)
81+
} else {
82+
logger.info(
83+
`(No live output; pass --verbose to stream the ${ecosystem} build output.)`,
84+
)
85+
spinner.start(`Resolving ${ecosystem} dependencies ...`)
86+
result = await runManifestScript(ecosystem, scriptOpts)
87+
if (result.code === 0) {
88+
spinner.successAndStop(`Resolved ${ecosystem} dependencies.`)
89+
} else {
90+
spinner.failAndStop(
91+
`${ecosystem} build exited with code ${result.code}.`,
92+
)
93+
}
94+
}
95+
} catch (e) {
96+
// Only a spawn-level failure (e.g. the build tool missing from PATH) reaches
97+
// here; runNeverThrow returns non-zero build exits rather than throwing.
98+
if (!verbose) {
99+
spinner.failAndStop(`Failed to run ${ecosystem}.`)
100+
}
101+
process.exitCode = 1
102+
logger.fail(
103+
`Could not run the ${ecosystem} build tool` +
104+
(verbose ? `: ${e}` : ' (run with --verbose for details).'),
105+
)
106+
return
107+
}
108+
const { artifactPaths, code, facts, report, stderr, stdout } = result
60109

61110
const rendered = renderResolutionErrorReport(
62111
report.failures,
@@ -69,10 +118,12 @@ export async function runManifestFacts({
69118
if (ignoreUnresolved) {
70119
logger.warn(rendered.summary)
71120
} else {
121+
process.exitCode = 1
122+
logger.fail(rendered.summary)
72123
if (verbose && rendered.details) {
73124
logger.log(rendered.details)
74125
}
75-
throw new InputError(rendered.summary)
126+
return
76127
}
77128
}
78129
if (rendered.nonBlockingNotice) {
@@ -94,11 +145,22 @@ export async function runManifestFacts({
94145
!facts.projects?.length &&
95146
!report.failures.length
96147
) {
97-
const message = `The ${ecosystem} build failed (exit code ${code}) before producing any Socket facts. Re-run with --verbose for the build tool's output.`
98-
if (!ignoreUnresolved) {
99-
throw new InputError(message)
148+
if (!verbose) {
149+
const tail = tailBuildOutput(stdout, stderr)
150+
if (tail) {
151+
logger.group('Build output:')
152+
logger.error(tail)
153+
logger.groupEnd()
154+
}
100155
}
101-
logger.warn(message)
156+
// A crashed build is a process failure (missing JDK/build tool, unparseable
157+
// project, OOM, plugin error), not an unresolved dependency, so it fails
158+
// regardless of `ignoreUnresolved` — that flag only tolerates dependencies a
159+
// successful run couldn't resolve.
160+
process.exitCode = 1
161+
logger.fail(
162+
`The ${ecosystem} build failed (exit code ${code}) before producing any Socket facts.`,
163+
)
102164
return
103165
}
104166

0 commit comments

Comments
 (0)