From 05e9a98b5e6c7fd8a7c6c2406520b5ef25759de5 Mon Sep 17 00:00:00 2001 From: Mukul Date: Sun, 19 Jul 2026 11:49:57 +0530 Subject: [PATCH] Render CLI timestamps in local time so TZ is honored helm history, helm status, and helm list format release timestamps with the offset the release was stored with, so setting TZ has no effect on displayed times (the stored offset simply passes through). Convert to local time before formatting in the table writers for history, status (including test-suite hook times), and list. Go derives the local zone from TZ, so 'TZ=UTC helm history ...' now behaves as expected. JSON/YAML output is unchanged and keeps the stored offset. Golden files are unaffected: the pkg/cmd test init pins time.Local to UTC so table output stays deterministic across machines, and a regression test renders history in a fixed non-UTC zone to lock in the conversion. Closes #31902 Signed-off-by: Mukul --- pkg/cmd/helpers_test.go | 3 +++ pkg/cmd/history.go | 4 ++-- pkg/cmd/history_test.go | 27 +++++++++++++++++++++++++++ pkg/cmd/list.go | 4 ++-- pkg/cmd/status.go | 6 +++--- 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/helpers_test.go b/pkg/cmd/helpers_test.go index 3db5f38ab..5e88b2b62 100644 --- a/pkg/cmd/helpers_test.go +++ b/pkg/cmd/helpers_test.go @@ -45,6 +45,9 @@ func testTimestamper() time.Time { return time.Unix(242085845, 0).UTC() } func init() { action.Timestamper = testTimestamper + // Timestamps are rendered in local time; pin the local zone to UTC so + // golden-file output does not depend on the machine's timezone. + time.Local = time.UTC } func runTestCmd(t *testing.T, tests []cmdTestCase) { diff --git a/pkg/cmd/history.go b/pkg/cmd/history.go index f53a2f1a6..388ed7f3b 100644 --- a/pkg/cmd/history.go +++ b/pkg/cmd/history.go @@ -194,7 +194,7 @@ func (r releaseHistory) WriteTable(out io.Writer) error { tbl := uitable.New() tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "DESCRIPTION") for _, item := range r { - tbl.AddRow(item.Revision, item.Updated.Format(time.ANSIC), item.Status, item.Chart, item.AppVersion, item.Description) + tbl.AddRow(item.Revision, item.Updated.Local().Format(time.ANSIC), item.Status, item.Chart, item.AppVersion, item.Description) } return output.EncodeTable(out, tbl) } @@ -218,7 +218,7 @@ func (r releaseHistoryWithRollback) WriteTable(out io.Writer) error { if item.RollbackRevision > 0 { rollback = strconv.Itoa(item.RollbackRevision) } - tbl.AddRow(item.Revision, item.Updated.Format(time.ANSIC), item.Status, item.Chart, item.AppVersion, rollback, item.Description) + tbl.AddRow(item.Revision, item.Updated.Local().Format(time.ANSIC), item.Status, item.Chart, item.AppVersion, rollback, item.Description) } return output.EncodeTable(out, tbl) } diff --git a/pkg/cmd/history_test.go b/pkg/cmd/history_test.go index 2bace98e0..a8e1b6229 100644 --- a/pkg/cmd/history_test.go +++ b/pkg/cmd/history_test.go @@ -17,8 +17,10 @@ limitations under the License. package cmd import ( + "bytes" "encoding/json" "fmt" + "strings" "testing" "time" @@ -30,6 +32,31 @@ import ( release "helm.sh/helm/v4/pkg/release/v1" ) +// Regression test for #31902: table timestamps must be rendered in the +// local timezone (which Go derives from TZ) rather than the zone the +// release happened to be stored with. +func TestHistoryTableUsesLocalTime(t *testing.T) { + originalLocal := time.Local + time.Local = time.FixedZone("UTC+2", 2*60*60) + defer func() { time.Local = originalLocal }() + + h := releaseHistory{{ + Revision: 1, + Updated: time.Date(2026, 3, 4, 14, 38, 52, 0, time.UTC), + Status: "deployed", + Chart: "foo-0.1.0", + AppVersion: "1.0", + Description: "Install complete", + }} + + var buf bytes.Buffer + require.NoError(t, h.WriteTable(&buf)) + + if !strings.Contains(buf.String(), "Wed Mar 4 16:38:52 2026") { + t.Fatalf("expected timestamp rendered in local zone (UTC+2), got:\n%s", buf.String()) + } +} + func TestHistoryCmd(t *testing.T) { mk := func(name string, vers int, status common.Status) *release.Release { return release.Mock(&release.MockReleaseOptions{ diff --git a/pkg/cmd/list.go b/pkg/cmd/list.go index 54becddca..4b3867e33 100644 --- a/pkg/cmd/list.go +++ b/pkg/cmd/list.go @@ -171,9 +171,9 @@ func newReleaseListWriter(releases []*release.Release, timeFormat string, noHead t := "-" if tspb := r.Info.LastDeployed; !tspb.IsZero() { if timeFormat != "" { - t = tspb.Format(timeFormat) + t = tspb.Local().Format(timeFormat) } else { - t = tspb.String() + t = tspb.Local().String() } } element.Updated = t diff --git a/pkg/cmd/status.go b/pkg/cmd/status.go index 6998e341a..cfef344ef 100644 --- a/pkg/cmd/status.go +++ b/pkg/cmd/status.go @@ -147,7 +147,7 @@ func (s statusPrinter) WriteTable(out io.Writer) error { rel := s.getV1Release() _, _ = fmt.Fprintf(out, "NAME: %s\n", rel.Name) if !rel.Info.LastDeployed.IsZero() { - _, _ = fmt.Fprintf(out, "LAST DEPLOYED: %s\n", rel.Info.LastDeployed.Format(time.ANSIC)) + _, _ = fmt.Fprintf(out, "LAST DEPLOYED: %s\n", rel.Info.LastDeployed.Local().Format(time.ANSIC)) } _, _ = fmt.Fprintf(out, "NAMESPACE: %s\n", coloroutput.ColorizeNamespace(rel.Namespace, s.noColor)) _, _ = fmt.Fprintf(out, "STATUS: %s\n", coloroutput.ColorizeStatus(rel.Info.Status, s.noColor)) @@ -197,8 +197,8 @@ func (s statusPrinter) WriteTable(out io.Writer) error { } _, _ = fmt.Fprintf(out, "TEST SUITE: %s\n%s\n%s\n%s\n", h.Name, - "Last Started: "+h.LastRun.StartedAt.Format(time.ANSIC), - "Last Completed: "+h.LastRun.CompletedAt.Format(time.ANSIC), + "Last Started: "+h.LastRun.StartedAt.Local().Format(time.ANSIC), + "Last Completed: "+h.LastRun.CompletedAt.Local().Format(time.ANSIC), "Phase: "+h.LastRun.Phase, ) }