From ce0e8fe9cd77cbb3af0a08ddbc5916eebcc711b4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:35:01 +0200 Subject: [PATCH] fix(version): write update notice to stderr to preserve JSON stdout Redirect the version update message from stdout to stderr so command JSON output stays machine-readable. Expand checker tests with shared stdout/stderr capture helpers and assertions to verify: - update notices are emitted on stderr only - stdout remains clean during checks and errors - JSON output to stdout is unaffected when an update is available --- internal/version/checker.go | 3 +- internal/version/checker_test.go | 174 ++++++++++++++++++++----------- 2 files changed, 115 insertions(+), 62 deletions(-) diff --git a/internal/version/checker.go b/internal/version/checker.go index f1d75c3..fcda6a5 100644 --- a/internal/version/checker.go +++ b/internal/version/checker.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "os" "sort" "time" @@ -101,7 +102,7 @@ func CheckLatestVersionOfCli(_ bool) (string, error) { } if latestVersion.GreaterThan(currentVersion) { - fmt.Printf("A new version (%s) is available. Update with: coolify update\n", latestVersion.String()) + _, _ = fmt.Fprintf(os.Stderr, "A new version (%s) is available. Update with: coolify update\n", latestVersion.String()) } return latestVersion.String(), nil } diff --git a/internal/version/checker_test.go b/internal/version/checker_test.go index 101de7b..861c923 100644 --- a/internal/version/checker_test.go +++ b/internal/version/checker_test.go @@ -9,6 +9,39 @@ import ( "testing" ) +func captureOutput(t *testing.T, fn func()) (string, string) { + t.Helper() + + oldStdout := os.Stdout + oldStderr := os.Stderr + + stdoutR, stdoutW, err := os.Pipe() + if err != nil { + t.Fatalf("os.Pipe() stdout error = %v", err) + } + stderrR, stderrW, err := os.Pipe() + if err != nil { + t.Fatalf("os.Pipe() stderr error = %v", err) + } + + os.Stdout = stdoutW + os.Stderr = stderrW + + fn() + + _ = stdoutW.Close() + _ = stderrW.Close() + os.Stdout = oldStdout + os.Stderr = oldStderr + + var stdoutBuf bytes.Buffer + var stderrBuf bytes.Buffer + _, _ = io.Copy(&stdoutBuf, stdoutR) + _, _ = io.Copy(&stderrBuf, stderrR) + + return stdoutBuf.String(), stderrBuf.String() +} + func TestGetVersion(t *testing.T) { v := GetVersion() if v == "" { @@ -42,19 +75,11 @@ func TestCheckLatestVersionOfCli_UpdateAvailable(t *testing.T) { GitHubAPIURL = server.URL - // Capture stdout to check for update message - oldStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - latestVersion, err := CheckLatestVersionOfCli(false) - - _ = w.Close() - os.Stdout = oldStdout - - var buf bytes.Buffer - _, _ = io.Copy(&buf, r) - output := buf.String() + var latestVersion string + var err error + stdout, stderr := captureOutput(t, func() { + latestVersion, err = CheckLatestVersionOfCli(false) + }) if err != nil { t.Errorf("CheckLatestVersionOfCli() error = %v, want nil", err) @@ -64,10 +89,15 @@ func TestCheckLatestVersionOfCli_UpdateAvailable(t *testing.T) { t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want %q", latestVersion, "2.0.0") } - // Should print update message + // Should not write anything to stdout + if stdout != "" { + t.Errorf("CheckLatestVersionOfCli() stdout = %q, want empty string", stdout) + } + + // Should print update message to stderr expectedMsg := "A new version (2.0.0) is available. Update with: coolify update\n" - if output != expectedMsg { - t.Errorf("CheckLatestVersionOfCli() output = %q, want %q", output, expectedMsg) + if stderr != expectedMsg { + t.Errorf("CheckLatestVersionOfCli() stderr = %q, want %q", stderr, expectedMsg) } } @@ -92,19 +122,11 @@ func TestCheckLatestVersionOfCli_NoUpdate(t *testing.T) { GitHubAPIURL = server.URL - // Capture stdout - oldStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - latestVersion, err := CheckLatestVersionOfCli(false) - - _ = w.Close() - os.Stdout = oldStdout - - var buf bytes.Buffer - _, _ = io.Copy(&buf, r) - output := buf.String() + var latestVersion string + var err error + stdout, stderr := captureOutput(t, func() { + latestVersion, err = CheckLatestVersionOfCli(false) + }) if err != nil { t.Errorf("CheckLatestVersionOfCli() error = %v, want nil", err) @@ -116,8 +138,12 @@ func TestCheckLatestVersionOfCli_NoUpdate(t *testing.T) { } // Should NOT print any message when already on latest (current v99.99.99 > latest v2.0.0) - if output != "" { - t.Errorf("CheckLatestVersionOfCli() should not print anything when on latest version, got: %q", output) + if stdout != "" { + t.Errorf("CheckLatestVersionOfCli() should not write to stdout when on latest version, got: %q", stdout) + } + + if stderr != "" { + t.Errorf("CheckLatestVersionOfCli() should not write to stderr when on latest version, got: %q", stderr) } } @@ -137,19 +163,11 @@ func TestCheckLatestVersionOfCli_APIError_SilentFail(t *testing.T) { GitHubAPIURL = server.URL - // Capture stdout - oldStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - latestVersion, err := CheckLatestVersionOfCli(false) - - _ = w.Close() - os.Stdout = oldStdout - - var buf bytes.Buffer - _, _ = io.Copy(&buf, r) - output := buf.String() + var latestVersion string + var err error + stdout, stderr := captureOutput(t, func() { + latestVersion, err = CheckLatestVersionOfCli(false) + }) // Should return empty string and nil error (silent fail) if err != nil { @@ -161,8 +179,12 @@ func TestCheckLatestVersionOfCli_APIError_SilentFail(t *testing.T) { } // Should NOT print anything on error - if output != "" { - t.Errorf("CheckLatestVersionOfCli() should not print anything on API error, got: %q", output) + if stdout != "" { + t.Errorf("CheckLatestVersionOfCli() should not print anything to stdout on API error, got: %q", stdout) + } + + if stderr != "" { + t.Errorf("CheckLatestVersionOfCli() should not print anything to stderr on API error, got: %q", stderr) } } @@ -176,19 +198,11 @@ func TestCheckLatestVersionOfCli_NetworkError_SilentFail(t *testing.T) { // Use invalid URL to cause network error GitHubAPIURL = "http://localhost:1" // Port 1 should fail to connect - // Capture stdout - oldStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - latestVersion, err := CheckLatestVersionOfCli(false) - - _ = w.Close() - os.Stdout = oldStdout - - var buf bytes.Buffer - _, _ = io.Copy(&buf, r) - output := buf.String() + var latestVersion string + var err error + stdout, stderr := captureOutput(t, func() { + latestVersion, err = CheckLatestVersionOfCli(false) + }) // Should return empty string and nil error (silent fail) if err != nil { @@ -200,8 +214,46 @@ func TestCheckLatestVersionOfCli_NetworkError_SilentFail(t *testing.T) { } // Should NOT print anything on error - if output != "" { - t.Errorf("CheckLatestVersionOfCli() should not print anything on network error, got: %q", output) + if stdout != "" { + t.Errorf("CheckLatestVersionOfCli() should not print anything to stdout on network error, got: %q", stdout) + } + + if stderr != "" { + t.Errorf("CheckLatestVersionOfCli() should not print anything to stderr on network error, got: %q", stderr) + } +} + +func TestCheckLatestVersionOfCli_UpdateAvailable_LeavesStdoutAvailableForJSON(t *testing.T) { + originalURL := GitHubAPIURL + originalVersion := version + defer func() { + GitHubAPIURL = originalURL + version = originalVersion + }() + + version = "v0.0.1" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`[{"ref":"refs/tags/v2.0.0"}]`)) + })) + defer server.Close() + + GitHubAPIURL = server.URL + + stdout, stderr := captureOutput(t, func() { + _, _ = CheckLatestVersionOfCli(false) + _, _ = os.Stdout.WriteString(`[{"uuid":"demo"}]` + "\n") + }) + + expectedStdout := `[{"uuid":"demo"}]` + "\n" + if stdout != expectedStdout { + t.Fatalf("stdout = %q, want %q", stdout, expectedStdout) + } + + expectedStderr := "A new version (2.0.0) is available. Update with: coolify update\n" + if stderr != expectedStderr { + t.Fatalf("stderr = %q, want %q", stderr, expectedStderr) } }