Repository: go-task/task
Pull request #815 (2022-07-20 21:22:04)

feat: run default task in included file when task is omitted
I was experimenting with refactoring my (now very large) Taskfile into smaller, more manageable files and I came across #661. This functionality would really help us to make our tasks a bit simpler.

This PR adds the functionality mentioned by @tylermmorton and @andreynering in that issue. Namely:

- When a Taskfile with a `default` task inside is included, the `default` task is available by calling `task <namespace>` as opposed to `task <namespace>:default`.
- If the parent Taskfile contains a task with the same name as the namespace (i.e. the task is "shadowed"), the task in the _parent_ is run, and _not_ the default task in the included file.
  - This ensures that we maintain backward compatibility with existing Taskfiles.
- A couple of unit tests for ensuring the functionality works going forwards.

Note: Not sure about the "shadowed" terminology. Open to better suggestions.

An example below:

```yaml
# Taskfile.yml
version: '3'

includes:
  included:
    taskfile: Taskfile2.yml
```

```yaml
# Taskfile2.yml
version: '3'

tasks:
  default:
    cmds:
      - echo "included task"
```

`task included` will output: `included task`.
Currently you are required to run `task included:default`.

If we amend the parent taskfile like so:

```yaml
# Taskfile.yml
version: '3'

includes:
  included:
    taskfile: Taskfile2.yml

tasks:
  included:
    cmds:
      - echo "shadowed task"
```

`task included` will now output: `shadowed task` instead.

pd93: I've just seen #665. I didn't realise someone had already made an attempt at this. However, since the comments there don't seem to have been addressed, I've made an attempt to address them here instead.

> I think this implementation has unintended side-effects. Tasks assigned here will be duplicated on task --list.

@andreynering I tried out your suggestion in `variables.go`, but couldn't get it to work. This duplication (as I'm sure you know) is happening because we're merging the included task and then copying it to a new map key as well. The names are the same due to them sharing a memory address and the loop at the end of the `read.Taskfile` function is setting the name for _both_ map keys. Interestingly, because looping over maps is non-deterministic, the name is sometimes `docs:default` and sometimes `docs` depending on which map entry is looped over last.

I can see two fixes to this depending on the desired behaviour:

1. Copy the value so that they no longer share a pointer. This results in both `docs` and `docs:default` being written to the `--list` output.

```go
if includedTaskfile.Tasks["default"] != nil && t.Tasks[namespace] == nil {
	t.Tasks[namespace] = &taskfile.Task{}
	*t.Tasks[namespace] = *(includedTaskfile.Tasks["default"])
}
```

2. Delete the original map entry. This will result in _only_ `docs` being written to the `--list` output.

```go
if includedTaskfile.Tasks["default"] != nil && t.Tasks[namespace] == nil {
	t.Tasks[namespace] = includedTaskfile.Tasks["default"]
	delete(t.Tasks, namespace+":default")
}
```

IMO, the first is preferrable, since both `docs` and `docs:default` are still callable (and should remain so for backward compatibility). The second approach also breaks many tests 😞 

Patch:
diff --git a/docs/docs/contributing.md b/docs/docs/contributing.md
index db9c9708..1101ab0b 100644
--- a/docs/docs/contributing.md
+++ b/docs/docs/contributing.md
@@ -60,10 +60,8 @@ to add a new section. If you're updating an existing feature, ensure that the
 documentation and any examples are up-to-date. Ensure that any examples follow
 the [Taskfile Styleguide](./styleguide.md).
 
-If you added a new field, command or flag, ensure that you add it to the [API
-Reference](./api_reference.md). New fields also need to be added to the
-[JSON Schema](../static/schema.json). The descriptions for fields in the API
-reference and the schema should match.
+If you added a new command/flag, ensure that you add it to the [API
+Reference](./api_reference.md).
 
 ### Writing tests
 
diff --git a/taskfile/read/taskfile.go b/taskfile/read/taskfile.go
index d0ee697b..fd567916 100644
--- a/taskfile/read/taskfile.go
+++ b/taskfile/read/taskfile.go
@@ -153,6 +153,13 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
 		if err = taskfile.Merge(t, includedTaskfile, &includedTask, namespace); err != nil {
 			return err
 		}
