fix: ensure post-renderer is applied when --output-dir-template is used

When helmfile template is run with both --output-dir (from --output-dir-template)
and --post-renderer, Helm writes template files to the output directory before
invoking the post-renderer, bypassing it entirely.

Fix this by stripping --output-dir from the flags passed to Helm when a
post-renderer is also specified. Helm then sends all rendered manifests through
the post-renderer to stdout. writeRenderedManifests() captures that output and
writes it to the output directory, splitting documents by their '# Source:'
comments to recreate the per-template file structure. Documents without a
'# Source:' comment (e.g. resources injected by the post-renderer) are
collected into a single fallback 'manifest.yaml' file.

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/38cec14e-e49f-4e90-918e-d400de52bd8e

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-09 13:01:12 +00:00
committed by GitHub
co-authored by yxxhero
parent f6652485d1
commit c6aa46234c
2 changed files with 196 additions and 12 deletions
+70
View File
@@ -1255,6 +1255,76 @@ exec: helm --kubeconfig config --kube-context dev template release https://examp
}
}
func Test_WriteRenderedManifests(t *testing.T) {
t.Run("sourced documents are written to per-file paths", func(t *testing.T) {
dir := t.TempDir()
content := []byte("---\n# Source: chart/templates/deployment.yaml\napiVersion: apps/v1\nkind: Deployment\n\n---\n# Source: chart/templates/service.yaml\napiVersion: v1\nkind: Service\n")
require.NoError(t, writeRenderedManifests(dir, content))
deployment, err := os.ReadFile(filepath.Join(dir, "chart/templates/deployment.yaml"))
require.NoError(t, err)
require.Contains(t, string(deployment), "kind: Deployment")
service, err := os.ReadFile(filepath.Join(dir, "chart/templates/service.yaml"))
require.NoError(t, err)
require.Contains(t, string(service), "kind: Service")
})
t.Run("documents without Source comment are written to manifest.yaml", func(t *testing.T) {
dir := t.TempDir()
content := []byte("---\napiVersion: v1\nkind: Namespace\nmetadata:\n name: prometheus\n")
require.NoError(t, writeRenderedManifests(dir, content))
manifest, err := os.ReadFile(filepath.Join(dir, "manifest.yaml"))
require.NoError(t, err)
require.Contains(t, string(manifest), "kind: Namespace")
})
t.Run("empty content is a no-op", func(t *testing.T) {
dir := t.TempDir()
require.NoError(t, writeRenderedManifests(dir, nil))
require.NoError(t, writeRenderedManifests(dir, []byte{}))
})
t.Run("mixed sourced and unsourced documents", func(t *testing.T) {
dir := t.TempDir()
content := []byte("---\n# Source: chart/templates/deployment.yaml\napiVersion: apps/v1\nkind: Deployment\n\n---\napiVersion: v1\nkind: Namespace\nmetadata:\n name: added-by-post-renderer\n")
require.NoError(t, writeRenderedManifests(dir, content))
deployment, err := os.ReadFile(filepath.Join(dir, "chart/templates/deployment.yaml"))
require.NoError(t, err)
require.Contains(t, string(deployment), "kind: Deployment")
manifest, err := os.ReadFile(filepath.Join(dir, "manifest.yaml"))
require.NoError(t, err)
require.Contains(t, string(manifest), "kind: Namespace")
})
}
func Test_TemplateRelease_OutputDirWithPostRenderer(t *testing.T) {
// When both --output-dir and --post-renderer are specified, TemplateRelease
// should strip --output-dir from the flags forwarded to helm so that the
// post-renderer actually receives the rendered manifests.
outputDir := t.TempDir()
manifests := "---\n# Source: chart/templates/configmap.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: test\n"
runner := &mockRunner{output: []byte(manifests)}
logger := NewLogger(io.Discard, "error")
helm, err := New("helm", HelmExecOptions{}, logger, "", "", runner)
require.NoError(t, err)
err = helm.TemplateRelease("release", "chart",
"--output-dir", outputDir,
"--post-renderer", "./my-post-renderer.sh",
)
require.NoError(t, err)
// The file should exist and contain the (mock) post-rendered content.
content, err := os.ReadFile(filepath.Join(outputDir, "chart/templates/configmap.yaml"))
require.NoError(t, err)
require.Contains(t, string(content), "kind: ConfigMap")
}
func Test_IsHelm3(t *testing.T) {
helm3Runner := mockRunner{output: []byte("v3.0.0+ge29ce2a\n")}
helm, err := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm3Runner)