mirror of
				https://github.com/go-task/task.git
				synced 2025-10-30 23:58:01 +02:00 
			
		
		
		
	feat: add option to declare an included Taskfile as flatten (#1704)
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
							
								
								
									
										37
									
								
								task_test.go
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								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 { | ||||
|   | ||||
| @@ -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, | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
							
								
								
									
										1
									
								
								testdata/includes_flatten/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								testdata/includes_flatten/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| *.txt | ||||
							
								
								
									
										12
									
								
								testdata/includes_flatten/Taskfile.multiple.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								testdata/includes_flatten/Taskfile.multiple.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| version: '3' | ||||
|  | ||||
| includes: | ||||
|   included: | ||||
|     taskfile: ./included | ||||
|     flatten: true | ||||
|  | ||||
| tasks: | ||||
|   gen: | ||||
|     cmds: | ||||
|       - echo "gen multiple" | ||||
|  | ||||
							
								
								
									
										13
									
								
								testdata/includes_flatten/Taskfile.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								testdata/includes_flatten/Taskfile.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| version: '3' | ||||
|  | ||||
| includes: | ||||
|   included: | ||||
|     taskfile: ./included | ||||
|     dir: ./included | ||||
|     flatten: true | ||||
|  | ||||
| tasks: | ||||
|   default: | ||||
|     cmds: | ||||
|       - echo root_directory > root_directory.txt | ||||
|  | ||||
							
								
								
									
										23
									
								
								testdata/includes_flatten/included/Taskfile.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								testdata/includes_flatten/included/Taskfile.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										6
									
								
								testdata/includes_flatten/nested/Taskfile.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								testdata/includes_flatten/nested/Taskfile.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| version: '3' | ||||
|  | ||||
| tasks: | ||||
|   from_nested: | ||||
|     cmds: | ||||
|       - echo "from nested" | ||||
| @@ -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.                                                                                                                                                                                                    | | ||||
|   | ||||
| @@ -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. | ||||
|  | ||||
|  | ||||
| <Tabs defaultValue="1" | ||||
| 			values={[ | ||||
| 				{label: 'Taskfile.yml', value: '1'}, | ||||
| 				{label: 'Included.yml', value: '2'} | ||||
| 			]}> | ||||
|  | ||||
| 	<TabItem value="1"> | ||||
|  | ||||
| 		```yaml | ||||
| 		version: '3' | ||||
|  | ||||
| 		includes: | ||||
| 		  lib: | ||||
| 		    taskfile: ./Included.yml | ||||
| 		    flatten: true | ||||
|  | ||||
| 		tasks: | ||||
| 		  greet: | ||||
| 		    cmds: | ||||
| 		      - echo "Greet" | ||||
| 		      - task: foo | ||||
| 		``` | ||||
|  | ||||
|  | ||||
| 	</TabItem> | ||||
| 	<TabItem value="2"> | ||||
|  | ||||
| 		```yaml | ||||
| 		version: '3' | ||||
|  | ||||
| 		tasks: | ||||
| 		  foo: | ||||
| 		    cmds: | ||||
| 		      - echo "Foo" | ||||
| 		``` | ||||
|  | ||||
|  | ||||
| 	</TabItem></Tabs> | ||||
|  | ||||
|  | ||||
| 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: | ||||
|  | ||||
| <Tabs defaultValue="1" | ||||
| 			values={[ | ||||
| 				{label: 'Taskfile.yml', value: '1'}, | ||||
| 				{label: 'Included.yml', value: '2'} | ||||
| 			]}> | ||||
|  | ||||
| 	<TabItem value="1"> | ||||
|  | ||||
|     ```yaml | ||||
|     version: '3' | ||||
|     includes: | ||||
|       lib: | ||||
| 		    taskfile: ./Included.yml | ||||
| 		    flatten: true | ||||
|  | ||||
| 		tasks: | ||||
| 		  greet: | ||||
| 		    cmds: | ||||
| 		      - echo "Greet" | ||||
| 		      - task: foo | ||||
| 		``` | ||||
|  | ||||
|  | ||||
| 	</TabItem> | ||||
| 	<TabItem value="2"> | ||||
|  | ||||
| 		```yaml | ||||
| 		version: '3' | ||||
|  | ||||
| 		tasks: | ||||
| 		  greet: | ||||
| 		    cmds: | ||||
| 		       - echo "Foo" | ||||
| 		``` | ||||
|  | ||||
|  | ||||
| 	</TabItem></Tabs> | ||||
|  | ||||
| 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 | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user