+
+		if includedTaskfile.Tasks["default"] != nil && t.Tasks[namespace] == nil {
+			defaultTaskName := fmt.Sprintf("%s:default", namespace)
+			t.Tasks[defaultTaskName].Aliases = append(t.Tasks[defaultTaskName].Aliases, namespace)
+			t.Tasks[defaultTaskName].Aliases = append(t.Tasks[defaultTaskName].Aliases, includedTask.Aliases...)
+		}
+
 		return nil
 	})
 	if err != nil {


Test patch:
diff --git a/task_test.go b/task_test.go
index a2446bc5..30d90a54 100644
--- a/task_test.go
+++ b/task_test.go
@@ -1107,6 +1107,30 @@ func TestInternalTask(t *testing.T) {
 	}
 }
 
+func TestIncludesShadowedDefault(t *testing.T) {
+	tt := fileContentTest{
+		Dir:       "testdata/includes_shadowed_default",
+		Target:    "included",
+		TrimSpace: true,
+		Files: map[string]string{
+			"file.txt": "shadowed",
+		},
+	}
+	tt.Run(t)
+}
+
+func TestIncludesUnshadowedDefault(t *testing.T) {
+	tt := fileContentTest{
+		Dir:       "testdata/includes_unshadowed_default",
+		Target:    "included",
+		TrimSpace: true,
+		Files: map[string]string{
+			"file.txt": "included",
+		},
+	}
+	tt.Run(t)
+}
+
 func TestSupportedFileNames(t *testing.T) {
 	fileNames := []string{
 		"Taskfile.yml",
diff --git a/testdata/includes_shadowed_default/Taskfile.yml b/testdata/includes_shadowed_default/Taskfile.yml
new file mode 100644
index 00000000..5588c962
--- /dev/null
+++ b/testdata/includes_shadowed_default/Taskfile.yml
@@ -0,0 +1,10 @@
+version: '3'
+
+includes:
+  included:
+    taskfile: Taskfile2.yml
+
+tasks:
+  included:
+    cmds:
+      - echo "shadowed" > file.txt
diff --git a/testdata/includes_shadowed_default/Taskfile2.yml b/testdata/includes_shadowed_default/Taskfile2.yml
new file mode 100644
index 00000000..e9447992
--- /dev/null
+++ b/testdata/includes_shadowed_default/Taskfile2.yml
@@ -0,0 +1,6 @@
+version: '3'
+
+tasks:
+  default:
+    cmds:
+      - echo "included" > file.txt
diff --git a/testdata/includes_shadowed_default/file.txt b/testdata/includes_shadowed_default/file.txt
new file mode 100644
index 00000000..92a8299c
--- /dev/null
+++ b/testdata/includes_shadowed_default/file.txt
@@ -0,0 +1,1 @@
+shadowed
diff --git a/testdata/includes_unshadowed_default/Taskfile.yml b/testdata/includes_unshadowed_default/Taskfile.yml
new file mode 100644
index 00000000..017f9a8b
--- /dev/null
+++ b/testdata/includes_unshadowed_default/Taskfile.yml
@@ -0,0 +1,5 @@
+version: '3'
+
+includes:
+  included:
+    taskfile: Taskfile2.yml
diff --git a/testdata/includes_unshadowed_default/Taskfile2.yml b/testdata/includes_unshadowed_default/Taskfile2.yml
new file mode 100644
index 00000000..e9447992
--- /dev/null
+++ b/testdata/includes_unshadowed_default/Taskfile2.yml
@@ -0,0 +1,6 @@
+version: '3'
+
+tasks:
+  default:
+    cmds:
+      - echo "included" > file.txt
diff --git a/testdata/includes_unshadowed_default/file.txt b/testdata/includes_unshadowed_default/file.txt
new file mode 100644
index 00000000..201a102c
--- /dev/null
+++ b/testdata/includes_unshadowed_default/file.txt
@@ -0,0 +1,1 @@
+included