From f76bf242559b6ae74ffe4c162138f6bda62b56be Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Fri, 25 Sep 2026 06:57:27 +0200 Subject: [PATCH] fix(manifest): auto-manifest reads the Conda file it actually detected Detection accepted both environment.yml and environment.yaml but only returned a boolean, so the filename was dropped. The generator then read environment.yml unconditionally and failed on any repo using the .yaml spelling, unless a socket.json infile override was in place. Detection now reports which file it found and the generator reads that one. socket.json still wins when it sets infile. Two more things in the same path: - The generated requirements.txt was written relative to the process cwd instead of the target dir, so `socket manifest auto ` and `socket scan create ` dropped it outside the scanned tree. It now resolves against the target dir, which is what the --out flag already documents. - Conda was the one auto-manifest ecosystem with no fail-closed check. Gradle, Maven and sbt abort the run when their generator fails; Conda just set exit 1 and carried on, so a scan uploaded without the pip block and the CLI still exited non-zero. Conda now aborts like the others. --- CHANGELOG.md | 9 ++ src/commands/manifest/cmd-manifest-conda.mts | 3 +- .../manifest/detect-manifest-actions.mts | 24 ++++-- .../manifest/detect-manifest-actions.test.mts | 55 ++++++++++++ .../manifest/generate_auto_manifest.mts | 18 ++-- .../manifest/generate_auto_manifest.test.mts | 85 +++++++++++++++++++ .../manifest/handle-manifest-conda.mts | 10 ++- 7 files changed, 191 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74ce4b9551..af0f8a9df3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ 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/). +## [Unreleased] + +### Changed +- A failed Conda manifest step now stops `--auto-manifest` instead of uploading a scan that under-reports your `pip:` dependencies, matching how Gradle, Maven and sbt already behave. + +### Fixed +- `--auto-manifest` now reads whichever Conda environment file your project uses, `environment.yml` or `environment.yaml`. Repos on the `.yaml` spelling no longer need a `socket.json` workaround. +- The Conda `requirements.txt` now lands in the directory you targeted, so scans pick it up even when you run the CLI from somewhere else. + ## [1.1.179](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.179) - 2026-09-24 ### Changed diff --git a/src/commands/manifest/cmd-manifest-conda.mts b/src/commands/manifest/cmd-manifest-conda.mts index 31b1482970..2617c208bc 100644 --- a/src/commands/manifest/cmd-manifest-conda.mts +++ b/src/commands/manifest/cmd-manifest-conda.mts @@ -2,6 +2,7 @@ import path from 'node:path' import { logger } from '@socketsecurity/registry/lib/logger' +import { findCondaFile } from './detect-manifest-actions.mts' import { handleManifestConda } from './handle-manifest-conda.mts' import constants, { ENVIRONMENT_YAML, @@ -136,7 +137,7 @@ async function run( filename = sockJson.defaults?.manifest?.conda?.infile logger.info(`Using default --file from ${SOCKET_JSON}:`, filename) } else { - filename = ENVIRONMENT_YML + filename = findCondaFile(cwd) || ENVIRONMENT_YML } } if ( diff --git a/src/commands/manifest/detect-manifest-actions.mts b/src/commands/manifest/detect-manifest-actions.mts index e5f65e77f9..4198e17292 100644 --- a/src/commands/manifest/detect-manifest-actions.mts +++ b/src/commands/manifest/detect-manifest-actions.mts @@ -19,11 +19,24 @@ export interface GeneratableManifests { cdxgen: boolean count: number conda: boolean + // The Conda file found at cwd, empty when there is none. + condaFile: string gradle: boolean maven: boolean sbt: boolean } +// Conda reads both spellings of the environment file. +export function findCondaFile(cwd: string): string { + if (existsSync(path.join(cwd, ENVIRONMENT_YML))) { + return ENVIRONMENT_YML + } + if (existsSync(path.join(cwd, ENVIRONMENT_YAML))) { + return ENVIRONMENT_YAML + } + return '' +} + export async function detectManifestActions( // Passing in null means we attempt detection for every supported language // regardless of local socket.json status. Sometimes we want that. @@ -35,6 +48,7 @@ export async function detectManifestActions( cdxgen: false, // TODO count: 0, conda: false, + condaFile: '', gradle: false, maven: false, sbt: false, @@ -102,13 +116,11 @@ export async function detectManifestActions( `[DEBUG] - conda auto-detection is disabled in ${SOCKET_JSON}`, ) } else { - const envyml = path.join(cwd, ENVIRONMENT_YML) - const hasEnvyml = existsSync(envyml) - const envyaml = path.join(cwd, ENVIRONMENT_YAML) - const hasEnvyaml = !hasEnvyml && existsSync(envyaml) - if (hasEnvyml || hasEnvyaml) { - debugLog('notice', '[DEBUG] - Detected an environment.yml Conda file') + const condaFile = findCondaFile(cwd) + if (condaFile) { + debugLog('notice', `[DEBUG] - Detected the Conda file ${condaFile}`) output.conda = true + output.condaFile = condaFile output.count += 1 } } diff --git a/src/commands/manifest/detect-manifest-actions.test.mts b/src/commands/manifest/detect-manifest-actions.test.mts index 2ae961553f..9509162653 100644 --- a/src/commands/manifest/detect-manifest-actions.test.mts +++ b/src/commands/manifest/detect-manifest-actions.test.mts @@ -138,3 +138,58 @@ describe('detectManifestActions — gradle detector', () => { expect(result.count).toBe(0) }) }) + +describe('detectManifestActions — conda detector', () => { + let cwd: string + + beforeEach(() => { + cwd = mkTmp() + }) + + afterEach(() => { + rmSync(cwd, { recursive: true, force: true }) + }) + + it('detects environment.yml and reports it as the conda file', async () => { + touch(cwd, 'environment.yml') + const result = await detectManifestActions(null, cwd) + expect(result.conda).toBe(true) + expect(result.condaFile).toBe('environment.yml') + expect(result.count).toBe(1) + }) + + it('detects environment.yaml and reports it as the conda file', async () => { + touch(cwd, 'environment.yaml') + const result = await detectManifestActions(null, cwd) + expect(result.conda).toBe(true) + expect(result.condaFile).toBe('environment.yaml') + expect(result.count).toBe(1) + }) + + it('prefers environment.yml when both spellings exist', async () => { + touch(cwd, 'environment.yaml') + touch(cwd, 'environment.yml') + const result = await detectManifestActions(null, cwd) + expect(result.condaFile).toBe('environment.yml') + expect(result.count).toBe(1) + }) + + it('reports no conda file when neither spelling exists', async () => { + const result = await detectManifestActions(null, cwd) + expect(result.conda).toBe(false) + expect(result.condaFile).toBe('') + }) + + it('skips conda when defaults.manifest.conda.disabled is true', async () => { + touch(cwd, 'environment.yaml') + const result = await detectManifestActions( + { + defaults: { manifest: { conda: { disabled: true } } }, + } as SocketJson, + cwd, + ) + expect(result.conda).toBe(false) + expect(result.condaFile).toBe('') + expect(result.count).toBe(0) + }) +}) diff --git a/src/commands/manifest/generate_auto_manifest.mts b/src/commands/manifest/generate_auto_manifest.mts index f063e018b2..2c4325bfff 100644 --- a/src/commands/manifest/generate_auto_manifest.mts +++ b/src/commands/manifest/generate_auto_manifest.mts @@ -12,7 +12,11 @@ import { handleManifestConda } from './handle-manifest-conda.mts' import { parseBuildToolOpts } from './parse-build-tool-opts.mts' import { resolveBuildToolBin } from './scripts/build-tool.mts' import { hasSidecarEntries, serializeSidecar } from './scripts/sidecar.mts' -import { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts' +import { + ENVIRONMENT_YML, + REQUIREMENTS_TXT, + SOCKET_JSON, +} from '../../constants.mts' import { InputError } from '../../utils/errors.mts' import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' @@ -194,16 +198,20 @@ export async function generateAutoManifest({ } if (!sockJson?.defaults?.manifest?.conda?.disabled && detected.conda) { - logger.log( - 'Detected an environment.yml file, running default Conda generator...', - ) + const condaFile = + sockJson.defaults?.manifest?.conda?.infile || + detected.condaFile || + ENVIRONMENT_YML + logger.log(`Detected ${condaFile}, running default Conda generator...`) + const beforeExitCode = process.exitCode await handleManifestConda({ cwd, - filename: sockJson.defaults?.manifest?.conda?.infile ?? 'environment.yml', + filename: condaFile, outputKind, out: sockJson.defaults?.manifest?.conda?.outfile ?? REQUIREMENTS_TXT, verbose: Boolean(sockJson.defaults?.manifest?.conda?.verbose), }) + abortManifestRunIfFailed('conda', beforeExitCode) } if (!sockJson?.defaults?.manifest?.bazel?.disabled && detected.bazel) { diff --git a/src/commands/manifest/generate_auto_manifest.test.mts b/src/commands/manifest/generate_auto_manifest.test.mts index fe2e592358..f9bd231357 100644 --- a/src/commands/manifest/generate_auto_manifest.test.mts +++ b/src/commands/manifest/generate_auto_manifest.test.mts @@ -35,6 +35,7 @@ import { extractBazelToMaven } from './bazel/extract_bazel_to_maven.mts' import { convertGradleToFacts } from './convert-gradle-to-facts.mts' import { convertGradleToMaven } from './convert_gradle_to_maven.mts' import { generateAutoManifest } from './generate_auto_manifest.mts' +import { handleManifestConda } from './handle-manifest-conda.mts' import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' import type { SocketJson } from '../../utils/socket-json.mts' @@ -43,8 +44,10 @@ const baseDetected = { bazel: false, cdxgen: false, conda: false, + condaFile: '', count: 0, gradle: false, + maven: false, sbt: false, } @@ -302,3 +305,85 @@ describe('generateAutoManifest — bazel branch', () => { ) }) }) + +describe('generateAutoManifest — conda branch', () => { + beforeEach(() => { + vi.mocked(handleManifestConda).mockClear() + vi.mocked(readOrDefaultSocketJson).mockReturnValue({} as SocketJson) + process.exitCode = undefined + }) + + it('reads the detected environment.yaml rather than the .yml default', async () => { + await generateAutoManifest({ + cwd: '/tmp/repo', + detected: { + ...baseDetected, + conda: true, + condaFile: 'environment.yaml', + count: 1, + }, + outputKind: 'text', + verbose: false, + }) + expect(handleManifestConda).toHaveBeenCalledWith( + expect.objectContaining({ filename: 'environment.yaml' }), + ) + }) + + it('reads the detected environment.yml', async () => { + await generateAutoManifest({ + cwd: '/tmp/repo', + detected: { + ...baseDetected, + conda: true, + condaFile: 'environment.yml', + count: 1, + }, + outputKind: 'text', + verbose: false, + }) + expect(handleManifestConda).toHaveBeenCalledWith( + expect.objectContaining({ filename: 'environment.yml' }), + ) + }) + + it('lets a socket.json infile win over the detected file', async () => { + vi.mocked(readOrDefaultSocketJson).mockReturnValue({ + defaults: { manifest: { conda: { infile: 'conda-env.yml' } } }, + } as SocketJson) + await generateAutoManifest({ + cwd: '/tmp/repo', + detected: { + ...baseDetected, + conda: true, + condaFile: 'environment.yaml', + count: 1, + }, + outputKind: 'text', + verbose: false, + }) + expect(handleManifestConda).toHaveBeenCalledWith( + expect.objectContaining({ filename: 'conda-env.yml' }), + ) + }) + + it('aborts the run when the conda generator fails', async () => { + vi.mocked(handleManifestConda).mockImplementationOnce(async () => { + process.exitCode = 1 + }) + await expect( + generateAutoManifest({ + cwd: '/tmp/repo', + detected: { + ...baseDetected, + conda: true, + condaFile: 'environment.yaml', + count: 1, + }, + outputKind: 'text', + verbose: false, + }), + ).rejects.toThrow(/Auto-manifest generation failed for the conda project/) + process.exitCode = undefined + }) +}) diff --git a/src/commands/manifest/handle-manifest-conda.mts b/src/commands/manifest/handle-manifest-conda.mts index 7b0ef8991c..15608576c3 100644 --- a/src/commands/manifest/handle-manifest-conda.mts +++ b/src/commands/manifest/handle-manifest-conda.mts @@ -1,3 +1,5 @@ +import path from 'node:path' + import { convertCondaToRequirements } from './convert-conda-to-requirements.mts' import { outputRequirements } from './output-requirements.mts' @@ -18,5 +20,11 @@ export async function handleManifestConda({ }): Promise { const data = await convertCondaToRequirements(filename, cwd, verbose) - await outputRequirements(data, outputKind, out) + // --auto-manifest only collects the generated file when it lands inside + // the dir being scanned. + await outputRequirements( + data, + outputKind, + out === '-' ? out : path.resolve(cwd, out), + ) }