Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add option to declare an included Taskfile as flatten #1704

Merged
merged 9 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions errors/errors_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error is also returned when a task in a child taskfile conflicts with one in a parent taskfile (as opposed to two children taskfiles). We could improve the message to include those cases.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think about CodeTaskNameConflict or another one ?
At first I used CodeTaskNameConflict but I found more readable to distinct both. That being said, it would make sense, so I'll give a try to adjust the message

}

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 {
Expand Down
37 changes: 37 additions & 0 deletions task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions taskfile/ast/include.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Include struct {
Aliases []string
AdvancedImport bool
Vars *Vars
Flatten bool
}

// Includes represents information about included tasksfiles
Expand Down Expand Up @@ -81,6 +82,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
Dir string
Optional bool
Internal bool
Flatten bool
Aliases []string
Vars *Vars
}
Expand All @@ -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
}

Expand All @@ -114,5 +117,6 @@ func (include *Include) DeepCopy() *Include {
Internal: include.Internal,
AdvancedImport: include.AdvancedImport,
Vars: include.Vars.DeepCopy(),
Flatten: include.Flatten,
}
}
3 changes: 1 addition & 2 deletions taskfile/ast/taskfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
65 changes: 37 additions & 28 deletions taskfile/ast/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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 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 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))
// 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 {
Expand All @@ -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
})
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions taskfile/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions testdata/includes_flatten/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.txt
12 changes: 12 additions & 0 deletions testdata/includes_flatten/Taskfile.multiple.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: '3'

includes:
included:
taskfile: ./included
flatten: true

tasks:
gen:
cmds:
- echo "gen multiple"

13 changes: 13 additions & 0 deletions testdata/includes_flatten/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: '3'

includes:
included:
taskfile: ./included
dir: ./included
flatten: true

tasks:
default:
cmds:
- echo root_directory > root_directory.txt

23 changes: 23 additions & 0 deletions testdata/includes_flatten/included/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -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 changes: 6 additions & 0 deletions testdata/includes_flatten/nested/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: '3'

tasks:
from_nested:
cmds:
- echo "from nested"
5 changes: 3 additions & 2 deletions website/docs/reference/schema.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`. |
Expand All @@ -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. |
Expand Down
Loading
Loading