From 4f287ed842e7d9772f0e8172040f9657044e5251 Mon Sep 17 00:00:00 2001 From: Richard Hopper Date: Wed, 27 May 2026 19:35:16 +0000 Subject: [PATCH] fix: validate include 'taskfile' field and improve error message When a user accidentally uses 'includes:' with variable-like content (e.g., 'GOBIN: {sh: ...}') instead of 'vars:', the previous behavior was to silently accept it and later report a misleading 'include cycle detected' error, since the empty Taskfile path resolved to the same file. This fix validates that each include entry has a non-empty 'taskfile' field during YAML parsing and returns a clear, actionable error message suggesting the user may have meant to use 'vars:' instead. Closes #1881 --- task_test.go | 19 +++++++++++++++++ taskfile/ast/include.go | 21 +++++++++++++++++++ .../includes_missing_taskfile/Taskfile.yml | 9 ++++++++ .../TaskfileCommon.yml | 10 +++++++++ 4 files changed, 59 insertions(+) create mode 100644 testdata/includes_missing_taskfile/Taskfile.yml create mode 100644 testdata/includes_missing_taskfile/TaskfileCommon.yml diff --git a/task_test.go b/task_test.go index 80915c2c47..b676f31d94 100644 --- a/task_test.go +++ b/task_test.go @@ -1064,6 +1064,25 @@ func TestIncludeCycle(t *testing.T) { assert.Contains(t, err.Error(), "task: include cycle detected between") } +func TestIncludesMissingTaskfile(t *testing.T) { + t.Parallel() + + const dir = "testdata/includes_missing_taskfile" + + var buff bytes.Buffer + e := task.NewExecutor( + task.WithDir(dir), + task.WithStdout(&buff), + task.WithStderr(&buff), + task.WithSilent(true), + ) + + err := e.Setup() + require.Error(t, err) + assert.NotContains(t, err.Error(), "include cycle detected", "should not report a cycle error for a missing taskfile field") + assert.Contains(t, err.Error(), "missing the required \"taskfile\" field") +} + func TestIncludesIncorrect(t *testing.T) { t.Parallel() diff --git a/taskfile/ast/include.go b/taskfile/ast/include.go index 18090b662d..049c817b51 100644 --- a/taskfile/ast/include.go +++ b/taskfile/ast/include.go @@ -1,6 +1,7 @@ package ast import ( + "fmt" "iter" "sync" @@ -133,6 +134,26 @@ func (includes *Includes) UnmarshalYAML(node *yaml.Node) error { return errors.NewTaskfileDecodeError(err, node) } + // Validate that the include has a Taskfile path specified. + // If it doesn't, suggest the user may have meant to use 'vars:' instead + // of 'includes:' — a common copy-paste mistake that otherwise produces + // a misleading "include cycle detected" error downstream. + if v.Taskfile == "" { + rawKeys := []string{} + for j := 0; j < len(valueNode.Content); j += 2 { + rawKeys = append(rawKeys, valueNode.Content[j].Value) + } + keysStr := "" + if len(rawKeys) > 0 { + keysStr = fmt.Sprintf(" (unexpected keys: %v)", rawKeys) + } + err := fmt.Errorf( + "include \"%s\" is missing the required \"taskfile\" field%s; did you mean to use \"vars:\" instead?", + keyNode.Value, keysStr, + ) + return errors.NewTaskfileDecodeError(err, valueNode) + } + // Set the include namespace v.Namespace = keyNode.Value diff --git a/testdata/includes_missing_taskfile/Taskfile.yml b/testdata/includes_missing_taskfile/Taskfile.yml new file mode 100644 index 0000000000..1d14aed9a9 --- /dev/null +++ b/testdata/includes_missing_taskfile/Taskfile.yml @@ -0,0 +1,9 @@ +version: '3' + +includes: + common: ./TaskfileCommon.yml + +tasks: + test: + cmds: + - task: common:test diff --git a/testdata/includes_missing_taskfile/TaskfileCommon.yml b/testdata/includes_missing_taskfile/TaskfileCommon.yml new file mode 100644 index 0000000000..d38206a9f7 --- /dev/null +++ b/testdata/includes_missing_taskfile/TaskfileCommon.yml @@ -0,0 +1,10 @@ +version: '3' + +includes: + GOBIN: + sh: echo $(go env GOPATH)/bin + +tasks: + test: + cmds: + - "{{.GOBIN}}/richgo test ./..."