Skip to content
Closed
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/commands/manifest/cmd-manifest-conda.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
Expand Down
24 changes: 18 additions & 6 deletions src/commands/manifest/detect-manifest-actions.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -35,6 +48,7 @@ export async function detectManifestActions(
cdxgen: false, // TODO
count: 0,
conda: false,
condaFile: '',
gradle: false,
maven: false,
sbt: false,
Expand Down Expand Up @@ -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
}
}
Expand Down
55 changes: 55 additions & 0 deletions src/commands/manifest/detect-manifest-actions.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
18 changes: 13 additions & 5 deletions src/commands/manifest/generate_auto_manifest.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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) {
Expand Down
85 changes: 85 additions & 0 deletions src/commands/manifest/generate_auto_manifest.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -43,8 +44,10 @@ const baseDetected = {
bazel: false,
cdxgen: false,
conda: false,
condaFile: '',
count: 0,
gradle: false,
maven: false,
sbt: false,
}

Expand Down Expand Up @@ -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
})
})
10 changes: 9 additions & 1 deletion src/commands/manifest/handle-manifest-conda.mts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import path from 'node:path'

import { convertCondaToRequirements } from './convert-conda-to-requirements.mts'
import { outputRequirements } from './output-requirements.mts'

Expand All @@ -18,5 +20,11 @@ export async function handleManifestConda({
}): Promise<void> {
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),
)
}
Loading