Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 26 additions & 17 deletions cli/azd/internal/cmd/up_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -490,6 +483,30 @@ 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).
Comment thread
vhvb1989 marked this conversation as resolved.
startDeployTicker := func() {
if deployTracker == nil {
return
}
// 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()
}
Comment thread
vhvb1989 marked this conversation as resolved.
} else {
stopTicker = deployTracker.StartTicker(ctx)
}
}

opts := u.runOptions()
baseOnStepStart := opts.OnStepStart
baseOnStepDone := opts.OnStepDone
Expand All @@ -506,18 +523,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, "")
}
}
Expand Down
69 changes: 69 additions & 0 deletions cli/azd/pkg/input/console_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,75 @@ func TestAskerConsole_Previewer_ConcurrentWriteStress(t *testing.T) {
}
}

// TestAskerConsole_PausePreviewer_DiscardsHookOutput reproduces the bug from GitHub issue #8237:
// 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 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))
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 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()

// 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 (expected PausePreviewer behavior)")

// 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
Expand Down
Loading