Skip to content

Commit 132d76f

Browse files
committed
fix: abort scan on recursive manifest failure and keep sbt tmpDir alive
Under --dynamic-sbom-inference, scan-create now fails closed when any recursive build root reports failed status, matching the standalone dynamic-sbom-inference handler. Also pass the caller-owned socket-auto-manifest tmpDir into generateRecursiveManifests so sbt's shared global base (and withFiles artifactPaths under boot) survives until reachability finishes.
1 parent b5130fa commit 132d76f

4 files changed

Lines changed: 90 additions & 4 deletions

File tree

src/commands/manifest/generate-recursive-manifests.mts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ export async function generateRecursiveManifests({
217217
cwd,
218218
excludePaths,
219219
sidecarAcc,
220+
tmpDir,
220221
verbose,
221222
withFiles,
222223
}: {
@@ -225,6 +226,12 @@ export async function generateRecursiveManifests({
225226
// Reachability path only: run build tools with files and fold resolved
226227
// artifact paths into sidecarAcc, keyed by each root's own factsPath.
227228
sidecarAcc?: SidecarAccumulator | undefined
229+
// Caller-owned sbt global base; see ManifestScriptOptions.tmpDir. When
230+
// supplied (e.g. scan-create, which needs the Scala toolchain to outlive
231+
// this call for reachability's withFiles artifactPaths), reused as the
232+
// shared base across every sbt root. Unset ⇒ allocated ephemerally and
233+
// cleaned up before this function returns (standalone CLI path).
234+
tmpDir?: string | undefined
228235
verbose: boolean
229236
withFiles?: boolean | undefined
230237
}): Promise<RecursiveManifestOutcome[]> {
@@ -259,9 +266,13 @@ export async function generateRecursiveManifests({
259266
// instead of being reprovisioned per root (the plugin file is rewritten and
260267
// records.tsv is fully overwritten - not appended - on every invocation, so
261268
// reuse is safe). Skipped entirely when there's no sbt root to benefit.
262-
const outcomes = candidatesByTool.get('sbt')?.length
263-
? await withTmpDir('socket-sbt-facts-shared-', runAll)
264-
: await runAll(undefined)
269+
// Prefer a caller-owned tmpDir so reachability can keep withFiles paths
270+
// under <global.base>/boot alive after this function returns.
271+
const outcomes = !candidatesByTool.get('sbt')?.length
272+
? await runAll(undefined)
273+
: tmpDir
274+
? await runAll(tmpDir)
275+
: await withTmpDir('socket-sbt-facts-shared-', runAll)
265276

266277
if (verbose) {
267278
logger.info(`Discovered ${outcomes.length} build-tool candidate(s).`)

src/commands/manifest/generate-recursive-manifests.test.mts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,43 @@ describe('generateRecursiveManifests', () => {
577577
}
578578
})
579579

580+
it('reuses a caller-supplied tmpDir as the shared sbt global base instead of allocating one', async () => {
581+
const outer = await fs.mkdtemp(path.join(tmpdir(), 'sbt-caller-tmpdir-'))
582+
const callerTmp = path.join(outer, 'caller-owned-sbt-base')
583+
const sbtA = path.join(outer, 'sbt-a')
584+
const sbtB = path.join(outer, 'sbt-b')
585+
try {
586+
await fs.mkdir(callerTmp, { recursive: true })
587+
await fs.mkdir(sbtA, { recursive: true })
588+
await fs.mkdir(sbtB, { recursive: true })
589+
await fs.writeFile(path.join(sbtA, 'build.sbt'), '')
590+
await fs.writeFile(path.join(sbtB, 'build.sbt'), '')
591+
592+
const sbtTmpDirs: Array<string | undefined> = []
593+
vi.mocked(runManifestFacts).mockImplementation(
594+
async ({ cwd, tmpDir }) => {
595+
sbtTmpDirs.push(tmpDir)
596+
return {
597+
factsPath: path.join(cwd, '.socket.facts.json'),
598+
projects: [],
599+
}
600+
},
601+
)
602+
603+
await generateRecursiveManifests({
604+
cwd: outer,
605+
tmpDir: callerTmp,
606+
verbose: false,
607+
})
608+
609+
expect(sbtTmpDirs).toEqual([callerTmp, callerTmp])
610+
// Caller owns the directory; it must still exist after generation returns.
611+
await expect(fs.stat(callerTmp)).resolves.toBeDefined()
612+
} finally {
613+
await fs.rm(outer, { recursive: true, force: true })
614+
}
615+
})
616+
580617
it('does not allocate a shared tmpDir at all when no sbt root is discovered', async () => {
581618
const outer = await fs.mkdtemp(path.join(tmpdir(), 'no-sbt-tmpdir-'))
582619
try {

src/commands/scan/handle-create-new-scan.mts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import constants from '../../constants.mts'
1818
import { checkCommandInput } from '../../utils/check-input.mts'
1919
import { compressSocketFactsForUpload } from '../../utils/coana.mts'
2020
import { findSocketYmlSync } from '../../utils/config.mts'
21+
import { InputError } from '../../utils/errors.mts'
2122
import { withTmpDir } from '../../utils/fs.mts'
2223
import { getPackageFilesForScan } from '../../utils/path-resolve.mts'
2324
import { readOrDefaultSocketJson } from '../../utils/socket-json.mts'
@@ -177,9 +178,20 @@ export async function handleCreateNewScan({
177178
cwd,
178179
excludePaths: reach.excludePaths,
179180
sidecarAcc,
181+
// Keep the shared sbt global base alive until reachability below
182+
// consumes withFiles artifactPaths under <global.base>/boot.
183+
tmpDir: manifestTmpDir,
180184
verbose: false,
181185
withFiles: reach.runReachabilityAnalysis,
182186
})
187+
// Fail closed like handleManifestDynamicSbomInference /
188+
// abortManifestRunIfFailed: a partial multi-root run must not upload
189+
// or run reachability as if every build root succeeded.
190+
if (outcomes.some(o => o.status === 'failed')) {
191+
throw new InputError(
192+
'One or more build roots failed to generate Socket facts; aborting (see the errors above).',
193+
)
194+
}
183195
const generatedFactsPaths = outcomes
184196
.filter(o => o.status === 'generated')
185197
.map(o => o.factsPath!)

src/commands/scan/handle-create-new-scan.test.mts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,11 @@ describe('handleCreateNewScan excludePaths', () => {
201201
await handleCreateNewScan(config)
202202

203203
expect(mockGenerateRecursiveManifests).toHaveBeenCalledWith(
204-
expect.objectContaining({ cwd: '/repo', withFiles: false }),
204+
expect.objectContaining({
205+
cwd: '/repo',
206+
tmpDir: expect.any(String),
207+
withFiles: false,
208+
}),
205209
)
206210
expect(mockGenerateAutoManifest).toHaveBeenCalledWith(
207211
expect.objectContaining({
@@ -227,6 +231,28 @@ describe('handleCreateNewScan excludePaths', () => {
227231
)
228232
})
229233

234+
it('aborts the scan when recursive manifest generation reports a failed root under --dynamic-sbom-inference', async () => {
235+
mockGenerateRecursiveManifests.mockResolvedValueOnce([
236+
{
237+
dir: '/repo/service-a',
238+
ecosystem: 'maven',
239+
factsPath: '/repo/service-a/.socket.facts.json',
240+
status: 'generated',
241+
},
242+
{ dir: '/repo/service-b', ecosystem: 'maven', status: 'failed' },
243+
{ dir: '/repo/service-c', ecosystem: 'maven', status: 'aborted' },
244+
])
245+
246+
const config = createConfig({ autoManifest: true, targets: ['/repo'] })
247+
config.reach.dynamicSbomInference = true
248+
249+
await expect(handleCreateNewScan(config)).rejects.toThrow(
250+
'One or more build roots failed to generate Socket facts',
251+
)
252+
expect(mockGetPackageFilesForScan).not.toHaveBeenCalled()
253+
expect(mockFetchCreateOrgFullScan).not.toHaveBeenCalled()
254+
})
255+
230256
it('accumulates a sidecar across recursively discovered build roots and forwards it to reachability analysis', async () => {
231257
mockGenerateRecursiveManifests.mockImplementationOnce(
232258
async ({ sidecarAcc }) => {

0 commit comments

Comments
 (0)