Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

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

## [1.1.133](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.133) - 2026-07-01

### Changed
- 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.
- `--auto-manifest` now fails fast: if a Gradle, sbt, or Maven build fails, the run stops instead of continuing with an incomplete SBOM.

## [1.1.132](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.132) - 2026-06-30

### Changed
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "socket",
"version": "1.1.132",
"version": "1.1.133",
"description": "CLI for Socket.dev",
"homepage": "https://github.com/SocketDev/socket-cli",
"license": "MIT",
Expand Down
48 changes: 34 additions & 14 deletions src/commands/manifest/generate_auto_manifest.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { parseBuildToolOpts } from './parse-build-tool-opts.mts'
import { resolveBuildToolBin } from './scripts/build-tool.mts'
import { serializeSidecar } from './scripts/sidecar.mts'
import { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts'
import { InputError } from '../../utils/errors.mts'
import { readOrDefaultSocketJson } from '../../utils/socket-json.mts'

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

// Under --auto-manifest, a manifest generator that failed — raising the exit
// code above the value captured before it ran — aborts the whole run: a partial
// or empty SBOM silently under-reports dependencies. The generator has already
// logged the specifics. A tolerated resolution failure (ignoreUnresolved) warns
// without touching the exit code, so it passes through here and the run
// continues.
function abortManifestRunIfFailed(
ecosystem: string,
beforeExitCode: string | number | undefined,
): void {
if (process.exitCode && process.exitCode !== beforeExitCode) {
throw new InputError(
`Auto-manifest generation failed for the ${ecosystem} project; aborting (see the errors above).`,
)
}
}

export async function generateAutoManifest({
computeArtifactsSidecar,
cwd,
detected,
outputKind,
reachContinueOnInstallErrors,
verbose,
}: {
// Reachability path: run build tools with files to emit the sidecar.
computeArtifactsSidecar?: boolean | undefined
detected: GeneratableManifests
cwd: string
outputKind: OutputKind
// Reachability install-error gate: tolerate a blocking resolution failure.
reachContinueOnInstallErrors?: boolean | undefined
verbose: boolean
}): Promise<GenerateAutoManifestResult> {
const sockJson = readOrDefaultSocketJson(cwd)
Expand All @@ -52,11 +67,6 @@ export async function generateAutoManifest({
const sidecarAcc: SidecarAccumulator | undefined = computeArtifactsSidecar
? new Map()
: undefined
// Reachability: the install-error gate decides abort; manifest path: socket.json.
const resolveIgnoreUnresolved = (configured: boolean): boolean =>
computeArtifactsSidecar
? configured || Boolean(reachContinueOnInstallErrors)
: configured

if (verbose) {
logger.info(`Using this ${SOCKET_JSON} for defaults:`, sockJson)
Expand All @@ -77,22 +87,26 @@ export async function generateAutoManifest({
// `defaults.manifest.sbt.facts: false` in socket.json.
if (sockJson.defaults?.manifest?.sbt?.facts !== false) {
logger.log('Detected a Scala sbt build, generating Socket facts...')
const beforeExitCode = process.exitCode
await convertSbtToFacts({
...sbtArgs,
excludeConfigs: sockJson.defaults?.manifest?.sbt?.excludeConfigs ?? '',
ignoreUnresolved: resolveIgnoreUnresolved(
Boolean(sockJson.defaults?.manifest?.sbt?.ignoreUnresolved),
ignoreUnresolved: Boolean(
sockJson.defaults?.manifest?.sbt?.ignoreUnresolved,
),
includeConfigs: sockJson.defaults?.manifest?.sbt?.includeConfigs ?? '',
sidecarAcc,
withFiles: computeArtifactsSidecar,
})
abortManifestRunIfFailed('sbt', beforeExitCode)
} else {
logger.log('Detected a Scala sbt build, generating pom files with sbt...')
const beforeExitCode = process.exitCode
await convertSbtToMaven({
...sbtArgs,
out: sockJson.defaults?.manifest?.sbt?.outfile ?? './pom.xml',
})
abortManifestRunIfFailed('sbt', beforeExitCode)
}
}

Expand All @@ -114,37 +128,42 @@ export async function generateAutoManifest({
logger.log(
'Detected a gradle build (Gradle, Kotlin, Scala), generating Socket facts...',
)
const beforeExitCode = process.exitCode
await convertGradleToFacts({
...gradleArgs,
excludeConfigs:
sockJson.defaults?.manifest?.gradle?.excludeConfigs ?? '',
ignoreUnresolved: resolveIgnoreUnresolved(
Boolean(sockJson.defaults?.manifest?.gradle?.ignoreUnresolved),
ignoreUnresolved: Boolean(
sockJson.defaults?.manifest?.gradle?.ignoreUnresolved,
),
includeConfigs:
sockJson.defaults?.manifest?.gradle?.includeConfigs ?? '',
sidecarAcc,
withFiles: computeArtifactsSidecar,
})
abortManifestRunIfFailed('gradle', beforeExitCode)
} else {
logger.log(
'Detected a gradle build (Gradle, Kotlin, Scala), running default gradle generator...',
)
const beforeExitCode = process.exitCode
await convertGradleToMaven(gradleArgs)
abortManifestRunIfFailed('gradle', beforeExitCode)
}
}

if (!sockJson?.defaults?.manifest?.maven?.disabled && detected.maven) {
logger.log('Detected a Maven pom.xml build, generating Socket facts...')
const beforeExitCode = process.exitCode
await convertMavenToFacts({
// Configured bin wins; else prefer ./mvnw, else mvn on PATH.
bin:
sockJson.defaults?.manifest?.maven?.bin ??
resolveBuildToolBin('maven', cwd),
cwd,
excludeConfigs: sockJson.defaults?.manifest?.maven?.excludeConfigs ?? '',
ignoreUnresolved: resolveIgnoreUnresolved(
Boolean(sockJson.defaults?.manifest?.maven?.ignoreUnresolved),
ignoreUnresolved: Boolean(
sockJson.defaults?.manifest?.maven?.ignoreUnresolved,
),
includeConfigs: sockJson.defaults?.manifest?.maven?.includeConfigs ?? '',
mavenOpts: parseBuildToolOpts(
Expand All @@ -154,6 +173,7 @@ export async function generateAutoManifest({
verbose: Boolean(sockJson.defaults?.manifest?.maven?.verbose),
withFiles: computeArtifactsSidecar,
})
abortManifestRunIfFailed('maven', beforeExitCode)
}

if (!sockJson?.defaults?.manifest?.conda?.disabled && detected.conda) {
Expand Down
102 changes: 82 additions & 20 deletions src/commands/manifest/run-manifest-facts.mts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,29 @@ import { renderResolutionErrorReport } from './scripts/resolution-report-render.
import { runManifestScript } from './scripts/run.mts'
import { accumulateSidecar } from './scripts/sidecar.mts'
import constants from '../../constants.mts'
import { InputError } from '../../utils/errors.mts'

import type { BuildTool } from './scripts/build-tool.mts'
import type { ManifestRunResult } from './scripts/run.mts'
import type { SidecarAccumulator } from './scripts/sidecar.mts'

const MAX_FAILURE_OUTPUT_LINES = 40

// Last N non-empty lines of the captured build output, for diagnosing a crash
// without forcing a --verbose rebuild.
function tailBuildOutput(stdout: string, stderr: string): string {
const combined = [stdout, stderr]
.map(s => s.trimEnd())
.filter(Boolean)
.join('\n')
return combined.split('\n').slice(-MAX_FAILURE_OUTPUT_LINES).join('\n')
}

// Runs the bundled build-tool resolution script for a JVM project and writes
// `.socket.facts.json`. `withFiles` (reachability only) additionally folds
// resolved artifact paths into `sidecarAcc`. A blocking resolution failure — or
// a build that crashes without emitting any facts — throws unless
// `ignoreUnresolved`.
// resolved artifact paths into `sidecarAcc`. A blocking resolution failure sets
// a non-zero exit code and returns (matching the `--pom` generator) unless
// `ignoreUnresolved`; a crashed build — a process failure, not an unresolved
// dependency — always fails.
export async function runManifestFacts({
bin,
buildOpts,
Expand Down Expand Up @@ -46,17 +59,53 @@ export async function runManifestFacts({
`Generating Socket facts for the ${ecosystem} project at \`${cwd}\` ...`,
)

const { artifactPaths, code, facts, report } = await runManifestScript(
ecosystem,
{
bin: bin || undefined,
excludeConfigs: excludeConfigs || undefined,
includeConfigs: includeConfigs || undefined,
projectDir: cwd,
toolOpts: buildOpts,
withFiles,
},
)
const scriptOpts = {
bin: bin || undefined,
excludeConfigs: excludeConfigs || undefined,
includeConfigs: includeConfigs || undefined,
projectDir: cwd,
// Stream the build tool's output only when asked; otherwise capture it and
// show a spinner, surfacing the output only if the build crashes.
stdio: verbose ? ('inherit' as const) : ('pipe' as const),
toolOpts: buildOpts,
withFiles,
}
const { spinner } = constants
let result: ManifestRunResult
try {
if (verbose) {
logger.info(
`(Running ${ecosystem} with output streaming; this can take a while.)`,
)
result = await runManifestScript(ecosystem, scriptOpts)
} else {
logger.info(
`(No live output; pass --verbose to stream the ${ecosystem} build output.)`,
)
spinner.start(`Resolving ${ecosystem} dependencies ...`)
result = await runManifestScript(ecosystem, scriptOpts)
if (result.code === 0) {
spinner.successAndStop(`Resolved ${ecosystem} dependencies.`)
} else {
spinner.failAndStop(
`${ecosystem} build exited with code ${result.code}.`,
)
}
}
} catch (e) {
// Only a spawn-level failure (e.g. the build tool missing from PATH) reaches
// here; runNeverThrow returns non-zero build exits rather than throwing.
if (!verbose) {
spinner.failAndStop(`Failed to run ${ecosystem}.`)
}
process.exitCode = 1
logger.fail(
`Could not run the ${ecosystem} build tool` +
(verbose ? `: ${e}` : ' (run with --verbose for details).'),
)
return
}
const { artifactPaths, code, facts, report, stderr, stdout } = result

const rendered = renderResolutionErrorReport(
report.failures,
Expand All @@ -69,10 +118,12 @@ export async function runManifestFacts({
if (ignoreUnresolved) {
logger.warn(rendered.summary)
} else {
process.exitCode = 1
logger.fail(rendered.summary)
if (verbose && rendered.details) {
logger.log(rendered.details)
}
throw new InputError(rendered.summary)
return
}
}
if (rendered.nonBlockingNotice) {
Expand All @@ -94,11 +145,22 @@ export async function runManifestFacts({
!facts.projects?.length &&
!report.failures.length
) {
const message = `The ${ecosystem} build failed (exit code ${code}) before producing any Socket facts. Re-run with --verbose for the build tool's output.`
if (!ignoreUnresolved) {
throw new InputError(message)
if (!verbose) {
const tail = tailBuildOutput(stdout, stderr)
if (tail) {
logger.group('Build output:')
logger.error(tail)
logger.groupEnd()
}
}
logger.warn(message)
// A crashed build is a process failure (missing JDK/build tool, unparseable
// project, OOM, plugin error), not an unresolved dependency, so it fails
// regardless of `ignoreUnresolved` — that flag only tolerates dependencies a
// successful run couldn't resolve.
process.exitCode = 1
logger.fail(
`The ${ecosystem} build failed (exit code ${code}) before producing any Socket facts.`,
)
return
}

Expand Down
Loading
Loading