From fe8f0760cda62b3721ce1aa4e68adcf8fdd294ef Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Thu, 11 Jun 2026 15:03:11 +0200 Subject: [PATCH 1/3] feat(scan): forward socket.json build-tool config into reachability socket scan create --reach now maps socket.json's per-ecosystem manifest build-tool options (bin, include/exclude-configs, gradle/sbt opts) into a Coana-defined AutoManifestConfig and passes it to `coana run` via --auto-manifest-config (a temp JSON file path Coana reads), so reach-time dependency resolution invokes the build tool the way the project is configured rather than with defaults. Under --auto-manifest the config also carries top-level failOnBuildToolError=true (fail-closed: Coana treats a build-tool step failure as fatal instead of tolerating it); plain --reach leaves it unset and stays permissive. This is the socket-cli side of the manifest-flag-propagation gap. The Coana `--auto-manifest-config` option is not yet released, so this must not ship until Coana publishes it and the pinned @coana-tech/cli is bumped; until then it is exercised via SOCKET_CLI_COANA_LOCAL_PATH. - add src/utils/auto-manifest-config.mts: BuildToolOptions/AutoManifestConfig types + buildAutoManifestConfig (socket.json -> config) + tests - ReachabilityOptions.autoManifestConfig; write the config to a temp file and pass its path to coana run, cleaning it up after - build the config at the cmd-scan-create assembly point --- src/commands/scan/cmd-scan-create.mts | 10 ++ .../scan/perform-reachability-analysis.mts | 39 +++++++ src/utils/auto-manifest-config.mts | 110 ++++++++++++++++++ src/utils/auto-manifest-config.test.mts | 95 +++++++++++++++ 4 files changed, 254 insertions(+) create mode 100644 src/utils/auto-manifest-config.mts create mode 100644 src/utils/auto-manifest-config.test.mts diff --git a/src/commands/scan/cmd-scan-create.mts b/src/commands/scan/cmd-scan-create.mts index db404f4a9..4221a52b6 100644 --- a/src/commands/scan/cmd-scan-create.mts +++ b/src/commands/scan/cmd-scan-create.mts @@ -13,6 +13,7 @@ import { suggestTarget } from './suggest_target.mts' import { validateReachabilityTarget } from './validate-reachability-target.mts' import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' +import { buildAutoManifestConfig } from '../../utils/auto-manifest-config.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { cmdFlagValueToArray } from '../../utils/cmd.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' @@ -622,6 +623,15 @@ async function run( pendingHead: Boolean(pendingHead), pullRequest: Number(pullRequest), reach: { + // Build-tool config for the reach-time resolution, mapped from socket.json + // (per-ecosystem). Best-effort on plain --reach; under --auto-manifest the + // config carries top-level failOnBuildToolError=true (fail-closed). Only + // built when reachability runs. + autoManifestConfig: reach + ? buildAutoManifestConfig(sockJson, { + autoManifest: Boolean(autoManifest), + }) + : undefined, excludePaths, reachAnalysisMemoryLimit: Number(reachAnalysisMemoryLimit), reachAnalysisTimeout: Number(reachAnalysisTimeout), diff --git a/src/commands/scan/perform-reachability-analysis.mts b/src/commands/scan/perform-reachability-analysis.mts index 0c623dab9..267a4186b 100644 --- a/src/commands/scan/perform-reachability-analysis.mts +++ b/src/commands/scan/perform-reachability-analysis.mts @@ -1,9 +1,13 @@ +import { randomUUID } from 'node:crypto' +import { promises as fs } from 'node:fs' +import { tmpdir } from 'node:os' import path from 'node:path' import { logger } from '@socketsecurity/registry/lib/logger' import constants from '../../constants.mts' import { handleApiCall } from '../../utils/api.mts' +import { isAutoManifestConfigEmpty } from '../../utils/auto-manifest-config.mts' import { extractTier1ReachabilityScanId } from '../../utils/coana.mts' import { spawnCoanaDlx } from '../../utils/dlx.mts' import { hasEnterpriseOrgPlan } from '../../utils/organization.mts' @@ -12,10 +16,12 @@ import { socketDevLink } from '../../utils/terminal-link.mts' import { fetchOrganization } from '../organization/fetch-organization-list.mts' import type { CResult } from '../../types.mts' +import type { AutoManifestConfig } from '../../utils/auto-manifest-config.mts' import type { PURL_Type } from '../../utils/ecosystem.mts' import type { Spinner } from '@socketsecurity/registry/lib/spinner' export type ReachabilityOptions = { + autoManifestConfig?: AutoManifestConfig | undefined excludePaths: string[] reachAnalysisMemoryLimit: number reachAnalysisTimeout: number @@ -170,6 +176,24 @@ export async function performReachabilityAnalysis( spinner?.infoAndStop('Running reachability analysis with Coana...') const outputFilePath = outputPath || constants.DOT_SOCKET_DOT_FACTS_JSON + + // Coana reads `--auto-manifest-config` from a JSON file, so write the resolved + // per-ecosystem build-tool config (mapped from socket.json) to a temp file and + // pass its absolute path. Cleaned up right after the run below. + let autoManifestConfigPath: string | undefined + const { autoManifestConfig } = reachabilityOptions + if (autoManifestConfig && !isAutoManifestConfigEmpty(autoManifestConfig)) { + autoManifestConfigPath = path.join( + tmpdir(), + `socket-auto-manifest-config-${randomUUID()}.json`, + ) + await fs.writeFile( + autoManifestConfigPath, + JSON.stringify(autoManifestConfig), + 'utf8', + ) + } + // Build Coana arguments. const coanaArgs = [ 'run', @@ -228,6 +252,12 @@ export async function performReachabilityAnalysis( ...(reachabilityOptions.reachUseOnlyPregeneratedSboms ? ['--use-only-pregenerated-sboms'] : []), + // Hand the per-ecosystem build-tool config (mapped from socket.json) to + // Coana's reach-time resolution, as a temp JSON file path. Coana side: + // REA-547. + ...(autoManifestConfigPath + ? ['--auto-manifest-config', autoManifestConfigPath] + : []), ] // Build environment variables. @@ -250,6 +280,15 @@ export async function performReachabilityAnalysis( stdio: 'inherit', }) + // The run no longer needs the temp config file; best-effort cleanup. + if (autoManifestConfigPath) { + try { + await fs.unlink(autoManifestConfigPath) + } catch { + // File may already be gone or unwritable. + } + } + if (wasSpinning) { spinner.start() } diff --git a/src/utils/auto-manifest-config.mts b/src/utils/auto-manifest-config.mts new file mode 100644 index 000000000..6a911107e --- /dev/null +++ b/src/utils/auto-manifest-config.mts @@ -0,0 +1,110 @@ +import type { SocketJson } from './socket-json.mts' + +// Per-ecosystem build-tool options handed off to the Coana CLI — used both when +// generating manifests (`coana manifest `) and, in socket mode, for +// reach-time dependency resolution (`coana run`). This mirrors the Coana-side +// `--auto-manifest-config` shape (REA-547): socket-cli owns mapping `socket.json` +// onto it, so Coana stays uncoupled from `socket.json`'s schema. Keeping the +// per-ecosystem options namespaced (rather than as flat CLI flags) avoids the +// ambiguity of a bare `--bin`/`--include-configs` when a repo has more than one +// build tool. +export type BuildToolOptions = { + // Build-tool executable override (e.g. `./gradlew`, `atlas-mvn`). + bin?: string | undefined + // Comma-separated config-name globs to skip. + excludeConfigs?: string | undefined + // `socket.json`'s per-ecosystem `ignoreUnresolved` (warn vs fail on unresolved + // dependencies), forwarded verbatim. NOTE: this is NOT the reach-time + // fail-closed switch — that's the run-wide `failOnBuildToolError` below. + ignoreUnresolved?: boolean | undefined + // Comma-separated config-name globs to resolve. + includeConfigs?: string | undefined + // Extra build-tool options, pre-split into argv. Coana maps these straight to + // the tool's opts (no splitting on its side). Mapped from `socket.json`'s + // `gradleOpts`/`sbtOpts` string. + opts?: string[] | undefined +} + +// The Coana hand-off config. `failOnBuildToolError` is run-wide (top level) +// because `--auto-manifest` is a single CLI mode, not a per-package-manager +// setting. The per-ecosystem entries are present only for ecosystems configured +// (and not disabled) in `socket.json`; absent ecosystems fall to Coana's own +// defaults. +export type AutoManifestConfig = { + // Run-wide fail-closed switch. When true, Coana treats a build-tool step + // failure as fatal rather than tolerating it. socket-cli sets it true under + // `--auto-manifest`; left unset on plain `--reach` (permissive — Coana's + // default best-effort behaviour). + failOnBuildToolError?: boolean | undefined + gradle?: BuildToolOptions | undefined + sbt?: BuildToolOptions | undefined +} + +// Splits a `socket.json` opts string (`gradleOpts`/`sbtOpts`) into argv, matching +// how the standalone `socket manifest` path splits it. Returns undefined when +// there's nothing to pass so the field is omitted from the config. +function parseOpts(value: string | undefined): string[] | undefined { + if (!value) { + return undefined + } + const parts = value + .split(' ') + .map(s => s.trim()) + .filter(Boolean) + return parts.length ? parts : undefined +} + +// Maps `socket.json`'s `defaults.manifest.` build-tool options onto +// the Coana hand-off config. +// +// `autoManifest` reflects whether the run is `--auto-manifest` (fail-closed: +// `failOnBuildToolError=true`) vs plain `--reach` (permissive: +// `failOnBuildToolError` left unset so Coana's default applies). Per-ecosystem +// options are forwarded verbatim from `socket.json`; disabled ecosystems are +// omitted so they fall back to Coana's defaults. +export function buildAutoManifestConfig( + sockJson: SocketJson, + { autoManifest }: { autoManifest: boolean }, +): AutoManifestConfig { + const manifest = sockJson.defaults?.manifest + const config: AutoManifestConfig = {} + + // `--auto-manifest` expects every build-tool command to succeed, so a + // build-tool step failure should be fatal rather than tolerated. + if (autoManifest) { + config.failOnBuildToolError = true + } + + const gradle = manifest?.gradle + if (gradle && !gradle.disabled) { + config.gradle = { + bin: gradle.bin, + excludeConfigs: gradle.excludeConfigs, + ignoreUnresolved: gradle.ignoreUnresolved, + includeConfigs: gradle.includeConfigs, + opts: parseOpts(gradle.gradleOpts), + } + } + + const sbt = manifest?.sbt + if (sbt && !sbt.disabled) { + config.sbt = { + bin: sbt.bin, + excludeConfigs: sbt.excludeConfigs, + ignoreUnresolved: sbt.ignoreUnresolved, + includeConfigs: sbt.includeConfigs, + opts: parseOpts(sbt.sbtOpts), + } + } + + return config +} + +// True when there's nothing to hand to Coana: no per-ecosystem options and the +// run mode is left at Coana's permissive default. When true, the +// `--auto-manifest-config` option should be omitted entirely. +export function isAutoManifestConfigEmpty(config: AutoManifestConfig): boolean { + return ( + !config.gradle && !config.sbt && config.failOnBuildToolError === undefined + ) +} diff --git a/src/utils/auto-manifest-config.test.mts b/src/utils/auto-manifest-config.test.mts new file mode 100644 index 000000000..f0ffdc600 --- /dev/null +++ b/src/utils/auto-manifest-config.test.mts @@ -0,0 +1,95 @@ +import { describe, expect, it } from 'vitest' + +import { + buildAutoManifestConfig, + isAutoManifestConfigEmpty, +} from './auto-manifest-config.mts' + +import type { SocketJson } from './socket-json.mts' + +// Builds a minimal SocketJson for the mapping under test; only +// `defaults.manifest` is read, so the header/version fields are irrelevant. +function socketJson( + manifest?: NonNullable['manifest']>, +): SocketJson { + return { defaults: { manifest } } as SocketJson +} + +describe('buildAutoManifestConfig', () => { + it('returns an empty config for plain --reach with no manifest defaults', () => { + expect( + buildAutoManifestConfig(socketJson(), { autoManifest: false }), + ).toEqual({}) + }) + + it('sets top-level failOnBuildToolError=true under --auto-manifest (fail-closed)', () => { + expect( + buildAutoManifestConfig(socketJson(), { autoManifest: true }), + ).toEqual({ failOnBuildToolError: true }) + }) + + it('leaves failOnBuildToolError unset on plain --reach (Coana default permissive)', () => { + const config = buildAutoManifestConfig( + socketJson({ gradle: { bin: './gradlew' } }), + { autoManifest: false }, + ) + expect(config.failOnBuildToolError).toBeUndefined() + }) + + it('maps gradle/sbt options, *Opts -> opts, ignoreUnresolved passthrough', () => { + const config = buildAutoManifestConfig( + socketJson({ + gradle: { + bin: './gradlew', + excludeConfigs: 'testCompileClasspath', + gradleOpts: '--offline --no-daemon', + ignoreUnresolved: true, + includeConfigs: '*RuntimeClasspath', + }, + sbt: { bin: 'sbt', sbtOpts: '-batch' }, + }), + { autoManifest: true }, + ) + expect(config).toEqual({ + failOnBuildToolError: true, + gradle: { + bin: './gradlew', + excludeConfigs: 'testCompileClasspath', + ignoreUnresolved: true, + includeConfigs: '*RuntimeClasspath', + opts: ['--offline', '--no-daemon'], + }, + sbt: { bin: 'sbt', opts: ['-batch'] }, + }) + }) + + it('omits disabled ecosystems so they fall back to Coana defaults', () => { + const config = buildAutoManifestConfig( + socketJson({ + gradle: { disabled: true, includeConfigs: '*RuntimeClasspath' }, + sbt: { bin: 'sbt' }, + }), + { autoManifest: false }, + ) + expect(config.gradle).toBeUndefined() + expect(config.sbt).toBeDefined() + }) +}) + +describe('isAutoManifestConfigEmpty', () => { + it('is true when there are no ecosystems and the run mode is default', () => { + expect(isAutoManifestConfigEmpty({})).toBe(true) + }) + + it('is false when failOnBuildToolError is set (fail-closed must reach Coana)', () => { + expect(isAutoManifestConfigEmpty({ failOnBuildToolError: true })).toBe( + false, + ) + }) + + it('is false when an ecosystem is configured', () => { + expect(isAutoManifestConfigEmpty({ gradle: { bin: './gradlew' } })).toBe( + false, + ) + }) +}) From 478fb22587ad32c8d1270220c656a9a91a06f42e Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Fri, 12 Jun 2026 14:40:43 +0200 Subject: [PATCH 2/3] =?UTF-8?q?chore(release):=201.1.120=20=E2=80=94=20Coa?= =?UTF-8?q?na=2015.4.1=20and=20socket.json=20build-tool=20config=20forward?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump @coana-tech/cli to 15.4.1 (which ships the --auto-manifest-config option the feat commit depends on), bump the package version to 1.1.120, and add the changelog entry. --- CHANGELOG.md | 7 +++++++ package.json | 4 ++-- pnpm-lock.yaml | 10 +++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bc540168..63e031ade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ 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/). +## [1.1.120](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.120) - 2026-06-12 + +### Changed +- `socket scan create --reach` now applies your project's build-tool settings from `socket.json` (configured via `socket manifest setup`) — custom build-tool binary, include/exclude configs, and Gradle/sbt options — when resolving dependencies for Gradle and sbt reachability analysis, instead of always invoking the build tool with defaults. +- `socket scan create --auto-manifest --reach` now fails with an error when a build tool fails during manifest generation, rather than tolerating it. Plain `--reach` (without `--auto-manifest`) keeps generating manifests on a best-effort basis. +- Updated the Coana CLI to v `15.4.1`. + ## [1.1.119](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.119) - 2026-06-11 ### Changed diff --git a/package.json b/package.json index 82975296c..b42947fbe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket", - "version": "1.1.119", + "version": "1.1.120", "description": "CLI for Socket.dev", "homepage": "https://github.com/SocketDev/socket-cli", "license": "MIT", @@ -96,7 +96,7 @@ "@babel/preset-typescript": "7.27.1", "@babel/runtime": "7.28.4", "@biomejs/biome": "2.2.4", - "@coana-tech/cli": "15.3.26", + "@coana-tech/cli": "15.4.1", "@cyclonedx/cdxgen": "12.1.2", "@dotenvx/dotenvx": "1.49.0", "@eslint/compat": "1.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d3197590..5960aad92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,8 +128,8 @@ importers: specifier: 2.2.4 version: 2.2.4 '@coana-tech/cli': - specifier: 15.3.26 - version: 15.3.26 + specifier: 15.4.1 + version: 15.4.1 '@cyclonedx/cdxgen': specifier: 12.1.2 version: 12.1.2 @@ -749,8 +749,8 @@ packages: resolution: {integrity: sha512-hAs5PPKPCQ3/Nha+1fo4A4/gL85fIfxZwHPehsjCJ+BhQH2/yw6/xReuaPA/RfNQr6iz1PcD7BZcE3ctyyl3EA==} cpu: [x64] - '@coana-tech/cli@15.3.26': - resolution: {integrity: sha512-l7Jnto1dCOUKVSxdzfJhmv+6VHi57b00z5IjTda45XxbSGVirFJUYxdhNMXjijYYQHc1bUf0b1Nh9VNWhZryug==} + '@coana-tech/cli@15.4.1': + resolution: {integrity: sha512-JvOz3ST9yN+DBWve92JaFt7zeWLEq0X94G5lSo4sNePlZLyfLwOEMnsGTcPBYsJ7SNIwBxwgqkm8/JfczWviqw==} hasBin: true '@colors/colors@1.5.0': @@ -5385,7 +5385,7 @@ snapshots: '@cdxgen/cdxgen-plugins-bin@2.0.2': optional: true - '@coana-tech/cli@15.3.26': {} + '@coana-tech/cli@15.4.1': {} '@colors/colors@1.5.0': optional: true From 7e4cd7aebc88dadb692a220758d8cd62cdea9b9b Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Mon, 15 Jun 2026 09:29:29 +0200 Subject: [PATCH 3/3] fix(scan): gate --auto-manifest-config on Coana version support Only forward the socket.json build-tool config to `coana run` when the resolved Coana version supports `--auto-manifest-config` (>= 15.4.1). Passing the flag to an older Coana, pinned via --reach-version, would abort the run on an unknown flag; we now skip it and warn instead. A local Coana build (SOCKET_CLI_COANA_LOCAL_PATH) has no resolvable version and is treated as supported. Also drop internal tracker references from source comments. --- .../ci/fetch-default-org-slug.test.mts | 2 +- .../scan/perform-reachability-analysis.mts | 44 ++++++++++++++----- src/utils/auto-manifest-config.mts | 24 +++++++++- src/utils/auto-manifest-config.test.mts | 27 ++++++++++++ 4 files changed, 82 insertions(+), 15 deletions(-) diff --git a/src/commands/ci/fetch-default-org-slug.test.mts b/src/commands/ci/fetch-default-org-slug.test.mts index 36ef05ac4..0844b0644 100644 --- a/src/commands/ci/fetch-default-org-slug.test.mts +++ b/src/commands/ci/fetch-default-org-slug.test.mts @@ -1,8 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultOrgSlug } from './fetch-default-org-slug.mts' -import { fetchOrganization } from '../organization/fetch-organization-list.mts' import { getConfigValueOrUndef } from '../../utils/config.mts' +import { fetchOrganization } from '../organization/fetch-organization-list.mts' vi.mock('../organization/fetch-organization-list.mts', () => ({ fetchOrganization: vi.fn(), diff --git a/src/commands/scan/perform-reachability-analysis.mts b/src/commands/scan/perform-reachability-analysis.mts index 267a4186b..d91fa65a7 100644 --- a/src/commands/scan/perform-reachability-analysis.mts +++ b/src/commands/scan/perform-reachability-analysis.mts @@ -7,7 +7,11 @@ import { logger } from '@socketsecurity/registry/lib/logger' import constants from '../../constants.mts' import { handleApiCall } from '../../utils/api.mts' -import { isAutoManifestConfigEmpty } from '../../utils/auto-manifest-config.mts' +import { + AUTO_MANIFEST_CONFIG_MIN_COANA_VERSION, + coanaSupportsAutoManifestConfig, + isAutoManifestConfigEmpty, +} from '../../utils/auto-manifest-config.mts' import { extractTier1ReachabilityScanId } from '../../utils/coana.mts' import { spawnCoanaDlx } from '../../utils/dlx.mts' import { hasEnterpriseOrgPlan } from '../../utils/organization.mts' @@ -183,15 +187,32 @@ export async function performReachabilityAnalysis( let autoManifestConfigPath: string | undefined const { autoManifestConfig } = reachabilityOptions if (autoManifestConfig && !isAutoManifestConfigEmpty(autoManifestConfig)) { - autoManifestConfigPath = path.join( - tmpdir(), - `socket-auto-manifest-config-${randomUUID()}.json`, - ) - await fs.writeFile( - autoManifestConfigPath, - JSON.stringify(autoManifestConfig), - 'utf8', - ) + // A local Coana build (SOCKET_CLI_COANA_LOCAL_PATH) has no resolvable version + // and is a developer opt-in, so assume it supports the flag. Otherwise gate + // on the resolved version understanding `--auto-manifest-config`; passing it + // to an older Coana would abort the run on an unknown flag. + const usingLocalCoana = !!process.env['SOCKET_CLI_COANA_LOCAL_PATH'] + const resolvedCoanaVersion = + reachabilityOptions.reachVersion || + constants.ENV.INLINED_SOCKET_CLI_COANA_TECH_CLI_VERSION + if ( + usingLocalCoana || + coanaSupportsAutoManifestConfig(resolvedCoanaVersion) + ) { + autoManifestConfigPath = path.join( + tmpdir(), + `socket-auto-manifest-config-${randomUUID()}.json`, + ) + await fs.writeFile( + autoManifestConfigPath, + JSON.stringify(autoManifestConfig), + 'utf8', + ) + } else { + logger.warn( + `Ignoring socket.json build-tool config for reachability: Coana ${resolvedCoanaVersion} does not support --auto-manifest-config (requires >= ${AUTO_MANIFEST_CONFIG_MIN_COANA_VERSION}).`, + ) + } } // Build Coana arguments. @@ -253,8 +274,7 @@ export async function performReachabilityAnalysis( ? ['--use-only-pregenerated-sboms'] : []), // Hand the per-ecosystem build-tool config (mapped from socket.json) to - // Coana's reach-time resolution, as a temp JSON file path. Coana side: - // REA-547. + // Coana's reach-time resolution, as a temp JSON file path. ...(autoManifestConfigPath ? ['--auto-manifest-config', autoManifestConfigPath] : []), diff --git a/src/utils/auto-manifest-config.mts b/src/utils/auto-manifest-config.mts index 6a911107e..accfb6f69 100644 --- a/src/utils/auto-manifest-config.mts +++ b/src/utils/auto-manifest-config.mts @@ -1,10 +1,17 @@ +import semver from 'semver' + import type { SocketJson } from './socket-json.mts' +// Coana gained the `--auto-manifest-config` option in this version. Older Coana +// builds reject the unknown flag and abort the run, so callers must not forward +// the config to a Coana older than this. +export const AUTO_MANIFEST_CONFIG_MIN_COANA_VERSION = '15.4.1' + // Per-ecosystem build-tool options handed off to the Coana CLI — used both when // generating manifests (`coana manifest `) and, in socket mode, for // reach-time dependency resolution (`coana run`). This mirrors the Coana-side -// `--auto-manifest-config` shape (REA-547): socket-cli owns mapping `socket.json` -// onto it, so Coana stays uncoupled from `socket.json`'s schema. Keeping the +// `--auto-manifest-config` shape: socket-cli owns mapping `socket.json` onto it, +// so Coana stays uncoupled from `socket.json`'s schema. Keeping the // per-ecosystem options namespaced (rather than as flat CLI flags) avoids the // ambiguity of a bare `--bin`/`--include-configs` when a repo has more than one // build tool. @@ -100,6 +107,19 @@ export function buildAutoManifestConfig( return config } +// Whether a resolved Coana version understands `--auto-manifest-config`. An +// unparseable version (e.g. a git ref or custom build tag) is treated as +// supported so explicit overrides aren't second-guessed; callers gate local +// Coana builds (which have no resolvable version) separately. +export function coanaSupportsAutoManifestConfig( + version: string | undefined, +): boolean { + const coerced = version ? semver.coerce(version) : undefined + return coerced + ? semver.gte(coerced, AUTO_MANIFEST_CONFIG_MIN_COANA_VERSION) + : true +} + // True when there's nothing to hand to Coana: no per-ecosystem options and the // run mode is left at Coana's permissive default. When true, the // `--auto-manifest-config` option should be omitted entirely. diff --git a/src/utils/auto-manifest-config.test.mts b/src/utils/auto-manifest-config.test.mts index f0ffdc600..8c601b881 100644 --- a/src/utils/auto-manifest-config.test.mts +++ b/src/utils/auto-manifest-config.test.mts @@ -1,7 +1,9 @@ import { describe, expect, it } from 'vitest' import { + AUTO_MANIFEST_CONFIG_MIN_COANA_VERSION, buildAutoManifestConfig, + coanaSupportsAutoManifestConfig, isAutoManifestConfigEmpty, } from './auto-manifest-config.mts' @@ -76,6 +78,31 @@ describe('buildAutoManifestConfig', () => { }) }) +describe('coanaSupportsAutoManifestConfig', () => { + it('supports the minimum version', () => { + expect( + coanaSupportsAutoManifestConfig(AUTO_MANIFEST_CONFIG_MIN_COANA_VERSION), + ).toBe(true) + }) + + it('supports versions newer than the minimum', () => { + expect(coanaSupportsAutoManifestConfig('15.5.0')).toBe(true) + expect(coanaSupportsAutoManifestConfig('16.0.0')).toBe(true) + }) + + it('does not support versions older than the minimum', () => { + expect(coanaSupportsAutoManifestConfig('15.3.26')).toBe(false) + expect(coanaSupportsAutoManifestConfig('15.4.0')).toBe(false) + expect(coanaSupportsAutoManifestConfig('14.12.222')).toBe(false) + }) + + it('treats unparseable or missing versions as supported (no second-guessing overrides)', () => { + expect(coanaSupportsAutoManifestConfig(undefined)).toBe(true) + expect(coanaSupportsAutoManifestConfig('')).toBe(true) + expect(coanaSupportsAutoManifestConfig('main')).toBe(true) + }) +}) + describe('isAutoManifestConfigEmpty', () => { it('is true when there are no ecosystems and the run mode is default', () => { expect(isAutoManifestConfigEmpty({})).toBe(true)