From 0c8ab13223c4f6c75392763eea3987af08eb39be Mon Sep 17 00:00:00 2001 From: barry3406 Date: Sat, 11 Apr 2026 09:38:08 -0700 Subject: [PATCH 1/2] fix(engine): render whole-number floats as TOML integers Helm values round-trip through JSON, so every YAML number enters the template engine as a float64 regardless of whether the source was written as "9" or "9.0". The BurntSushi TOML encoder inspects the Go type via reflection and writes float64(9) as "9.0", which surprises users whose values look like integers. encoding/json already omits the trailing ".0" for whole-number float64 values, so toJson does not exhibit the same issue; this change brings toToml in line with toJson. The fix normalizes whole-number float64 values to int64 inside toToml only, leaving NaN, Inf, non-whole floats, and out-of-range values untouched. Scoping the change to the encoding path preserves the in-template type of values returned by typeOf/typeIs, avoiding the ecosystem regression that caused #13533 to be reverted in #30884. Closes #32035 Signed-off-by: barry3406 --- pkg/engine/funcs.go | 48 ++++++++++++++++++++++++++++++++++++++-- pkg/engine/funcs_test.go | 26 ++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index e03c13b38..b928dfedd 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -20,6 +20,7 @@ import ( "bytes" "encoding/json" "maps" + "math" "strings" "text/template" @@ -157,7 +158,7 @@ func fromYAMLArray(str string) []any { func toTOML(v any) string { b := bytes.NewBuffer(nil) e := toml.NewEncoder(b) - err := e.Encode(v) + err := e.Encode(normalizeForTOML(v)) if err != nil { return err.Error() } @@ -172,13 +173,56 @@ func toTOML(v any) string { func mustToTOML(v any) string { b := bytes.NewBuffer(nil) e := toml.NewEncoder(b) - err := e.Encode(v) + err := e.Encode(normalizeForTOML(v)) if err != nil { panic(err) } return b.String() } +// normalizeForTOML walks v and rewrites any float64 that is a whole number +// (within the int64 range) as an int64. Helm values round-trip through JSON, +// so every YAML number arrives in the template engine as a float64 regardless +// of whether the source was written as "9" or "9.0"; the BurntSushi TOML +// encoder then writes float64(9) as "9.0", which surprises users. This brings +// toToml in line with encoding/json (which already drops trailing ".0" for +// whole-number float64). Non-whole floats and values outside the int64 range +// are left untouched so that true floats still round-trip as floats. +// +// This fix is intentionally scoped to the TOML encoding path. #13533 solved +// the same issue by switching the values loader to json.Decoder.UseNumber, +// which changed every numeric value to json.Number globally and broke charts +// relying on typeOf/typeIs returning "float64" (#30880). Normalizing only +// inside toTOML preserves the in-template type of values so that regression +// cannot recur. +func normalizeForTOML(v any) any { + switch x := v.(type) { + case map[string]any: + out := make(map[string]any, len(x)) + for k, val := range x { + out[k] = normalizeForTOML(val) + } + return out + case []any: + out := make([]any, len(x)) + for i, val := range x { + out[i] = normalizeForTOML(val) + } + return out + case float64: + // Guard against NaN/Inf and against Go's implementation-defined + // behavior for out-of-range float-to-int conversions. 1<<63 is the + // first positive float64 strictly greater than math.MaxInt64. + if math.IsNaN(x) || math.IsInf(x, 0) || x != math.Trunc(x) || + x >= 1<<63 || x < -(1<<63) { + return x + } + return int64(x) + default: + return v + } +} + // fromTOML converts a TOML document into a map[string]interface{}. // // This is not a general-purpose TOML parser, and will not parse all valid diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index be9d0153f..a76a9810b 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -86,6 +86,32 @@ keyInElement1 = "valueInElement1"`, tpl: `{{ toToml . }}`, expect: "[mast]\n sail = \"white\"\n", vars: map[string]map[string]string{"mast": {"sail": "white"}}, + }, { + // Regression for https://github.com/helm/helm/issues/32035 + // Helm values round-trip through JSON so YAML integers arrive as + // float64. toToml must still emit them as TOML integers, matching + // what toJson already does. Fractional floats must be preserved. + tpl: `{{ toToml . }}`, + expect: "bar = 9\nbaz = 9.5\n\n[[items]]\n id = 1\n\n" + + "[[items]]\n id = 2\n\n[nested]\n deep = 42\n", + vars: map[string]any{ + "bar": float64(9), + "baz": float64(9.5), + "nested": map[string]any{ + "deep": float64(42), + }, + "items": []any{ + map[string]any{"id": float64(1)}, + map[string]any{"id": float64(2)}, + }, + }, + }, { + // Regression for https://github.com/helm/helm/issues/32035 + // Non-finite floats and floats outside the int64 range must stay + // floats so that true floats still round-trip as floats. + tpl: `{{ toToml . }}`, + expect: "big = 1e+20\n", + vars: map[string]any{"big": float64(1e20)}, }, { tpl: `{{ fromYaml . }}`, expect: "map[Error:error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go value of type map[string]interface {}]", From 8773bccb2153fb7c4106f9d948953d9dd9bf106a Mon Sep 17 00:00:00 2001 From: barry3406 Date: Thu, 16 Apr 2026 11:28:30 -0700 Subject: [PATCH 2/2] test(engine): cover zero and negative whole-number floats in toToml Add a regression case exercising float64(0), float64(-7), and the negative fractional float64(-3.25) so the normalization is exercised across the full int64 range (not just positive values) and so negative fractional floats are shown to still round-trip as TOML floats. Requested by @TerryHowe on #32040. Signed-off-by: barry3406 --- pkg/engine/funcs_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index a76a9810b..00ffc8fdb 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -112,6 +112,17 @@ keyInElement1 = "valueInElement1"`, tpl: `{{ toToml . }}`, expect: "big = 1e+20\n", vars: map[string]any{"big": float64(1e20)}, + }, { + // Regression for https://github.com/helm/helm/issues/32035 + // Zero and negative whole-number floats must also render as TOML + // integers, and negative fractional floats must stay floats. + tpl: `{{ toToml . }}`, + expect: "neg = -7\nnegFrac = -3.25\nzero = 0\n", + vars: map[string]any{ + "zero": float64(0), + "neg": float64(-7), + "negFrac": float64(-3.25), + }, }, { tpl: `{{ fromYaml . }}`, expect: "map[Error:error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go value of type map[string]interface {}]",