From ef3b8537281e10e6dca2e1c8491e9ab85ea4e744 Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Mon, 26 Aug 2024 23:17:39 +0200 Subject: [PATCH] feat: add option to declare an included Taskfile as flatten (#1704) --- .editorconfig | 2 +- errors/errors_task.go | 13 ++ task_test.go | 37 ++++++ taskfile/ast/include.go | 4 + taskfile/ast/taskfile.go | 3 +- taskfile/ast/tasks.go | 75 ++++++------ taskfile/reader.go | 1 + testdata/includes_flatten/.gitignore | 1 + .../includes_flatten/Taskfile.multiple.yml | 12 ++ testdata/includes_flatten/Taskfile.yml | 13 ++ .../includes_flatten/included/Taskfile.yml | 23 ++++ testdata/includes_flatten/nested/Taskfile.yml | 6 + website/docs/reference/schema.mdx | 5 +- website/docs/usage.mdx | 111 ++++++++++++++++++ website/static/schema.json | 4 + 15 files changed, 272 insertions(+), 38 deletions(-) create mode 100644 testdata/includes_flatten/.gitignore create mode 100644 testdata/includes_flatten/Taskfile.multiple.yml create mode 100644 testdata/includes_flatten/Taskfile.yml create mode 100644 testdata/includes_flatten/included/Taskfile.yml create mode 100644 testdata/includes_flatten/nested/Taskfile.yml 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"