From 6caac7345478e3d5f6aa25fe01d4075b53036b28 Mon Sep 17 00:00:00 2001 From: Sumit Solanki Date: Mon, 23 Mar 2026 15:52:10 +0530 Subject: [PATCH 1/2] fix(action): normalize EOF newline before kyaml parse in post-renderer pipeline Resolves: https://github.com/helm/helm/issues/31948 Signed-off-by: Sumit Solanki --- pkg/action/action.go | 15 +++++++++++++++ pkg/action/action_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/pkg/action/action.go b/pkg/action/action.go index 75b6cf8a0..6f71f4ff6 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -150,6 +150,19 @@ const ( // "---apiVersion: v1" which is not a valid YAML document separator. // This function inserts a newline after any "---" at the start of a line // that is immediately followed by non-whitespace content. +// ensureTrailingNewlineForYAML appends a final newline when the input omits one. +// kyaml's reader does not add a trailing newline to the last YAML document when +// splitting streams (see kio.ByteReader.Read); parsing and re-serializing then +// rewrites literal block scalars from "|" to "|-" (strip chomping). A trailing +// newline at EOF avoids that round-trip change so post-renderer merge/split +// matches direct template output. See https://github.com/helm/helm/issues/31948 +func ensureTrailingNewlineForYAML(content string) string { + if content == "" || strings.HasSuffix(content, "\n") { + return content + } + return content + "\n" +} + func fixDocSeparators(content string) string { var b strings.Builder remaining := content @@ -198,6 +211,7 @@ func annotateAndMerge(files map[string]string) (string, error) { // separator. Insert the missing newline so kio.ParseAll can // parse the content correctly. content = fixDocSeparators(content) + content = ensureTrailingNewlineForYAML(content) manifests, err := kio.ParseAll(content) if err != nil { @@ -221,6 +235,7 @@ func annotateAndMerge(files map[string]string) (string, error) { // splitAndDeannotate reconstructs individual files from a merged YAML stream, // removing filename annotations and grouping documents by their original filenames. func splitAndDeannotate(postrendered string) (map[string]string, error) { + postrendered = ensureTrailingNewlineForYAML(postrendered) manifests, err := kio.ParseAll(postrendered) if err != nil { return nil, fmt.Errorf("error parsing YAML: %w", err) diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 5271e34fa..6512aad8c 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -710,6 +710,31 @@ metadata: } } +// Regression: https://github.com/helm/helm/issues/31948 — kyaml parse/serialize +// without a trailing newline rewrites "|" block scalars to "|-" unless we normalize EOF. +func TestAnnotateAndMerge_PreservesLiteralBlockScalarWithoutEOFNewline(t *testing.T) { + files := map[string]string{ + "templates/cm.yaml": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config +data: + value: | + my value + with multiple lines`, + } + merged, err := annotateAndMerge(files) + require.NoError(t, err) + assert.Contains(t, merged, "value: |") + assert.NotContains(t, merged, "value: |-") + + reconstructed, err := splitAndDeannotate(merged) + require.NoError(t, err) + out := reconstructed["templates/cm.yaml"] + assert.Contains(t, out, "value: |") + assert.NotContains(t, out, "value: |-") +} + func TestSplitAndDeannotate(t *testing.T) { tests := []struct { name string From 58e71f16771381854aa79ce5cf392aa601a08da6 Mon Sep 17 00:00:00 2001 From: Sumit Solanki Date: Mon, 23 Mar 2026 16:22:55 +0530 Subject: [PATCH 2/2] Update pkg/action/action_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Sumit Solanki --- pkg/action/action_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 6512aad8c..447ee2598 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -728,11 +728,20 @@ data: assert.Contains(t, merged, "value: |") assert.NotContains(t, merged, "value: |-") + // Original behavior: splitAndDeannotate on the merged stream as produced. reconstructed, err := splitAndDeannotate(merged) require.NoError(t, err) out := reconstructed["templates/cm.yaml"] assert.Contains(t, out, "value: |") assert.NotContains(t, out, "value: |-") + + // New regression coverage: simulate a post-renderer output stream lacking a trailing newline. + mergedNoEOF := strings.TrimSuffix(merged, "\n") + reconstructedNoEOF, err := splitAndDeannotate(mergedNoEOF) + require.NoError(t, err) + outNoEOF := reconstructedNoEOF["templates/cm.yaml"] + assert.Contains(t, outNoEOF, "value: |") + assert.NotContains(t, outNoEOF, "value: |-") } func TestSplitAndDeannotate(t *testing.T) {