diff --git a/Taskfile.yml b/Taskfile.yml index 91621a54..2568851f 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -23,7 +23,7 @@ tasks: desc: Downloads cli dependencies cmds: - task: go-get - vars: {REPO: github.com/golang/lint/golint} + vars: {REPO: golang.org/x/lint/golint} - task: go-get vars: {REPO: github.com/golang/dep/cmd/dep} - task: go-get @@ -68,7 +68,7 @@ tasks: ci: cmds: - task: go-get - vars: {REPO: github.com/golang/lint/golint} + vars: {REPO: golang.org/x/lint/golint} - task: lint - task: test diff --git a/docs/taskfile_versions.md b/docs/taskfile_versions.md index 997334a7..9b74c527 100644 --- a/docs/taskfile_versions.md +++ b/docs/taskfile_versions.md @@ -128,5 +128,21 @@ tasks: ignore_error: true ``` -[output]: https://github.com/go-task/task#output-syntax -[ignore_errors]: https://github.com/go-task/task#ignore-errors +## Version 2.2 + +Version 2.2 comes with a global `includes` options to include other +Taskfiles: + +```yaml +version: '2' + +includes: + docs: ./documentation # will look for ./documentation/Taskfile.yml + docker: ./DockerTasks.yml +``` + +Please check the [documentation][includes] + +[output]: usage#output-syntax +[ignore_errors]: usage#ignore-errors +[includes]: usage#including-other-taskfiles diff --git a/docs/usage.md b/docs/usage.md index 1c5087cc..2bc75067 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -48,7 +48,7 @@ tasks: hallo: welt ``` -## OS specific task +## Operating System specific tasks If you add a `Taskfile_{{GOOS}}.yml` you can override or amend your Taskfile based on the operating system. @@ -86,6 +86,31 @@ It's also possible to have an OS specific `Taskvars.yml` file, like `Taskvars_windows.yml`, `Taskfile_linux.yml`, or `Taskvars_darwin.yml`. See the [variables section](#variables) below. +## Including other Taskfiles + +If you want to share tasks between different projects (Taskfiles), you can use +the importing mechanism to include other Taskfiles using the `includes` keyword: + +```yaml +version: '2' + +includes: + docs: ./documentation # will look for ./documentation/Taskfile.yml + docker: ./DockerTasks.yml +``` + +The tasks described in the given Taskfiles will be available with the informed +namespace. So, you'd call `task docs:serve` to run the `serve` task from +`documentation/Taskfile.yml` or `task docker:build` to run the `build` task +from the `DockerTasks.yml` file. + +> The included Taskfiles must be using the same schema version the main +> Taskfile uses. + +> Also, for now included Taskfiles can't include other Taskfiles. +> This was a deliberate decision to keep use and implementation simple. +> If you disagree, open an GitHub issue and explain your use case. =) + ## Task directory By default, tasks will be executed in the directory where the Taskfile is diff --git a/internal/taskfile/merge.go b/internal/taskfile/merge.go index 4825cb1d..19a4fec7 100644 --- a/internal/taskfile/merge.go +++ b/internal/taskfile/merge.go @@ -2,10 +2,14 @@ package taskfile import ( "fmt" + "strings" ) +// NamespaceSeparator contains the character that separates namescapes +const NamespaceSeparator = ":" + // Merge merges the second Taskfile into the first -func Merge(t1, t2 *Taskfile) error { +func Merge(t1, t2 *Taskfile, namespaces ...string) error { if t1.Version != t2.Version { return fmt.Errorf(`Taskfiles versions should match. First is "%s" but second is "%s"`, t1.Version, t2.Version) } @@ -16,12 +20,19 @@ func Merge(t1, t2 *Taskfile) error { if t2.Output != "" { t1.Output = t2.Output } + for k, v := range t2.Includes { + t1.Includes[k] = v + } for k, v := range t2.Vars { t1.Vars[k] = v } for k, v := range t2.Tasks { - t1.Tasks[k] = v + t1.Tasks[taskNameWithNamespace(k, namespaces...)] = v } return nil } + +func taskNameWithNamespace(taskName string, namespaces ...string) string { + return strings.Join(append(namespaces, taskName), NamespaceSeparator) +} diff --git a/internal/taskfile/read/taskfile.go b/internal/taskfile/read/taskfile.go index 191d1415..9c7c971a 100644 --- a/internal/taskfile/read/taskfile.go +++ b/internal/taskfile/read/taskfile.go @@ -1,6 +1,7 @@ package read import ( + "errors" "fmt" "os" "path/filepath" @@ -11,6 +12,9 @@ import ( "gopkg.in/yaml.v2" ) +// ErrIncludedTaskfilesCantHaveIncludes is returned when a included Taskfile contains includes +var ErrIncludedTaskfilesCantHaveIncludes = errors.New("task: Included Taskfiles can't have includes. Please, move the include to the main Taskfile") + // Taskfile reads a Taskfile for a given directory func Taskfile(dir string) (*taskfile.Taskfile, error) { path := filepath.Join(dir, "Taskfile.yml") @@ -22,6 +26,27 @@ func Taskfile(dir string) (*taskfile.Taskfile, error) { return nil, err } + for namespace, path := range t.Includes { + path = filepath.Join(dir, path) + info, err := os.Stat(path) + if err != nil { + return nil, err + } + if info.IsDir() { + path = filepath.Join(path, "Taskfile.yml") + } + includedTaskfile, err := readTaskfile(path) + if err != nil { + return nil, err + } + if len(includedTaskfile.Includes) > 0 { + return nil, ErrIncludedTaskfilesCantHaveIncludes + } + if err = taskfile.Merge(t, includedTaskfile, namespace); err != nil { + return nil, err + } + } + path = filepath.Join(dir, fmt.Sprintf("Taskfile_%s.yml", runtime.GOOS)) if _, err = os.Stat(path); err == nil { osTaskfile, err := readTaskfile(path) diff --git a/internal/taskfile/taskfile.go b/internal/taskfile/taskfile.go index 2ac0cf50..58311aab 100644 --- a/internal/taskfile/taskfile.go +++ b/internal/taskfile/taskfile.go @@ -5,6 +5,7 @@ type Taskfile struct { Version string Expansions int Output string + Includes map[string]string Vars Vars Tasks Tasks } @@ -20,6 +21,7 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error { Version string Expansions int Output string + Includes map[string]string Vars Vars Tasks Tasks } @@ -29,6 +31,7 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error { tf.Version = taskfile.Version tf.Expansions = taskfile.Expansions tf.Output = taskfile.Output + tf.Includes = taskfile.Includes tf.Vars = taskfile.Vars tf.Tasks = taskfile.Tasks if tf.Expansions <= 0 { diff --git a/internal/taskfile/version/version.go b/internal/taskfile/version/version.go index 2853c5c3..bb2176e4 100644 --- a/internal/taskfile/version/version.go +++ b/internal/taskfile/version/version.go @@ -9,6 +9,7 @@ var ( v2 = mustVersion("2") v21 = mustVersion("2.1") v22 = mustVersion("2.2") + v23 = mustVersion("2.3") ) // IsV1 returns if is a given Taskfile version is version 1 @@ -31,6 +32,11 @@ func IsV22(v *semver.Constraints) bool { return v.Check(v22) } +// IsV23 returns if is a given Taskfile version is at least version 2.3 +func IsV23(v *semver.Constraints) bool { + return v.Check(v23) +} + func mustVersion(s string) *semver.Version { v, err := semver.NewVersion(s) if err != nil { diff --git a/task.go b/task.go index e9e29045..e8ba9381 100644 --- a/task.go +++ b/task.go @@ -116,7 +116,7 @@ func (e *Executor) Setup() error { Vars: e.taskvars, Logger: e.Logger, } - case version.IsV2(v), version.IsV21(v): + case version.IsV2(v), version.IsV21(v), version.IsV22(v): e.Compiler = &compilerv2.CompilerV2{ Dir: e.Dir, Taskvars: e.taskvars, @@ -124,13 +124,16 @@ func (e *Executor) Setup() error { Expansions: e.Taskfile.Expansions, Logger: e.Logger, } - case version.IsV22(v): - return fmt.Errorf(`task: Taskfile versions greater than v2.1 not implemented in the version of Task`) + case version.IsV23(v): + return fmt.Errorf(`task: Taskfile versions greater than v2.3 not implemented in the version of Task`) } if !version.IsV21(v) && e.Taskfile.Output != "" { return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`) } + if !version.IsV22(v) && len(e.Taskfile.Includes) > 0 { + return fmt.Errorf(`task: Including Taskfiles is only available starting on Taskfile version v2.2`) + } switch e.Taskfile.Output { case "", "interleaved": e.Output = output.Interleaved{} diff --git a/task_test.go b/task_test.go index 58687cab..9b0bbb1a 100644 --- a/task_test.go +++ b/task_test.go @@ -470,3 +470,17 @@ func TestDry(t *testing.T) { t.Errorf("File should not exist %s", file) } } + +func TestIncludes(t *testing.T) { + tt := fileContentTest{ + Dir: "testdata/includes", + Target: "default", + TrimSpace: true, + Files: map[string]string{ + "main.txt": "main", + "included_directory.txt": "included_directory", + "included_taskfile.txt": "included_taskfile", + }, + } + tt.Run(t) +} diff --git a/testdata/includes/.gitignore b/testdata/includes/.gitignore new file mode 100644 index 00000000..2211df63 --- /dev/null +++ b/testdata/includes/.gitignore @@ -0,0 +1 @@ +*.txt diff --git a/testdata/includes/Taskfile.yml b/testdata/includes/Taskfile.yml new file mode 100644 index 00000000..6b7f29ef --- /dev/null +++ b/testdata/includes/Taskfile.yml @@ -0,0 +1,16 @@ +version: '2' + +includes: + included: ./included + included_taskfile: ./Taskfile2.yml + +tasks: + default: + cmds: + - task: gen + - task: included:gen + - task: included_taskfile:gen + + gen: + cmds: + - echo main > main.txt diff --git a/testdata/includes/Taskfile2.yml b/testdata/includes/Taskfile2.yml new file mode 100644 index 00000000..dbb4a34c --- /dev/null +++ b/testdata/includes/Taskfile2.yml @@ -0,0 +1,6 @@ +version: '2' + +tasks: + gen: + cmds: + - echo included_taskfile > included_taskfile.txt diff --git a/testdata/includes/included/Taskfile.yml b/testdata/includes/included/Taskfile.yml new file mode 100644 index 00000000..e8fe2ad2 --- /dev/null +++ b/testdata/includes/included/Taskfile.yml @@ -0,0 +1,6 @@ +version: '2' + +tasks: + gen: + cmds: + - echo included_directory > included_directory.txt