diff --git a/CHANGELOG.md b/CHANGELOG.md index 38c5e7b60..69cb233a9 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 a88db41e2..7127fa535 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 21768a6ca..9c735cfc0 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 4d2608450..152df102d 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 ce18e70f2..76007c104 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 8aefbf9f6..5befb7c63 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);