From 4c0ddd33dc8926241e160d7891b07c11bddd349c Mon Sep 17 00:00:00 2001 From: snowyukitty <270071858+snowyukitty@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:56:29 +0900 Subject: [PATCH 1/2] Fix error output in JSON mode Write fatal errors directly to stderr when JSON output is enabled so os.Exit does not discard the buffered message. Keep stdout empty and preserve the configured UI for non-JSON errors. Add a subprocess regression test covering streams and exit status. Fixes #455 Signed-off-by: snowyukitty <270071858+snowyukitty@users.noreply.github.com> --- cmd/kbld/kbld.go | 11 +++++++-- cmd/kbld/kbld_test.go | 52 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 cmd/kbld/kbld_test.go diff --git a/cmd/kbld/kbld.go b/cmd/kbld/kbld.go index f668df85..59bc2cd4 100644 --- a/cmd/kbld/kbld.go +++ b/cmd/kbld/kbld.go @@ -4,6 +4,7 @@ package main import ( + "fmt" "io" "log" "math/rand" @@ -26,11 +27,17 @@ func main() { confUI := ui.NewConfUI(ui.NewNoopLogger()) defer confUI.Flush() - command := cmd.NewDefaultKbldCmd(confUI) + options := cmd.NewKbldOptions(confUI) + command := cmd.NewKbldCmd(options) err := command.Execute() if err != nil { - confUI.ErrorLinef("kbld: Error: %s", uierrs.NewMultiLineError(err)) + multiLineErr := uierrs.NewMultiLineError(err) + if options.UIFlags.JSON { + _, _ = fmt.Fprintf(os.Stderr, "kbld: Error: %s\n", multiLineErr) + } else { + confUI.ErrorLinef("kbld: Error: %s", multiLineErr) + } os.Exit(1) } diff --git a/cmd/kbld/kbld_test.go b/cmd/kbld/kbld_test.go new file mode 100644 index 00000000..02b3c687 --- /dev/null +++ b/cmd/kbld/kbld_test.go @@ -0,0 +1,52 @@ +// Copyright 2026 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const failureExitCode = 1 + +func TestJSONErrorsAreWrittenToStderr(t *testing.T) { + cmd := exec.Command(os.Args[0], "-test.run=^TestKbldProcess$", "--", + "--json", "-f", filepath.Join(t.TempDir(), "missing.yml")) + cmd.Env = append(os.Environ(), "KBLD_TEST_PROCESS=1") + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + var exitErr *exec.ExitError + require.ErrorAs(t, err, &exitErr) + assert.Equal(t, failureExitCode, exitErr.ExitCode()) + assert.Empty(t, stdout.String()) + assert.True(t, strings.HasPrefix( + stderr.String(), "kbld: Error: Unable to stat file:")) +} + +func TestKbldProcess(t *testing.T) { + if os.Getenv("KBLD_TEST_PROCESS") != "1" { + return + } + + for i, arg := range os.Args { + if arg == "--" { + os.Args = append([]string{"kbld"}, os.Args[i+1:]...) + main() + return + } + } + + t.Fatal("missing command separator") +} From 90137df517ee6b4b795aff394087c3fddf22f433 Mon Sep 17 00:00:00 2001 From: snowyukitty <270071858+snowyukitty@users.noreply.github.com> Date: Fri, 14 Aug 2026 10:05:45 +0900 Subject: [PATCH 2/2] Move JSON error regression to e2e tests Signed-off-by: snowyukitty <270071858+snowyukitty@users.noreply.github.com> --- cmd/kbld/kbld_test.go | 52 ---------------------------------------- test/e2e/resolve_test.go | 14 +++++++---- 2 files changed, 9 insertions(+), 57 deletions(-) delete mode 100644 cmd/kbld/kbld_test.go diff --git a/cmd/kbld/kbld_test.go b/cmd/kbld/kbld_test.go deleted file mode 100644 index 02b3c687..00000000 --- a/cmd/kbld/kbld_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2026 The Carvel Authors. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "bytes" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const failureExitCode = 1 - -func TestJSONErrorsAreWrittenToStderr(t *testing.T) { - cmd := exec.Command(os.Args[0], "-test.run=^TestKbldProcess$", "--", - "--json", "-f", filepath.Join(t.TempDir(), "missing.yml")) - cmd.Env = append(os.Environ(), "KBLD_TEST_PROCESS=1") - - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - err := cmd.Run() - var exitErr *exec.ExitError - require.ErrorAs(t, err, &exitErr) - assert.Equal(t, failureExitCode, exitErr.ExitCode()) - assert.Empty(t, stdout.String()) - assert.True(t, strings.HasPrefix( - stderr.String(), "kbld: Error: Unable to stat file:")) -} - -func TestKbldProcess(t *testing.T) { - if os.Getenv("KBLD_TEST_PROCESS") != "1" { - return - } - - for i, arg := range os.Args { - if arg == "--" { - os.Args = append([]string{"kbld"}, os.Args[i+1:]...) - main() - return - } - } - - t.Fatal("missing command separator") -} diff --git a/test/e2e/resolve_test.go b/test/e2e/resolve_test.go index ad2f80da..f905dc79 100644 --- a/test/e2e/resolve_test.go +++ b/test/e2e/resolve_test.go @@ -6,6 +6,7 @@ package e2e import ( + "bytes" "io/ioutil" "os" "strings" @@ -156,13 +157,16 @@ spec: - image: nginx@sha256:digest ` - _, err := kbld.RunWithOpts([]string{"-f", "-", "--images-annotation=false"}, RunOpts{ - StdinReader: strings.NewReader(input), - AllowError: true, + var stderr bytes.Buffer + out, err := kbld.RunWithOpts([]string{"--json", "-f", "-", "--images-annotation=false"}, RunOpts{ + StdinReader: strings.NewReader(input), + StderrWriter: &stderr, + AllowError: true, }) - expectedErr := "Expected valid digest reference, but found 'nginx@sha256:digest', reason: invalid checksum digest length" - require.Contains(t, err.Error(), expectedErr) + require.EqualError(t, err, "Execution error: stdout: '' stderr: '' error: 'exit status 1'") + require.Empty(t, out) + require.Equal(t, "kbld: Error: \n- Resolving image 'nginx@sha256:digest': Expected valid digest reference, but found 'nginx@sha256:digest', reason: invalid checksum digest length\n", stderr.String()) } func TestResolveUnknownImage(t *testing.T) {