From ea0a7be62aa96666b5997a935da617aa5b56c517 Mon Sep 17 00:00:00 2001 From: Ilyes512 Date: Sat, 16 May 2026 19:08:57 +0200 Subject: [PATCH 1/3] fix: use texttemplate.New in extractRefs so Go builtins like eq are recognised MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parse.New (the raw parser) does not register Go's builtin template functions (eq, ne, and, or, not, …). When a computed value expression used one of those builtins, extractRefs silently returned nil (parse error swallowed), the topo sort saw no computed-key dependencies, and PhpDockerTag (or any such value) could be scheduled before the plain-string computed values it depends on — producing a 'map has no entry for key' error at execution time. Fix: replace parse.New(t).Parse with texttemplate.New().Delims.Funcs.Parse, the same path used by renderExpr, so both builtins and Sprout functions are available to the parser. Add a regression test that covers nested ternary with custom delimiters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pkg/template/context.go | 12 ++++++++---- pkg/template/context_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/pkg/template/context.go b/pkg/template/context.go index ade9cdc..8896a23 100644 --- a/pkg/template/context.go +++ b/pkg/template/context.go @@ -272,18 +272,22 @@ func topoSort(keys []string, deps map[string][]string) ([]string, error) { // extractRefs parses a delimited template expression and returns all // top-level .Key references found in it. +// It uses texttemplate.New (not parse.New) so that Go's builtin functions +// such as eq, ne, and, or, not are registered and the parser accepts them. func extractRefs(expr string, funcMap texttemplate.FuncMap, delims specs.Delimiters) []string { if !strings.Contains(expr, delims.Left) { return nil } - funcs := map[string]any(funcMap) - tree, err := parse.New("t").Parse(expr, delims.Left, delims.Right, map[string]*parse.Tree{}, funcs) - if err != nil || tree == nil || tree.Root == nil { + tmpl, err := texttemplate.New(""). + Delims(delims.Left, delims.Right). + Funcs(funcMap). + Parse(expr) + if err != nil || tmpl == nil || tmpl.Tree == nil || tmpl.Tree.Root == nil { return nil // parse errors surface during actual rendering } seen := make(map[string]bool) var refs []string - walkForRefs(tree.Root, seen, &refs) + walkForRefs(tmpl.Tree.Root, seen, &refs) return refs } diff --git a/pkg/template/context_test.go b/pkg/template/context_test.go index 5c06cb5..01b1781 100644 --- a/pkg/template/context_test.go +++ b/pkg/template/context_test.go @@ -246,6 +246,32 @@ computed: } } +func TestApplyComputed_ChainWithCustomDelimitersAndBuiltins(t *testing.T) { + // Regression: extractRefs used parse.New (raw parser) which does not include + // Go's builtin template functions (eq, ne, and, or, not, …). Expressions using + // those builtins silently produced nil deps, causing non-deterministic topo sort + // and a "map has no entry" error at execution time. + ctx := map[string]any{"PhpVersion": "8.5"} + delims := specs.Delimiters{Left: "[[", Right: "]]"} + defs := map[string]string{ + "Php85DockerTag": "0.5.3", + "Php84DockerTag": "1.5.3", + "Php83DockerTag": "1.8.3", + "PhpDockerTag": `[[ eq .PhpVersion "8.4" | ternary .Php84DockerTag (eq .PhpVersion "8.3" | ternary .Php83DockerTag .Php85DockerTag) ]]`, + } + + result, err := pkgtemplate.ApplyComputed(ctx, defs, pkgtemplate.FuncMap(pkgtemplate.Config{}), delims) + if err != nil { + t.Fatalf("ApplyComputed: %v", err) + } + if result["PhpDockerTag"] != "0.5.3" { + t.Errorf("PhpDockerTag = %q, want %q", result["PhpDockerTag"], "0.5.3") + } + if result["Php84DockerTag"] != "1.5.3" { + t.Errorf("Php84DockerTag = %q, want %q", result["Php84DockerTag"], "1.5.3") + } +} + func TestApplyComputed_NoDefs(t *testing.T) { ctx := map[string]any{"Name": "test"} result, err := pkgtemplate.ApplyComputed(ctx, nil, pkgtemplate.FuncMap(pkgtemplate.Config{}), specs.DefaultDelimiters) From 755e5b4c0f2be6eefb80d9aa3b56136ba26fa7ad Mon Sep 17 00:00:00 2001 From: Ilyes512 Date: Sat, 16 May 2026 19:12:51 +0200 Subject: [PATCH 2/3] fix: log debug warning when extractRefs silently drops a parse error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously a parse failure in extractRefs was completely invisible — no log at any level, not even with --debug. This made the root cause of topo-sort bugs very hard to find. Now the error is emitted at slog.Debug so it shows up when --debug is passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pkg/template/context.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/template/context.go b/pkg/template/context.go index 8896a23..d9f0dec 100644 --- a/pkg/template/context.go +++ b/pkg/template/context.go @@ -283,6 +283,7 @@ func extractRefs(expr string, funcMap texttemplate.FuncMap, delims specs.Delimit Funcs(funcMap). Parse(expr) if err != nil || tmpl == nil || tmpl.Tree == nil || tmpl.Tree.Root == nil { + slog.Debug("extractRefs: failed to parse expression; dependency detection skipped", "err", err) return nil // parse errors surface during actual rendering } seen := make(map[string]bool) From 54fe0198d47f715892fe23f8b0be962bc5ecfd51 Mon Sep 17 00:00:00 2001 From: Ilyes512 Date: Sat, 16 May 2026 19:17:24 +0200 Subject: [PATCH 3/3] fix: log debug warning when analyseExpr silently drops a parse error Mirrors the same improvement made to extractRefs: parse failures in analyseExpr were completely invisible at all log levels. A failing parse here causes conditional-analysis to be skipped for that expression (safe, conservative), but was impossible to diagnose. Now emits slog.Debug so --debug surfaces the issue. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pkg/template/analysis.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/template/analysis.go b/pkg/template/analysis.go index 342e484..5473cde 100644 --- a/pkg/template/analysis.go +++ b/pkg/template/analysis.go @@ -3,6 +3,7 @@ package template import ( "fmt" "io/fs" + "log/slog" "os" "path/filepath" "strings" @@ -91,6 +92,9 @@ func analyseExpr( } tmpl, err := texttemplate.New("").Delims(delims.Left, delims.Right).Funcs(funcMap).Parse(src) if err != nil || tmpl == nil || tmpl.Tree == nil || tmpl.Tree.Root == nil { + if err != nil { + slog.Debug("analyseExpr: failed to parse expression; conditional analysis skipped", "err", err) + } return } walkNode(tmpl.Tree.Root, outerGate, funcMap, conds, always)