From b1a15b9e5559266839c1bf5f2428f99b8f025221 Mon Sep 17 00:00:00 2001 From: Karthik Chowdary <21139050+Karthik-Chowdary@users.noreply.github.com> Date: Mon, 17 Aug 2026 07:02:12 +0000 Subject: [PATCH 1/3] feat(cmd): show progress before waiting for resources Signed-off-by: Karthik Chowdary <21139050+Karthik-Chowdary@users.noreply.github.com> --- pkg/cmd/install.go | 10 ++++++++++ pkg/cmd/testdata/output/install-with-wait-for-jobs.txt | 1 + pkg/cmd/testdata/output/install-with-wait.txt | 1 + pkg/cmd/testdata/output/upgrade-with-wait-for-jobs.txt | 1 + pkg/cmd/testdata/output/upgrade-with-wait.txt | 1 + pkg/cmd/upgrade.go | 4 ++++ 6 files changed, 18 insertions(+) diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index 182b4a7ff..86cc206d4 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -38,6 +38,7 @@ import ( "helm.sh/helm/v4/pkg/cmd/require" "helm.sh/helm/v4/pkg/downloader" "helm.sh/helm/v4/pkg/getter" + "helm.sh/helm/v4/pkg/kube" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -157,6 +158,8 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } client.DryRunStrategy = dryRunStrategy + printWaitMessage(out, outfmt, client.WaitStrategy, client.Timeout) + rel, err := runInstall(args, client, valueOpts, out) if err != nil { return fmt.Errorf("INSTALLATION FAILED: %w", err) @@ -370,6 +373,13 @@ func checkIfInstallable(ch chart.Accessor) error { return fmt.Errorf("%s charts are not installable", meta["Type"]) } +func printWaitMessage(out io.Writer, outfmt output.Format, strategy kube.WaitStrategy, timeout time.Duration) { + if outfmt != output.Table || strategy == kube.HookOnlyStrategy { + return + } + fmt.Fprintf(out, "Waiting for resources to become ready (timeout: %s)\n", timeout) +} + // Provide dynamic auto-completion for the install and template commands func compInstall(args []string, toComplete string, client *action.Install) ([]string, cobra.ShellCompDirective) { requiredArgs := 1 diff --git a/pkg/cmd/testdata/output/install-with-wait-for-jobs.txt b/pkg/cmd/testdata/output/install-with-wait-for-jobs.txt index c5676c610..67e223847 100644 --- a/pkg/cmd/testdata/output/install-with-wait-for-jobs.txt +++ b/pkg/cmd/testdata/output/install-with-wait-for-jobs.txt @@ -1,3 +1,4 @@ +Waiting for resources to become ready (timeout: 5m0s) NAME: apollo LAST DEPLOYED: Fri Sep 2 22:04:05 1977 NAMESPACE: default diff --git a/pkg/cmd/testdata/output/install-with-wait.txt b/pkg/cmd/testdata/output/install-with-wait.txt index c5676c610..67e223847 100644 --- a/pkg/cmd/testdata/output/install-with-wait.txt +++ b/pkg/cmd/testdata/output/install-with-wait.txt @@ -1,3 +1,4 @@ +Waiting for resources to become ready (timeout: 5m0s) NAME: apollo LAST DEPLOYED: Fri Sep 2 22:04:05 1977 NAMESPACE: default diff --git a/pkg/cmd/testdata/output/upgrade-with-wait-for-jobs.txt b/pkg/cmd/testdata/output/upgrade-with-wait-for-jobs.txt index 21784413c..44524d8df 100644 --- a/pkg/cmd/testdata/output/upgrade-with-wait-for-jobs.txt +++ b/pkg/cmd/testdata/output/upgrade-with-wait-for-jobs.txt @@ -1,3 +1,4 @@ +Waiting for resources to become ready (timeout: 5m0s) Release "crazy-bunny" has been upgraded. Happy Helming! NAME: crazy-bunny LAST DEPLOYED: Fri Sep 2 22:04:05 1977 diff --git a/pkg/cmd/testdata/output/upgrade-with-wait.txt b/pkg/cmd/testdata/output/upgrade-with-wait.txt index 21784413c..44524d8df 100644 --- a/pkg/cmd/testdata/output/upgrade-with-wait.txt +++ b/pkg/cmd/testdata/output/upgrade-with-wait.txt @@ -1,3 +1,4 @@ +Waiting for resources to become ready (timeout: 5m0s) Release "crazy-bunny" has been upgraded. Happy Helming! NAME: crazy-bunny LAST DEPLOYED: Fri Sep 2 22:04:05 1977 diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index 688842141..3df130b3c 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -163,6 +163,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.Replace = true } + printWaitMessage(out, outfmt, instClient.WaitStrategy, instClient.Timeout) + rel, err := runInstall(args, instClient, valueOpts, out) if err != nil { return err @@ -257,6 +259,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { cancel() }() + printWaitMessage(out, outfmt, client.WaitStrategy, client.Timeout) + rel, err := client.RunWithContext(ctx, args[0], ch, vals) if err != nil { return fmt.Errorf("UPGRADE FAILED: %w", err) From d22502c90e0c0b213f1a2ee7ea0fdfa471a9c1a7 Mon Sep 17 00:00:00 2001 From: Karthik Chowdary <21139050+Karthik-Chowdary@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:22:13 +0000 Subject: [PATCH 2/3] fix(cmd): suppress wait progress during dry runs Signed-off-by: Karthik Chowdary <21139050+Karthik-Chowdary@users.noreply.github.com> --- pkg/cmd/install.go | 6 +++--- pkg/cmd/upgrade.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index 86cc206d4..8d3e00604 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -158,7 +158,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } client.DryRunStrategy = dryRunStrategy - printWaitMessage(out, outfmt, client.WaitStrategy, client.Timeout) + printWaitMessage(out, outfmt, client.WaitStrategy, client.DryRunStrategy, client.Timeout) rel, err := runInstall(args, client, valueOpts, out) if err != nil { @@ -373,8 +373,8 @@ func checkIfInstallable(ch chart.Accessor) error { return fmt.Errorf("%s charts are not installable", meta["Type"]) } -func printWaitMessage(out io.Writer, outfmt output.Format, strategy kube.WaitStrategy, timeout time.Duration) { - if outfmt != output.Table || strategy == kube.HookOnlyStrategy { +func printWaitMessage(out io.Writer, outfmt output.Format, strategy kube.WaitStrategy, dryRun action.DryRunStrategy, timeout time.Duration) { + if outfmt != output.Table || strategy == kube.HookOnlyStrategy || dryRun != action.DryRunNone { return } fmt.Fprintf(out, "Waiting for resources to become ready (timeout: %s)\n", timeout) diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index 3df130b3c..e8bc834ac 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -163,7 +163,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.Replace = true } - printWaitMessage(out, outfmt, instClient.WaitStrategy, instClient.Timeout) + printWaitMessage(out, outfmt, instClient.WaitStrategy, instClient.DryRunStrategy, instClient.Timeout) rel, err := runInstall(args, instClient, valueOpts, out) if err != nil { @@ -259,7 +259,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { cancel() }() - printWaitMessage(out, outfmt, client.WaitStrategy, client.Timeout) + printWaitMessage(out, outfmt, client.WaitStrategy, client.DryRunStrategy, client.Timeout) rel, err := client.RunWithContext(ctx, args[0], ch, vals) if err != nil { From 835fc4cba0eaad06287b9455ad51334921cdae62 Mon Sep 17 00:00:00 2001 From: Karthik Chowdary <21139050+Karthik-Chowdary@users.noreply.github.com> Date: Thu, 27 Aug 2026 06:51:21 +0000 Subject: [PATCH 3/3] fix(cmd): report implicit watcher waits Signed-off-by: Karthik Chowdary <21139050+Karthik-Chowdary@users.noreply.github.com> --- pkg/cmd/install.go | 9 +++++-- pkg/cmd/install_test.go | 58 +++++++++++++++++++++++++++++++++++++++++ pkg/cmd/upgrade.go | 4 +-- pkg/cmd/upgrade_test.go | 31 ++++++++++++++++++++++ 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index 8d3e00604..499f3a0af 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -158,7 +158,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } client.DryRunStrategy = dryRunStrategy - printWaitMessage(out, outfmt, client.WaitStrategy, client.DryRunStrategy, client.Timeout) + printWaitMessage(out, outfmt, client.WaitStrategy, client.RollbackOnFailure, client.DryRunStrategy, client.Timeout) rel, err := runInstall(args, client, valueOpts, out) if err != nil { @@ -373,7 +373,12 @@ func checkIfInstallable(ch chart.Accessor) error { return fmt.Errorf("%s charts are not installable", meta["Type"]) } -func printWaitMessage(out io.Writer, outfmt output.Format, strategy kube.WaitStrategy, dryRun action.DryRunStrategy, timeout time.Duration) { +func printWaitMessage(out io.Writer, outfmt output.Format, strategy kube.WaitStrategy, rollbackOnFailure bool, dryRun action.DryRunStrategy, timeout time.Duration) { + // Rollback-on-failure implicitly enables watcher waits in the action layer. + // Account for that here, before the action mutates its WaitStrategy. + if strategy == kube.HookOnlyStrategy && rollbackOnFailure { + strategy = kube.StatusWatcherStrategy + } if outfmt != output.Table || strategy == kube.HookOnlyStrategy || dryRun != action.DryRunNone { return } diff --git a/pkg/cmd/install_test.go b/pkg/cmd/install_test.go index d1e081e5c..59092efa9 100644 --- a/pkg/cmd/install_test.go +++ b/pkg/cmd/install_test.go @@ -17,14 +17,19 @@ limitations under the License. package cmd import ( + "bytes" "fmt" "net/http" "net/http/httptest" "path/filepath" "testing" + "time" "github.com/stretchr/testify/require" + "helm.sh/helm/v4/pkg/action" + "helm.sh/helm/v4/pkg/cli/output" + "helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/repo/v1/repotest" ) @@ -323,3 +328,56 @@ func TestInstallFileCompletion(t *testing.T) { checkFileCompletion(t, "install myname", true) checkFileCompletion(t, "install myname mychart", false) } + +func TestPrintWaitMessage(t *testing.T) { + const message = "Waiting for resources to become ready (timeout: 42s)\n" + tests := []struct { + name string + format output.Format + strategy kube.WaitStrategy + rollbackOnFailure bool + dryRun action.DryRunStrategy + want string + }{ + {name: "explicit watcher wait", format: output.Table, strategy: kube.StatusWatcherStrategy, dryRun: action.DryRunNone, want: message}, + {name: "implicit watcher wait", format: output.Table, strategy: kube.HookOnlyStrategy, rollbackOnFailure: true, dryRun: action.DryRunNone, want: message}, + {name: "hook-only wait", format: output.Table, strategy: kube.HookOnlyStrategy, dryRun: action.DryRunNone}, + {name: "structured output", format: output.JSON, strategy: kube.StatusWatcherStrategy, dryRun: action.DryRunNone}, + {name: "client dry run", format: output.Table, strategy: kube.StatusWatcherStrategy, dryRun: action.DryRunClient}, + {name: "server dry run", format: output.Table, strategy: kube.StatusWatcherStrategy, dryRun: action.DryRunServer}, + {name: "implicit wait client dry run", format: output.Table, strategy: kube.HookOnlyStrategy, rollbackOnFailure: true, dryRun: action.DryRunClient}, + {name: "implicit wait server dry run", format: output.Table, strategy: kube.HookOnlyStrategy, rollbackOnFailure: true, dryRun: action.DryRunServer}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var out bytes.Buffer + printWaitMessage(&out, tt.format, tt.strategy, tt.rollbackOnFailure, tt.dryRun, 42*time.Second) + require.Equal(t, tt.want, out.String()) + }) + } +} + +func TestInstallImplicitWaitProgress(t *testing.T) { + for _, flag := range []string{"--rollback-on-failure", "--atomic"} { + t.Run(flag, func(t *testing.T) { + defer resetEnv()() + + _, out, err := executeActionCommand("install implicit-wait testdata/testcharts/empty " + flag) + require.NoError(t, err) + require.Contains(t, out, "Waiting for resources to become ready (timeout: 5m0s)\n") + }) + } +} + +func TestInstallDryRunSuppressesWaitProgress(t *testing.T) { + for _, dryRun := range []string{"client", "server"} { + t.Run(dryRun, func(t *testing.T) { + defer resetEnv()() + + _, out, err := executeActionCommand("install dry-run-wait testdata/testcharts/empty --rollback-on-failure --dry-run=" + dryRun) + require.NoError(t, err) + require.NotContains(t, out, "Waiting for resources to become ready") + }) + } +} diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index e8bc834ac..6be0bab45 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -163,7 +163,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.Replace = true } - printWaitMessage(out, outfmt, instClient.WaitStrategy, instClient.DryRunStrategy, instClient.Timeout) + printWaitMessage(out, outfmt, instClient.WaitStrategy, instClient.RollbackOnFailure, instClient.DryRunStrategy, instClient.Timeout) rel, err := runInstall(args, instClient, valueOpts, out) if err != nil { @@ -259,7 +259,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { cancel() }() - printWaitMessage(out, outfmt, client.WaitStrategy, client.DryRunStrategy, client.Timeout) + printWaitMessage(out, outfmt, client.WaitStrategy, client.RollbackOnFailure, client.DryRunStrategy, client.Timeout) rel, err := client.RunWithContext(ctx, args[0], ch, vals) if err != nil { diff --git a/pkg/cmd/upgrade_test.go b/pkg/cmd/upgrade_test.go index 5aba7b11b..3a16d8323 100644 --- a/pkg/cmd/upgrade_test.go +++ b/pkg/cmd/upgrade_test.go @@ -587,3 +587,34 @@ func TestUpgradeInstallServerSideApply(t *testing.T) { }) } } + +func TestUpgradeImplicitWaitProgress(t *testing.T) { + tests := []struct { + name string + install bool + }{ + {name: "upgrade"}, + {name: "upgrade install", install: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer resetEnv()() + + releaseName := "implicit-wait" + relMock, ch, chartPath := prepareMockRelease(t, releaseName) + store := storageFixture() + installFlag := "" + if tt.install { + installFlag = "--install" + } else { + require.NoError(t, store.Create(relMock(releaseName, 1, ch))) + } + + cmd := fmt.Sprintf("upgrade %s %s --rollback-on-failure '%s'", releaseName, installFlag, chartPath) + _, out, err := executeActionCommandC(store, cmd) + require.NoError(t, err) + assert.Contains(t, out, "Waiting for resources to become ready (timeout: 5m0s)\n") + }) + } +}