diff --git a/cli/azd/internal/cmd/up_graph.go b/cli/azd/internal/cmd/up_graph.go index 76f5c6946d4..b9d3514a80e 100644 --- a/cli/azd/internal/cmd/up_graph.go +++ b/cli/azd/internal/cmd/up_graph.go @@ -476,6 +476,7 @@ func (u *UpGraphAction) Run( DependsOn: []string{postDeployEventStep}, Tags: []string{"cmdhook"}, Action: func(ctx context.Context) error { + u.console.Message(ctx, "Running postdeploy hook...") return runProjectCommandHook( ctx, hookDeps, ext.HookTypePost, string(project.ProjectEventDeploy), ) @@ -495,12 +496,23 @@ func (u *UpGraphAction) Run( // in parallel with provisioning). This avoids conflicting with // the provisioning progress display. var ( - tickerOnce sync.Once - stopTicker func() + tickerOnce sync.Once + stopTicker func() + deployProgressFinalized bool ) if deployTracker != nil { stopTicker = func() {} // no-op until started } + finalizeDeployProgress := func() { + if stopTicker != nil { + stopTicker() + stopTicker = nil + } + if deployTracker != nil && deployTracker.HasActivity() && !deployProgressFinalized { + deployTracker.RenderFinal() + deployProgressFinalized = true + } + } // 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 @@ -534,6 +546,11 @@ func (u *UpGraphAction) Run( if baseOnStepStart != nil { baseOnStepStart(stepName) } + // Finalize deploy progress before postdeploy hooks execute so that + // hook output renders normally and does not look like a hang. + if shouldFinalizeDeployProgressBeforeStep(stepName) { + finalizeDeployProgress() + } // Update deploy progress tracker for service steps. // Packaging runs in parallel with provisioning, so only update the // data model silently. Start the visual ticker when the first @@ -579,13 +596,9 @@ func (u *UpGraphAction) Run( result := exegraph.RunWithResult(ctx, g, opts) - // Stop the progress ticker and render a final summary table. - if stopTicker != nil { - stopTicker() - } - if deployTracker != nil && deployTracker.HasActivity() { - deployTracker.RenderFinal() - } + // Stop the progress ticker and render a final summary table if it has + // not already been finalized before the postdeploy hook. + finalizeDeployProgress() // Clean up temporary package artifacts regardless of success/failure. state.CleanupTempArtifacts() @@ -710,6 +723,10 @@ func phaseDurations(steps []exegraph.StepTiming) (provision, deploy time.Duratio return provision, deploy } +func shouldFinalizeDeployProgressBeforeStep(stepName string) bool { + return stepName == "cmdhook-postdeploy" +} + // changedFlagNames returns the names of flags that were explicitly set on // the FlagSet. Returns nil for a nil FlagSet so callers can pass through // without a guard. Mirrors the changedFlags collection in diff --git a/cli/azd/internal/cmd/up_graph_test.go b/cli/azd/internal/cmd/up_graph_test.go index e39cea392fa..4920146feda 100644 --- a/cli/azd/internal/cmd/up_graph_test.go +++ b/cli/azd/internal/cmd/up_graph_test.go @@ -83,3 +83,26 @@ func TestPhaseTimingBreakdown(t *testing.T) { }) } } + +func TestShouldFinalizeDeployProgressBeforeStep(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + stepName string + want bool + }{ + {name: "postdeploy hook", stepName: "cmdhook-postdeploy", want: true}, + {name: "predeploy hook", stepName: "cmdhook-predeploy", want: false}, + {name: "deploy step", stepName: "deploy-web", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := shouldFinalizeDeployProgressBeforeStep(tt.stepName); got != tt.want { + t.Fatalf("shouldFinalizeDeployProgressBeforeStep(%q)=%t, want %t", tt.stepName, got, tt.want) + } + }) + } +}