From 14a40eb23f5ad5bfb7d27c787edb9b263acbf2c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Apr 2026 09:51:52 +0000 Subject: [PATCH] fix: detect --post-renderer= form and use targeted file cleanup Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/8c9e4af4-84ae-4cbd-bc0a-8fcd9adddaed Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com> --- pkg/helmexec/exec.go | 11 +++--- pkg/helmexec/exec_test.go | 82 +++++++++++++++++++++++---------------- 2 files changed, 54 insertions(+), 39 deletions(-) diff --git a/pkg/helmexec/exec.go b/pkg/helmexec/exec.go index 9478dd41..a2153576 100644 --- a/pkg/helmexec/exec.go +++ b/pkg/helmexec/exec.go @@ -653,7 +653,7 @@ func (helm *execer) TemplateRelease(name string, chart string, flags ...string) if f == "--output-dir" || strings.HasPrefix(f, "--output-dir=") { outputToFile = true } - if f == "--post-renderer" { + if f == "--post-renderer" || strings.HasPrefix(f, "--post-renderer=") { hasPostRenderer = true } } @@ -690,14 +690,16 @@ func (helm *execer) TemplateRelease(name string, chart string, flags ...string) templatesDir := filepath.Join(outputDir, "templates") legacyOutputPath := filepath.Join(outputDir, name+".yaml") + outputPath := filepath.Join(templatesDir, name+".yaml") if removeErr := os.Remove(legacyOutputPath); removeErr != nil && !os.IsNotExist(removeErr) { return fmt.Errorf("failed to remove legacy output file %s: %w", legacyOutputPath, removeErr) } - // Clean up any stale files from previous runs before writing new output. - if removeErr := os.RemoveAll(templatesDir); removeErr != nil { - return fmt.Errorf("failed to remove stale templates directory %s: %w", templatesDir, removeErr) + // Remove only the specific file written by the previous run to avoid clobbering + // unrelated files in a shared output directory. + if removeErr := os.Remove(outputPath); removeErr != nil && !os.IsNotExist(removeErr) { + return fmt.Errorf("failed to remove stale output file %s: %w", outputPath, removeErr) } if len(out) > 0 { @@ -705,7 +707,6 @@ func (helm *execer) TemplateRelease(name string, chart string, flags ...string) return fmt.Errorf("failed to create templates directory %s: %w", templatesDir, mkdirErr) } - outputPath := filepath.Join(templatesDir, name+".yaml") if writeErr := os.WriteFile(outputPath, append(out, '\n'), 0644); writeErr != nil { return fmt.Errorf("failed to write output file %s: %w", outputPath, writeErr) } diff --git a/pkg/helmexec/exec_test.go b/pkg/helmexec/exec_test.go index 109c6b30..2d4dc577 100644 --- a/pkg/helmexec/exec_test.go +++ b/pkg/helmexec/exec_test.go @@ -1262,46 +1262,60 @@ exec: helm --kubeconfig config --kube-context dev template release https://examp } func Test_Template_PostRendererWithOutputDir(t *testing.T) { - tmpDir := t.TempDir() - var buffer bytes.Buffer - logger := NewLogger(&buffer, "debug") - - // Use Helm 3 version for the version probe so the Helm 3 workaround is applied. - // The workaround is not needed for Helm 4, which natively applies --post-renderer to --output-dir output. - runner := &mockRunner{versionOutput: []byte("v3.20.0")} - helm, err := New("helm", HelmExecOptions{}, logger, "config", "dev", runner) - if err != nil { - t.Fatalf("unexpected error: %v", err) + tests := []struct { + name string + postRendererFlag string + }{ + {"separate flags", "--post-renderer"}, + {"combined flag", "--post-renderer=/bin/echo"}, } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + var buffer bytes.Buffer + logger := NewLogger(&buffer, "debug") - runner.output = []byte("apiVersion: v1\nkind: Namespace\n") + // Use Helm 3 version for the version probe so the Helm 3 workaround is applied. + // The workaround is not needed for Helm 4, which natively applies --post-renderer to --output-dir output. + runner := &mockRunner{versionOutput: []byte("v3.20.0")} + helm, err := New("helm", HelmExecOptions{}, logger, "config", "dev", runner) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - err = helm.TemplateRelease("myrelease", "path/to/chart", - "--post-renderer", "/bin/echo", - "--output-dir", tmpDir, - "--values", "file.yml", - ) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + runner.output = []byte("apiVersion: v1\nkind: Namespace\n") - outputPath := filepath.Join(tmpDir, "templates", "myrelease.yaml") - data, err := os.ReadFile(outputPath) - if err != nil { - t.Fatalf("expected output file %s to exist: %v", outputPath, err) - } + var flags []string + if tt.postRendererFlag == "--post-renderer" { + flags = []string{"--post-renderer", "/bin/echo", "--output-dir", tmpDir, "--values", "file.yml"} + } else { + flags = []string{tt.postRendererFlag, "--output-dir", tmpDir, "--values", "file.yml"} + } - expected := "apiVersion: v1\nkind: Namespace\n\n" - if string(data) != expected { - t.Errorf("output file content:\nactual=%q\nexpect=%q", string(data), expected) - } + err = helm.TemplateRelease("myrelease", "path/to/chart", flags...) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - outputLog := buffer.String() - if strings.Contains(outputLog, "--output-dir") { - t.Errorf("helm should NOT have been called with --output-dir, got: %s", outputLog) - } - if !strings.Contains(outputLog, "--post-renderer") { - t.Errorf("helm should have been called with --post-renderer, got: %s", outputLog) + outputPath := filepath.Join(tmpDir, "templates", "myrelease.yaml") + data, err := os.ReadFile(outputPath) + if err != nil { + t.Fatalf("expected output file %s to exist: %v", outputPath, err) + } + + expected := "apiVersion: v1\nkind: Namespace\n\n" + if string(data) != expected { + t.Errorf("output file content:\nactual=%q\nexpect=%q", string(data), expected) + } + + outputLog := buffer.String() + if strings.Contains(outputLog, "--output-dir") { + t.Errorf("helm should NOT have been called with --output-dir, got: %s", outputLog) + } + if !strings.Contains(outputLog, "--post-renderer") { + t.Errorf("helm should have been called with --post-renderer, got: %s", outputLog) + } + }) } }