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 <nmukul32@gmail.com>
pull/32391/head
Mukul 3 months ago
parent ccb8f595ab
commit 05e9a98b5e

@ -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) {

@ -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)
}

@ -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{

@ -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

@ -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,
)
}

Loading…
Cancel
Save