From 588d2b3a7d853ee5c258bc9b38555b5d08259b1a Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 19 May 2026 23:47:25 -0700 Subject: [PATCH 1/3] fix(hooks): restore lifecycle hook output visibility during azd up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PausePreviewer() was being called at graph build time in up_graph.go, before any steps execute. This caused ShowPreviewer() to return io.Discard for the entire Run() duration, silencing all lifecycle hook output (preprovision, postprovision, predeploy, postdeploy) during zd up. The fix moves PausePreviewer() into startDeployTicker (called via tickerOnce.Do when the first publish or deploy step begins). The previewer is now only suppressed during the publish/deploy phase when the progress table is actually rendering — not during the earlier provision and hook phases. Adds a regression test (TestAskerConsole_PausePreviewer_DiscardsHookOutput) documenting the broken behavior and the PreviewerPauser mechanism. Fixes #8237. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/internal/cmd/up_graph.go | 41 +++++++++++-------- cli/azd/pkg/input/console_test.go | 68 +++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 17 deletions(-) diff --git a/cli/azd/internal/cmd/up_graph.go b/cli/azd/internal/cmd/up_graph.go index a4680e8c51a..c4fba1c252f 100644 --- a/cli/azd/internal/cmd/up_graph.go +++ b/cli/azd/internal/cmd/up_graph.go @@ -337,13 +337,6 @@ func (u *UpGraphAction) Run( serviceNames[i] = svc.Name } deployTracker = newDeployProgressTracker(w, u.console.IsSpinnerInteractive(), serviceNames) - // Suppress previewer output at the shared console level so that - // DI-injected consumers (e.g. ContainerHelper's Docker output) - // don't corrupt the progress table display. - if ps, ok := u.console.(input.PreviewerPauser); ok { - ps.PausePreviewer() - defer ps.ResumePreviewer() - } } updateDeployProgress := func(svcName string, phase deployPhase, detail string) { @@ -490,6 +483,28 @@ func (u *UpGraphAction) Run( stopTicker = func() {} // no-op until started } + // startDeployTicker is called once (via tickerOnce) when the first publish or deploy + // step begins. It starts the progress table ticker and suppresses the console previewer + // so that DI-injected ShowPreviewer callers (e.g. ContainerHelper's Docker output) + // don't corrupt the progress table display. + // Previewer is not paused during the earlier provision + hook phases so that + // preprovision/postprovision hook output remains visible (fixes #8237). + startDeployTicker := func() { + if deployTracker == nil { + return + } + stop := deployTracker.StartTicker(ctx) + if ps, ok := u.console.(input.PreviewerPauser); ok { + ps.PausePreviewer() + stopTicker = func() { + stop() + ps.ResumePreviewer() + } + } else { + stopTicker = stop + } + } + opts := u.runOptions() baseOnStepStart := opts.OnStepStart baseOnStepDone := opts.OnStepDone @@ -506,18 +521,10 @@ func (u *UpGraphAction) Run( if svc, ok := strings.CutPrefix(stepName, "package-"); ok { updateDeployProgress(svc, phasePackaging, "") } else if svc, ok := strings.CutPrefix(stepName, "publish-"); ok { - tickerOnce.Do(func() { - if deployTracker != nil { - stopTicker = deployTracker.StartTicker(ctx) - } - }) + tickerOnce.Do(startDeployTicker) updateDeployProgress(svc, phasePublish, "") } else if svc, ok := strings.CutPrefix(stepName, "deploy-"); ok { - tickerOnce.Do(func() { - if deployTracker != nil { - stopTicker = deployTracker.StartTicker(ctx) - } - }) + tickerOnce.Do(startDeployTicker) updateDeployProgress(svc, phaseDeploying, "") } } diff --git a/cli/azd/pkg/input/console_test.go b/cli/azd/pkg/input/console_test.go index 60a24a326fb..6ec7f672204 100644 --- a/cli/azd/pkg/input/console_test.go +++ b/cli/azd/pkg/input/console_test.go @@ -541,6 +541,74 @@ func TestAskerConsole_Previewer_ConcurrentWriteStress(t *testing.T) { } } +// TestAskerConsole_PausePreviewer_DiscardsHookOutput reproduces the bug from GitHub issue #8237: +// in azd 1.25.0+, azd up calls PausePreviewer() early in the execution graph setup (up_graph.go), +// which causes ShowPreviewer to return io.Discard for the entire duration of the run. This means +// lifecycle hook output (preprovision, postprovision, predeploy, postdeploy) is silently thrown away. +// +// Before 1.25.0, azd up used a workflow runner that invoked azd provision + azd deploy as +// sub-commands. Each ran independently and hooks used ShowPreviewer normally — output was visible. +// +// The fix is to move PausePreviewer() to only be called when the deploy progress table ticker +// actually starts (publish/deploy phase), not upfront before any graph steps execute. +func TestAskerConsole_PausePreviewer_DiscardsHookOutput(t *testing.T) { + formatter, err := output.NewFormatter(string(output.NoneFormat)) + require.NoError(t, err) + + lines := &lineCapturer{} + c := NewConsole( + false, + false, + Writers{Output: lines}, + ConsoleHandles{ + Stderr: os.Stderr, + Stdin: os.Stdin, + Stdout: lines, + }, + formatter, + nil, + ) + + ctx := t.Context() + + // Regression: ShowPreviewer should return a real writer before PausePreviewer is called. + // This simulates the pre-1.25.0 behavior where hooks ran via normal sequential sub-commands + // (no PausePreviewer call), so hook output was visible. + writerBeforePause := c.ShowPreviewer(ctx, &ShowPreviewerOptions{ + Title: "preprovision Hook Output", + MaxLineCount: 8, + }) + require.NotEqual(t, io.Discard, writerBeforePause, + "ShowPreviewer should return a real writer when previewer is not paused") + c.StopPreviewer(ctx, false) + + // Simulate what azd 1.25.0 up_graph.go does: PausePreviewer() is called early, + // before any graph steps execute (before preprovision/postprovision hooks run). + ps, ok := c.(PreviewerPauser) + require.True(t, ok, "AskerConsole must implement PreviewerPauser") + ps.PausePreviewer() + + // Bug: ShowPreviewer now returns io.Discard, silencing all hook output + // (preprovision, postprovision, predeploy, postdeploy hooks that call ShowPreviewer + // will have their stdout set to io.Discard, so their output is never seen). + writerWhilePaused := c.ShowPreviewer(ctx, &ShowPreviewerOptions{ + Title: "preprovision Hook Output", + MaxLineCount: 8, + }) + require.Equal(t, io.Discard, writerWhilePaused, + "ShowPreviewer returns io.Discard when previewer is paused — this is the bug: hook output is silenced") + + // After ResumePreviewer, ShowPreviewer should work again. + ps.ResumePreviewer() + writerAfterResume := c.ShowPreviewer(ctx, &ShowPreviewerOptions{ + Title: "postprovision Hook Output", + MaxLineCount: 8, + }) + require.NotEqual(t, io.Discard, writerAfterResume, + "ShowPreviewer should return a real writer after ResumePreviewer") + c.StopPreviewer(ctx, false) +} + // writerAdapter wraps *strings.Builder to satisfy io.Writer for test purposes. type writerAdapter struct { *strings.Builder From 7fd4f7a6b86ded3df171abef5e2138ca592ce399 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Wed, 20 May 2026 17:45:41 +0000 Subject: [PATCH 2/3] fix: address Copilot review feedback (iteration 1) - Move PausePreviewer() before StartTicker() to close race window - Reword test assert message to clarify PausePreviewer behavior vs bug Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/internal/cmd/up_graph.go | 6 ++++-- cli/azd/pkg/input/console_test.go | 9 +++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cli/azd/internal/cmd/up_graph.go b/cli/azd/internal/cmd/up_graph.go index c4fba1c252f..d791ae72902 100644 --- a/cli/azd/internal/cmd/up_graph.go +++ b/cli/azd/internal/cmd/up_graph.go @@ -493,15 +493,17 @@ func (u *UpGraphAction) Run( if deployTracker == nil { return } - stop := deployTracker.StartTicker(ctx) + // Pause the previewer before starting the ticker to avoid a window where + // the progress table renders while ShowPreviewer is still active. if ps, ok := u.console.(input.PreviewerPauser); ok { ps.PausePreviewer() + stop := deployTracker.StartTicker(ctx) stopTicker = func() { stop() ps.ResumePreviewer() } } else { - stopTicker = stop + stopTicker = deployTracker.StartTicker(ctx) } } diff --git a/cli/azd/pkg/input/console_test.go b/cli/azd/pkg/input/console_test.go index 6ec7f672204..f8c573b476e 100644 --- a/cli/azd/pkg/input/console_test.go +++ b/cli/azd/pkg/input/console_test.go @@ -588,15 +588,16 @@ func TestAskerConsole_PausePreviewer_DiscardsHookOutput(t *testing.T) { require.True(t, ok, "AskerConsole must implement PreviewerPauser") ps.PausePreviewer() - // Bug: ShowPreviewer now returns io.Discard, silencing all hook output - // (preprovision, postprovision, predeploy, postdeploy hooks that call ShowPreviewer - // will have their stdout set to io.Discard, so their output is never seen). + // PausePreviewer is designed to suppress previewer output — ShowPreviewer returns + // io.Discard while paused. This is expected behavior. The actual bug (#8237) was that + // azd up called PausePreviewer too early (before hooks), not that PausePreviewer + // suppresses output. writerWhilePaused := c.ShowPreviewer(ctx, &ShowPreviewerOptions{ Title: "preprovision Hook Output", MaxLineCount: 8, }) require.Equal(t, io.Discard, writerWhilePaused, - "ShowPreviewer returns io.Discard when previewer is paused — this is the bug: hook output is silenced") + "ShowPreviewer returns io.Discard when previewer is paused (expected PausePreviewer behavior)") // After ResumePreviewer, ShowPreviewer should work again. ps.ResumePreviewer() From 470d43693462f2f15247a828541c6ccb39a3d965 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Wed, 20 May 2026 18:55:46 +0000 Subject: [PATCH 3/3] fix: use past tense in test comments for fixed behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/pkg/input/console_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/azd/pkg/input/console_test.go b/cli/azd/pkg/input/console_test.go index f8c573b476e..f7c16700d5a 100644 --- a/cli/azd/pkg/input/console_test.go +++ b/cli/azd/pkg/input/console_test.go @@ -542,14 +542,14 @@ func TestAskerConsole_Previewer_ConcurrentWriteStress(t *testing.T) { } // TestAskerConsole_PausePreviewer_DiscardsHookOutput reproduces the bug from GitHub issue #8237: -// in azd 1.25.0+, azd up calls PausePreviewer() early in the execution graph setup (up_graph.go), -// which causes ShowPreviewer to return io.Discard for the entire duration of the run. This means -// lifecycle hook output (preprovision, postprovision, predeploy, postdeploy) is silently thrown away. +// in azd 1.25.0+, azd up called PausePreviewer() early in the execution graph setup (up_graph.go), +// which caused ShowPreviewer to return io.Discard for the entire duration of the run. This meant +// lifecycle hook output (preprovision, postprovision, predeploy, postdeploy) was silently thrown away. // // Before 1.25.0, azd up used a workflow runner that invoked azd provision + azd deploy as // sub-commands. Each ran independently and hooks used ShowPreviewer normally — output was visible. // -// The fix is to move PausePreviewer() to only be called when the deploy progress table ticker +// The fix moved PausePreviewer() to only be called when the deploy progress table ticker // actually starts (publish/deploy phase), not upfront before any graph steps execute. func TestAskerConsole_PausePreviewer_DiscardsHookOutput(t *testing.T) { formatter, err := output.NewFormatter(string(output.NoneFormat)) @@ -582,8 +582,8 @@ func TestAskerConsole_PausePreviewer_DiscardsHookOutput(t *testing.T) { "ShowPreviewer should return a real writer when previewer is not paused") c.StopPreviewer(ctx, false) - // Simulate what azd 1.25.0 up_graph.go does: PausePreviewer() is called early, - // before any graph steps execute (before preprovision/postprovision hooks run). + // Simulate what azd 1.25.0 up_graph.go did: PausePreviewer() was called early, + // before any graph steps executed (before preprovision/postprovision hooks ran). ps, ok := c.(PreviewerPauser) require.True(t, ok, "AskerConsole must implement PreviewerPauser") ps.PausePreviewer()