diff --git a/cliv2-private/go.mod b/cliv2-private/go.mod index 2ef7f0e48f..8728246c02 100644 --- a/cliv2-private/go.mod +++ b/cliv2-private/go.mod @@ -1,6 +1,6 @@ module github.com/snyk/cli/cliv2-private -go 1.26.5 +go 1.26.6 require ( github.com/snyk/ambient-canary v0.0.0-20260722064253-fba619a134a9 diff --git a/cliv2/go.mod b/cliv2/go.mod index 55abc0d69d..5f5e6ffc9c 100644 --- a/cliv2/go.mod +++ b/cliv2/go.mod @@ -1,6 +1,6 @@ module github.com/snyk/cli/cliv2 -go 1.26.5 +go 1.26.6 require ( github.com/elazarl/goproxy v1.7.2 diff --git a/cliv2/pkg/core/main.go b/cliv2/pkg/core/main.go index bc832ba635..224422330a 100644 --- a/cliv2/pkg/core/main.go +++ b/cliv2/pkg/core/main.go @@ -455,11 +455,41 @@ func doctorTip(isCI bool) string { return "Try snyk doctor: `snyk -d 2>&1 | snyk doctor --stdin`" } +// shouldSuppressDisplay reports whether err is worth printing. +// +// Joined errors match no direct type assertion, so they are unwrapped and checked +// one at a time. +func shouldSuppressDisplay(err error) bool { + if wrappedErr, ok := err.(interface{ Unwrap() []error }); ok { + unwrappedErrs := wrappedErr.Unwrap() + if len(unwrappedErrs) == 0 { + return false + } + + for _, err := range unwrappedErrs { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) && exitErr.ExitCode() < constants.SNYK_EXIT_CODE_ERROR { + return true + } + } + + for _, err := range unwrappedErrs { + if !shouldSuppressDisplay(err) { + return false + } + } + return true + } + + _, isExitError := err.(*exec.ExitError) + _, isErrorWithCode := err.(*cli_errors.ErrorWithExitCode) + + return isExitError || isErrorWithCode || errorHasBeenShown(err) +} + func displayError(err error, userInterface ui.UserInterface, config configuration.Configuration, ctx context.Context, isCI bool) { if err != nil { - _, isExitError := err.(*exec.ExitError) - _, isErrorWithCode := err.(*cli_errors.ErrorWithExitCode) - if isExitError || isErrorWithCode || errorHasBeenShown(err) { + if shouldSuppressDisplay(err) { return } diff --git a/cliv2/pkg/core/main_test.go b/cliv2/pkg/core/main_test.go index 8d10a60ae4..ce57bc3bfe 100644 --- a/cliv2/pkg/core/main_test.go +++ b/cliv2/pkg/core/main_test.go @@ -656,6 +656,50 @@ func Test_displayError(t *testing.T) { }) } +func Test_shouldSuppressDisplay(t *testing.T) { + exitVulnerabilitiesFound := exec.Command("sh", "-c", "exit 1").Run() + require.Error(t, exitVulnerabilitiesFound) + exitFailure := exec.Command("sh", "-c", "exit 2").Run() + require.Error(t, exitFailure) + + authError := func(alreadyShown bool) snyk_errors.Error { + return snyk_errors.Error{ + Title: "Authentication error", + ErrorCode: "SNYK-0005", + Level: "error", + Meta: map[string]any{cliv2.ERROR_HAS_BEEN_DISPLAYED: alreadyShown}, + } + } + + testCases := []struct { + name string + err error + suppress bool + }{ + {"nil", nil, false}, + {"bare exit error", exitVulnerabilitiesFound, true}, + {"error carrying only an exit code", &clierrors.ErrorWithExitCode{ExitCode: 1}, true}, + {"error wrapping an exit error still shows its own message", &wrErr{wraps: exitVulnerabilitiesFound}, false}, + {"plain error", fmt.Errorf("connection refused"), false}, + {"catalog error", authError(false), false}, + + // The command failed: the user needs to know why. + {"failure combined with an error to report", errors.Join(exitFailure, authError(false)), false}, + {"failure combined with an error already reported", errors.Join(exitFailure, authError(true)), true}, + + // The command produced valid output; anything collected is auxiliary and + // printing it would append a second, misleading result. + {"success combined with a collected error", errors.Join(exitVulnerabilitiesFound, fmt.Errorf("connection refused")), true}, + {"success combined with an exit code carrier", errors.Join(exitVulnerabilitiesFound, &clierrors.ErrorWithExitCode{ExitCode: 2}), true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.suppress, shouldSuppressDisplay(tc.err)) + }) + } +} + func Test_doctorTip(t *testing.T) { t.Run("CI advertises the --input flag", func(t *testing.T) { tip := doctorTip(true) diff --git a/package-lock.json b/package-lock.json index a02f6e7eb5..25f438af97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,7 +67,7 @@ "semver": "^6.0.0", "snyk-config": "^5.0.0", "snyk-cpp-plugin": "^2.24.3", - "snyk-docker-plugin": "9.19.0", + "snyk-docker-plugin": "9.20.0", "snyk-go-plugin": "2.2.1", "snyk-gradle-plugin": "7.1.2", "snyk-module": "3.1.0", @@ -19761,9 +19761,9 @@ "license": "ISC" }, "node_modules/snyk-docker-plugin": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/snyk-docker-plugin/-/snyk-docker-plugin-9.19.0.tgz", - "integrity": "sha512-XVOEneK1YKfFxOqyUR8PRBHzxosRNnfEuejNZHIKUqRdFrJjfVzxPRNo5a2ypis8z26C0/Hgha18nCwitvejRA==", + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/snyk-docker-plugin/-/snyk-docker-plugin-9.20.0.tgz", + "integrity": "sha512-wzvEK7aO7od4/bp1ZXivQzzMJNRYSX9s3DY+sCB5wCw/MVx93R2fNcTqXt3HtObJYPSUtEm5pA8nYVSG77NqYw==", "license": "Apache-2.0", "dependencies": { "@snyk/composer-lockfile-parser": "^1.4.1", diff --git a/package.json b/package.json index 4dc2c88bca..cdb8e7b953 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "semver": "^6.0.0", "snyk-config": "^5.0.0", "snyk-cpp-plugin": "^2.24.3", - "snyk-docker-plugin": "9.19.0", + "snyk-docker-plugin": "9.20.0", "snyk-go-plugin": "2.2.1", "snyk-gradle-plugin": "7.1.2", "snyk-module": "3.1.0",