From 28cad18c23bc96c1603fb3e14ab507da469030ed Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Thu, 24 Sep 2026 22:36:52 +0200 Subject: [PATCH] fix(manifest): fail clearly on a directory without a Maven or sbt build Maven hands the extension a stand-in project with no basedir when it finds no POM, and the extension's workspace lookup threw an NPE that hid Maven's own "no POM in this directory" error. Skip that project so Maven's error surfaces. sbt runs in any directory by synthesizing a default project, so pointing `socket manifest scala` at a non-sbt directory wrote a bogus facts file and exited 0. Refuse to run sbt without a build.sbt or project directory. --- CHANGELOG.md | 3 ++ src/commands/manifest/run-manifest-facts.mts | 9 ++++ .../manifest/run-manifest-facts.test.mts | 45 +++++++++++++++++++ src/commands/manifest/scripts/build-tool.mts | 10 +++++ .../socket/SocketFactsRecordsEngine.java | 4 ++ .../socket/SocketWorkspacesRecordsEngine.java | 3 ++ 6 files changed, 74 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38c5e7b604..69cb233a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Updated the Coana CLI to v `15.10.55`. - Generated Maven, Gradle and sbt `.socket.facts.json` files are now substantially smaller, making uploads for large JVM projects faster and more reliable. +### Fixed +- Running `socket manifest maven` or `socket manifest scala` on a directory without a build now fails with a clear message, instead of crashing (Maven) or silently producing a bogus Socket facts file (sbt). + ## [1.1.178](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.178) - 2026-09-23 ### Changed diff --git a/src/commands/manifest/run-manifest-facts.mts b/src/commands/manifest/run-manifest-facts.mts index a88db41e2f..7127fa5350 100644 --- a/src/commands/manifest/run-manifest-facts.mts +++ b/src/commands/manifest/run-manifest-facts.mts @@ -7,6 +7,7 @@ import { expandEnvVarRefs, formatMissingEnvVarRefs, } from './expand-env-var-refs.mts' +import { looksLikeSbtBuild } from './scripts/build-tool.mts' import { renderResolutionErrorReport } from './scripts/resolution-report-render.mts' import { runManifestScript } from './scripts/run.mts' import { accumulateSidecar } from './scripts/sidecar.mts' @@ -93,6 +94,14 @@ export async function runManifestFacts({ resolvedJavaHome = expanded.value } + if (ecosystem === 'sbt' && !looksLikeSbtBuild(cwd)) { + process.exitCode = 1 + logger.fail( + `No sbt build found at \`${cwd}\` (expected a build.sbt file or a project directory).`, + ) + return null + } + logger.info( `Generating Socket facts for the ${ecosystem} project at \`${cwd}\` ...`, ) diff --git a/src/commands/manifest/run-manifest-facts.test.mts b/src/commands/manifest/run-manifest-facts.test.mts index 21768a6ca2..9c735cfc03 100644 --- a/src/commands/manifest/run-manifest-facts.test.mts +++ b/src/commands/manifest/run-manifest-facts.test.mts @@ -159,3 +159,48 @@ describe('runManifestFacts - sidecar', () => { ) }) }) + +describe('runManifestFacts - sbt build detection', () => { + let cwd = '' + + beforeEach(async () => { + cwd = await fs.mkdtemp(path.join(tmpdir(), 'run-manifest-facts-')) + vi.mocked(runManifestScript).mockReset() + process.exitCode = undefined + }) + afterEach(async () => { + await fs.rm(cwd, { recursive: true, force: true }) + process.exitCode = undefined + }) + + it('fails without running sbt when the directory has no sbt build', async () => { + const outcome = await runManifestFacts({ + ...baseArgs, + cwd, + ecosystem: 'sbt', + }) + + expect(outcome).toBeNull() + expect(process.exitCode).toBe(1) + expect(runManifestScript).not.toHaveBeenCalled() + await expect( + fs.access(path.join(cwd, '.socket.facts.json')), + ).rejects.toThrow() + }) + + it.each(['build.sbt', 'project'])( + 'runs sbt when the directory has %s', + async marker => { + if (marker === 'project') { + await fs.mkdir(path.join(cwd, marker)) + } else { + await fs.writeFile(path.join(cwd, marker), '') + } + vi.mocked(runManifestScript).mockResolvedValue(okResult()) + + await runManifestFacts({ ...baseArgs, cwd, ecosystem: 'sbt' }) + + expect(runManifestScript).toHaveBeenCalledOnce() + }, + ) +}) diff --git a/src/commands/manifest/scripts/build-tool.mts b/src/commands/manifest/scripts/build-tool.mts index 4d26084500..152df102df 100644 --- a/src/commands/manifest/scripts/build-tool.mts +++ b/src/commands/manifest/scripts/build-tool.mts @@ -19,6 +19,16 @@ const BUILD_TOOL_WRAPPER = { maven: 'mvnw', } as unknown as Partial> +// sbt happily runs in any directory, synthesizing a default project from its +// name, so an sbt run outside a build yields a plausible but bogus SBOM. Maven +// and Gradle refuse such a directory themselves. +export function looksLikeSbtBuild(projectDir: string): boolean { + return ( + existsSync(resolve(projectDir, 'build.sbt')) || + existsSync(resolve(projectDir, 'project')) + ) +} + export function resolveBuildToolBin( tool: BuildTool, projectDir: string, diff --git a/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketFactsRecordsEngine.java b/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketFactsRecordsEngine.java index ce18e70f28..76007c1043 100644 --- a/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketFactsRecordsEngine.java +++ b/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketFactsRecordsEngine.java @@ -95,6 +95,9 @@ public void run(MavenSession session, List reactor, File rootDir, rec(lines, "meta", "maven", mavenVersion, System.getProperty("java.version")); for (MavenProject module : reactor) { + // No basedir: Maven's stand-in project for a directory without a POM. Skipping it lets Maven's + // own "no POM in this directory" error surface instead of an NPE. + if (module.getBasedir() == null) continue; String ws = SocketSupport.workspace(rootDir.toPath(), module.getBasedir().toPath()); if (SocketSupport.isExcludedPath(ws, excludes)) continue; rec(lines, "project", ws, module.getGroupId(), module.getArtifactId(), module.getVersion(), ws); @@ -109,6 +112,7 @@ public void run(MavenSession session, List reactor, File rootDir, Set failures = new LinkedHashSet<>(); int rootIdx = 0; for (MavenProject module : reactor) { + if (module.getBasedir() == null) continue; String ws = SocketSupport.workspace(rootDir.toPath(), module.getBasedir().toPath()); // A wholly excluded reactor module is not resolved (matches the project-record skip above). if (SocketSupport.isExcludedPath(ws, excludes)) continue; diff --git a/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketWorkspacesRecordsEngine.java b/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketWorkspacesRecordsEngine.java index 8aefbf9f69..5befb7c63c 100644 --- a/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketWorkspacesRecordsEngine.java +++ b/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketWorkspacesRecordsEngine.java @@ -34,6 +34,9 @@ public static void run(List reactor, File rootDir, Options opts, S rec(lines, "meta", "maven", mavenVersion, System.getProperty("java.version")); for (MavenProject module : reactor) { + // No basedir: Maven's stand-in project for a directory without a POM. Skipping it lets Maven's + // own "no POM in this directory" error surface instead of an NPE. + if (module.getBasedir() == null) continue; String ws = SocketSupport.workspace(rootDir.toPath(), module.getBasedir().toPath()); if (SocketSupport.isExcludedPath(ws, excludes)) continue; rec(lines, "project", ws, module.getGroupId(), module.getArtifactId(), module.getVersion(), ws);