From da7098a0211a0304b4141888a27bde0fab7fc981 Mon Sep 17 00:00:00 2001 From: Abraham White Date: Fri, 13 Nov 2026 14:03:29 -0700 Subject: [PATCH 1/2] Output update available notice to stderr --- pkg/app/run.go | 4 +++- pkg/app/run_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/pkg/app/run.go b/pkg/app/run.go index 5dd84fd46..74e567cca 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -336,7 +336,9 @@ func Exec(data *global.Data) error { f := checkForUpdates(data.Versioners.CLI, commandName) defer func() { if !data.Flags.Quiet && !data.Flags.JSON { - f(data.Output) + // always write update notification to stderr so it doesn't corrupt + // `fastly auth token` which might be piped or used in a command + f(data.ErrOutput) } }() diff --git a/pkg/app/run_test.go b/pkg/app/run_test.go index 5edca1b92..d2bf8f8c0 100644 --- a/pkg/app/run_test.go +++ b/pkg/app/run_test.go @@ -221,6 +221,52 @@ func TestExecJSONLeavesStdoutCleanAndWritesWarningToStderr(t *testing.T) { } } +func TestAuthTokenUpdateNoticeWritesToStderr(t *testing.T) { + origVersion := revision.AppVersion + origInit := app.Init + revision.AppVersion = "1.0.0" + t.Cleanup(func() { + revision.AppVersion = origVersion + app.Init = origInit + }) + + for _, testcase := range []struct { + name string + args string + latest string + wantNotice bool + }{ + {name: "update available", args: "auth token", latest: "1.0.1", wantNotice: true}, + {name: "quiet", args: "auth token --quiet", latest: "1.0.1"}, + {name: "up to date", args: "auth token", latest: "1.0.0"}, + } { + t.Run(testcase.name, func(t *testing.T) { + var stdout, stderr bytes.Buffer + args := testutil.SplitArgs(testcase.args) + app.Init = func(_ []string, _ io.Reader) (*global.Data, error) { + data := testutil.MockGlobalData(args, &stdout) + data.ErrOutput = &stderr + data.Versioners.CLI = mock.AssetVersioner{AssetVersion: testcase.latest} + return data, nil + } + + if err := app.Run(args, nil); err != nil { + t.Fatalf("app.Run returned unexpected error: %v", err) + } + if got := stdout.String(); got != "mock-token" { + t.Errorf("expected only the token on stdout, got: %q", got) + } + if testcase.wantNotice { + if !strings.Contains(stderr.String(), "A new version of the Fastly CLI is available.") { + t.Errorf("expected update notice on stderr, got: %q", stderr.String()) + } + } else if stderr.Len() != 0 { + t.Errorf("expected no stderr output, got: %q", stderr.String()) + } + }) + } +} + // TestStatsJSONSuppressesUpdateNotice verifies that --json and --format=json on // stats commands suppress the deferred update-check notice, keeping stdout as // clean JSON. This is the regression test for the timing bug where @@ -238,10 +284,11 @@ func TestStatsJSONSuppressesUpdateNotice(t *testing.T) { for _, flag := range []string{"--json", "--format=json"} { t.Run(flag, func(t *testing.T) { - var stdout bytes.Buffer + var stdout, stderr bytes.Buffer args := testutil.SplitArgs("stats aggregate " + flag) app.Init = func(_ []string, _ io.Reader) (*global.Data, error) { data := testutil.MockGlobalData(args, &stdout) + data.ErrOutput = &stderr data.APIClientFactory = mock.APIClient(mock.API{ GetAggregateJSONFn: aggregateOK, }) @@ -255,6 +302,9 @@ func TestStatsJSONSuppressesUpdateNotice(t *testing.T) { if strings.Contains(stdout.String(), "new version") { t.Errorf("update notice should be suppressed in JSON mode, got: %s", stdout.String()) } + if strings.Contains(stderr.String(), "new version") { + t.Errorf("update notice should be suppressed in JSON mode, got stderr: %s", stderr.String()) + } }) } } From 25c7663abbae3d058f16249bd2fc10e8e0330735 Mon Sep 17 00:00:00 2001 From: Abraham White Date: Fri, 13 Nov 2026 14:07:17 -0700 Subject: [PATCH 2/2] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4763dbdf6..d71405430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### Bug Fixes: - fix(compute/serve): provide default `override_host` to Viceroy via temporary manifest and handle IPv6 hostnames ([#1940](https://github.com/fastly/cli/pull/1940)) +- fix(auth): ensure `fastly auth token` output is not corrupted by a version update notice written to stdout ([#1971](https://github.com/fastly/cli/pull/1971)) ### Enhancements: