diff --git a/.editorconfig b/.editorconfig
index d37c349a..8bb6a753 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -8,6 +8,6 @@ charset = utf-8
trim_trailing_whitespace = true
indent_style = tab
-[*.{md,yml,yaml,json,toml,htm,html,js,css,svg,sh,bash,fish}]
+[*.{md,mdx,yml,yaml,json,toml,htm,html,js,css,svg,sh,bash,fish}]
indent_style = space
indent_size = 2
diff --git a/errors/errors_task.go b/errors/errors_task.go
index bcbf1bb3..1214a5a9 100644
--- a/errors/errors_task.go
+++ b/errors/errors_task.go
@@ -80,6 +80,19 @@ func (err *TaskNameConflictError) Code() int {
return CodeTaskNameConflict
}
+type TaskNameFlattenConflictError struct {
+ TaskName string
+ Include string
+}
+
+func (err *TaskNameFlattenConflictError) Error() string {
+ return fmt.Sprintf(`task: Found multiple tasks (%s) included by "%s""`, err.TaskName, err.Include)
+}
+
+func (err *TaskNameFlattenConflictError) Code() int {
+ return CodeTaskNameConflict
+}
+
// TaskCalledTooManyTimesError is returned when the maximum task call limit is
// exceeded. This is to prevent infinite loops and cyclic dependencies.
type TaskCalledTooManyTimesError struct {
diff --git a/task_test.go b/task_test.go
index d8117eee..4ca92fb1 100644
--- a/task_test.go
+++ b/task_test.go
@@ -1230,6 +1230,43 @@ func TestIncludesInternal(t *testing.T) {
}
}
+func TestIncludesFlatten(t *testing.T) {
+ const dir = "testdata/includes_flatten"
+ tests := []struct {
+ name string
+ taskfile string
+ task string
+ expectedErr bool
+ expectedOutput string
+ }{
+ {name: "included flatten", taskfile: "Taskfile.yml", task: "gen", expectedOutput: "gen from included\n"},
+ {name: "included flatten with deps", taskfile: "Taskfile.yml", task: "with_deps", expectedOutput: "gen from included\nwith_deps from included\n"},
+ {name: "included flatten nested", taskfile: "Taskfile.yml", task: "from_nested", expectedOutput: "from nested\n"},
+ {name: "included flatten multiple same task", taskfile: "Taskfile.multiple.yml", task: "gen", expectedErr: true, expectedOutput: "task: Found multiple tasks (gen) included by \"included\"\""},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ var buff bytes.Buffer
+ e := task.Executor{
+ Dir: dir,
+ Entrypoint: dir + "/" + test.taskfile,
+ Stdout: &buff,
+ Stderr: &buff,
+ Silent: true,
+ }
+ err := e.Setup()
+ if test.expectedErr {
+ assert.EqualError(t, err, test.expectedOutput)
+ } else {
+ require.NoError(t, err)
+ _ = e.Run(context.Background(), &ast.Call{Task: test.task})
+ assert.Equal(t, test.expectedOutput, buff.String())
+ }
+ })
+ }
+}
+
func TestIncludesInterpolation(t *testing.T) {
const dir = "testdata/includes_interpolation"
tests := []struct {
diff --git a/taskfile/ast/include.go b/taskfile/ast/include.go
index defe7c44..659070de 100644
--- a/taskfile/ast/include.go
+++ b/taskfile/ast/include.go
@@ -17,6 +17,7 @@ type Include struct {
Aliases []string
AdvancedImport bool
Vars *Vars
+ Flatten bool
}
// Includes represents information about included tasksfiles
@@ -81,6 +82,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
Dir string
Optional bool
Internal bool
+ Flatten bool
Aliases []string
Vars *Vars
}
@@ -94,6 +96,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
include.Aliases = includedTaskfile.Aliases
include.AdvancedImport = true
include.Vars = includedTaskfile.Vars
+ include.Flatten = includedTaskfile.Flatten
return nil
}
@@ -114,5 +117,6 @@ func (include *Include) DeepCopy() *Include {
Internal: include.Internal,
AdvancedImport: include.AdvancedImport,
Vars: include.Vars.DeepCopy(),
+ Flatten: include.Flatten,
}
}
diff --git a/taskfile/ast/taskfile.go b/taskfile/ast/taskfile.go
index 42a8486a..9e0c0b79 100644
--- a/taskfile/ast/taskfile.go
+++ b/taskfile/ast/taskfile.go
@@ -55,8 +55,7 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
}
t1.Vars.Merge(t2.Vars, include)
t1.Env.Merge(t2.Env, include)
- t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
- return nil
+ return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
}
func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
diff --git a/taskfile/ast/tasks.go b/taskfile/ast/tasks.go
index b49a9a09..a440a096 100644
--- a/taskfile/ast/tasks.go
+++ b/taskfile/ast/tasks.go
@@ -47,43 +47,48 @@ func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask {
return matchingTasks
}
-func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) {
- _ = t2.Range(func(name string, v *Task) error {
+func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) error {
+ err := t2.Range(func(name string, v *Task) error {
// We do a deep copy of the task struct here to ensure that no data can
// be changed elsewhere once the taskfile is merged.
task := v.DeepCopy()
-
// Set the task to internal if EITHER the included task or the included
// taskfile are marked as internal
task.Internal = task.Internal || (include != nil && include.Internal)
-
- // Add namespaces to task dependencies
- for _, dep := range task.Deps {
- if dep != nil && dep.Task != "" {
- dep.Task = taskNameWithNamespace(dep.Task, include.Namespace)
- }
- }
-
- // Add namespaces to task commands
- for _, cmd := range task.Cmds {
- if cmd != nil && cmd.Task != "" {
- cmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace)
- }
- }
-
- // Add namespaces to task aliases
- for i, alias := range task.Aliases {
- task.Aliases[i] = taskNameWithNamespace(alias, include.Namespace)
- }
-
- // Add namespace aliases
- if include != nil {
- for _, namespaceAlias := range include.Aliases {
- task.Aliases = append(task.Aliases, taskNameWithNamespace(task.Task, namespaceAlias))
- for _, alias := range v.Aliases {
- task.Aliases = append(task.Aliases, taskNameWithNamespace(alias, namespaceAlias))
+ taskName := name
+ if !include.Flatten {
+ // Add namespaces to task dependencies
+ for _, dep := range task.Deps {
+ if dep != nil && dep.Task != "" {
+ dep.Task = taskNameWithNamespace(dep.Task, include.Namespace)
}
}
+
+ // Add namespaces to task commands
+ for _, cmd := range task.Cmds {
+ if cmd != nil && cmd.Task != "" {
+ cmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace)
+ }
+ }
+
+ // Add namespaces to task aliases
+ for i, alias := range task.Aliases {
+ task.Aliases[i] = taskNameWithNamespace(alias, include.Namespace)
+ }
+
+ // Add namespace aliases
+ if include != nil {
+ for _, namespaceAlias := range include.Aliases {
+ task.Aliases = append(task.Aliases, taskNameWithNamespace(task.Task, namespaceAlias))
+ for _, alias := range v.Aliases {
+ task.Aliases = append(task.Aliases, taskNameWithNamespace(alias, namespaceAlias))
+ }
+ }
+ }
+
+ taskName = taskNameWithNamespace(name, include.Namespace)
+ task.Namespace = include.Namespace
+ task.Task = taskName
}
if include.AdvancedImport {
@@ -95,11 +100,14 @@ func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) {
task.IncludedTaskfileVars = includedTaskfileVars.DeepCopy()
}
+ if t1.Get(taskName) != nil {
+ return &errors.TaskNameFlattenConflictError{
+ TaskName: taskName,
+ Include: include.Namespace,
+ }
+ }
// Add the task to the merged taskfile
- taskNameWithNamespace := taskNameWithNamespace(name, include.Namespace)
- task.Namespace = include.Namespace
- task.Task = taskNameWithNamespace
- t1.Set(taskNameWithNamespace, task)
+ t1.Set(taskName, task)
return nil
})
@@ -114,6 +122,7 @@ func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) {
t1.Get(defaultTaskName).Aliases = append(t1.Get(defaultTaskName).Aliases, include.Namespace)
t1.Get(defaultTaskName).Aliases = slices.Concat(t1.Get(defaultTaskName).Aliases, include.Aliases)
}
+ return err
}
func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
diff --git a/taskfile/reader.go b/taskfile/reader.go
index 7899ee09..32783dea 100644
--- a/taskfile/reader.go
+++ b/taskfile/reader.go
@@ -109,6 +109,7 @@ func (r *Reader) include(node Node) error {
Dir: templater.Replace(include.Dir, cache),
Optional: include.Optional,
Internal: include.Internal,
+ Flatten: include.Flatten,
Aliases: include.Aliases,
AdvancedImport: include.AdvancedImport,
Vars: include.Vars,
diff --git a/testdata/includes_flatten/.gitignore b/testdata/includes_flatten/.gitignore
new file mode 100644
index 00000000..2211df63
--- /dev/null
+++ b/testdata/includes_flatten/.gitignore
@@ -0,0 +1 @@
+*.txt
diff --git a/testdata/includes_flatten/Taskfile.multiple.yml b/testdata/includes_flatten/Taskfile.multiple.yml
new file mode 100644
index 00000000..87909649
--- /dev/null
+++ b/testdata/includes_flatten/Taskfile.multiple.yml
@@ -0,0 +1,12 @@
+version: '3'
+
+includes:
+ included:
+ taskfile: ./included
+ flatten: true
+
+tasks:
+ gen:
+ cmds:
+ - echo "gen multiple"
+
diff --git a/testdata/includes_flatten/Taskfile.yml b/testdata/includes_flatten/Taskfile.yml
new file mode 100644
index 00000000..1ed6cd9c
--- /dev/null
+++ b/testdata/includes_flatten/Taskfile.yml
@@ -0,0 +1,13 @@
+version: '3'
+
+includes:
+ included:
+ taskfile: ./included
+ dir: ./included
+ flatten: true
+
+tasks:
+ default:
+ cmds:
+ - echo root_directory > root_directory.txt
+
diff --git a/testdata/includes_flatten/included/Taskfile.yml b/testdata/includes_flatten/included/Taskfile.yml
new file mode 100644
index 00000000..c7af41dc
--- /dev/null
+++ b/testdata/includes_flatten/included/Taskfile.yml
@@ -0,0 +1,23 @@
+version: '3'
+
+includes:
+ nested:
+ taskfile: ../nested
+ flatten: true
+
+tasks:
+ gen:
+ cmds:
+ - echo "gen from included"
+
+ with_deps:
+ deps:
+ - gen
+ cmds:
+ - echo "with_deps from included"
+
+
+ pwd:
+ desc: Print working directory
+ cmds:
+ - pwd
diff --git a/testdata/includes_flatten/nested/Taskfile.yml b/testdata/includes_flatten/nested/Taskfile.yml
new file mode 100644
index 00000000..4bf281a6
--- /dev/null
+++ b/testdata/includes_flatten/nested/Taskfile.yml
@@ -0,0 +1,6 @@
+version: '3'
+
+tasks:
+ from_nested:
+ cmds:
+ - echo "from nested"
diff --git a/website/docs/reference/schema.mdx b/website/docs/reference/schema.mdx
index 7d37a315..e67a033c 100644
--- a/website/docs/reference/schema.mdx
+++ b/website/docs/reference/schema.mdx
@@ -8,7 +8,7 @@ toc_max_heading_level: 5
# Schema Reference
| Attribute | Type | Default | Description |
-| ---------- | ---------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+|------------|------------------------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `version` | `string` | | Version of the Taskfile. The current version is `3`. |
| `output` | `string` | `interleaved` | Output mode. Available options: `interleaved`, `group` and `prefixed`. |
| `method` | `string` | `checksum` | Default method in this Taskfile. Can be overridden in a task by task basis. Available options: `checksum`, `timestamp` and `none`. |
@@ -26,10 +26,11 @@ toc_max_heading_level: 5
## Include
| Attribute | Type | Default | Description |
-| ---------- | --------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+|------------|-----------------------|-------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `taskfile` | `string` | | The path for the Taskfile or directory to be included. If a directory, Task will look for files named `Taskfile.yml` or `Taskfile.yaml` inside that directory. If a relative path, resolved relative to the directory containing the including Taskfile. |
| `dir` | `string` | The parent Taskfile directory | The working directory of the included tasks when run. |
| `optional` | `bool` | `false` | If `true`, no errors will be thrown if the specified file does not exist. |
+| `flatten` | `bool` | `false` | If `true`, the tasks from the included Taskfile will be available in the including Taskfile without a namespace. If a task with the same name already exists in the including Taskfile, an error will be thrown. |
| `internal` | `bool` | `false` | Stops any task in the included Taskfile from being callable on the command line. These commands will also be omitted from the output when used with `--list`. |
| `aliases` | `[]string` | | Alternative names for the namespace of the included Taskfile. |
| `vars` | `map[string]Variable` | | A set of variables to apply to the included Taskfile. |
diff --git a/website/docs/usage.mdx b/website/docs/usage.mdx
index b71fb72c..77c80c89 100644
--- a/website/docs/usage.mdx
+++ b/website/docs/usage.mdx
@@ -334,6 +334,117 @@ includes:
internal: true
```
+### Flatten includes
+
+You can flatten the included Taskfile tasks into the main Taskfile by using the `flatten` option.
+It means that the included Taskfile tasks will be available without the namespace.
+
+
+
+
+
+
+ ```yaml
+ version: '3'
+
+ includes:
+ lib:
+ taskfile: ./Included.yml
+ flatten: true
+
+ tasks:
+ greet:
+ cmds:
+ - echo "Greet"
+ - task: foo
+ ```
+
+
+
+
+
+ ```yaml
+ version: '3'
+
+ tasks:
+ foo:
+ cmds:
+ - echo "Foo"
+ ```
+
+
+
+
+
+If you run `task -a` it will print :
+
+```sh
+task: Available tasks for this project:
+* greet:
+* foo
+```
+
+You can run `task foo` directly without the namespace.
+
+You can also reference the task in other tasks without the namespace. So if you run `task greet` it will run `greet` and `foo` tasks and the output will be :
+
+```text
+```
+
+If multiple tasks have the same name, an error will be thrown:
+
+
+
+
+
+ ```yaml
+ version: '3'
+ includes:
+ lib:
+ taskfile: ./Included.yml
+ flatten: true
+
+ tasks:
+ greet:
+ cmds:
+ - echo "Greet"
+ - task: foo
+ ```
+
+
+
+
+
+ ```yaml
+ version: '3'
+
+ tasks:
+ greet:
+ cmds:
+ - echo "Foo"
+ ```
+
+
+
+
+If you run `task -a` it will print:
+```text
+task: Found multiple tasks (greet) included by "lib"
+
+```
+
+
+
+
+
### Vars of included Taskfiles
You can also specify variables when including a Taskfile. This may be useful for
diff --git a/website/static/schema.json b/website/static/schema.json
index 899adff5..317213b4 100644
--- a/website/static/schema.json
+++ b/website/static/schema.json
@@ -610,6 +610,10 @@
"description": "If `true`, no errors will be thrown if the specified file does not exist.",
"type": "boolean"
},
+ "flatten": {
+ "description": "If `true`, the tasks from the included Taskfile will be available in the including Taskfile without a namespace. If a task with the same name already exists in the including Taskfile, an error will be thrown.",
+ "type": "boolean"
+ },
"internal": {
"description": "Stops any task in the included Taskfile from being callable on the command line. These commands will also be omitted from the output when used with `--list`.",
"type": "boolean"