From 9a5a1e225363df403a11072b4be5185c3f2d2ad8 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Sun, 9 Sep 2018 22:29:29 -0300 Subject: [PATCH 1/6] Start support to including Taskfiles --- internal/taskfile/merge.go | 15 +++++++++++++-- internal/taskfile/read/taskfile.go | 17 +++++++++++++++++ internal/taskfile/taskfile.go | 3 +++ 3 files changed, 33 insertions(+), 2 deletions(-) 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 0c36380d..e9c08960 100644 --- a/internal/taskfile/read/taskfile.go +++ b/internal/taskfile/read/taskfile.go @@ -19,6 +19,23 @@ func Taskfile(dir string) (*taskfile.Taskfile, error) { return nil, fmt.Errorf(`No Taskfile.yml found. Use "task --init" to create a new one`) } + for namespace, path := range t.Includes { + 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 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 { From 5a285601770ae5f0471800db822602727b811c79 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Sat, 13 Oct 2018 16:56:51 -0300 Subject: [PATCH 2/6] Write first test for including a Taskfile --- internal/taskfile/read/taskfile.go | 1 + task_test.go | 13 +++++++++++++ testdata/includes/.gitignore | 1 + testdata/includes/Taskfile.yml | 14 ++++++++++++++ testdata/includes/included/Taskfile.yml | 6 ++++++ 5 files changed, 35 insertions(+) create mode 100644 testdata/includes/.gitignore create mode 100644 testdata/includes/Taskfile.yml create mode 100644 testdata/includes/included/Taskfile.yml diff --git a/internal/taskfile/read/taskfile.go b/internal/taskfile/read/taskfile.go index dab897b9..a96e4a9e 100644 --- a/internal/taskfile/read/taskfile.go +++ b/internal/taskfile/read/taskfile.go @@ -23,6 +23,7 @@ func Taskfile(dir string) (*taskfile.Taskfile, error) { } for namespace, path := range t.Includes { + path = filepath.Join(dir, path) info, err := os.Stat(path) if err != nil { return nil, err diff --git a/task_test.go b/task_test.go index 58687cab..fef7302c 100644 --- a/task_test.go +++ b/task_test.go @@ -470,3 +470,16 @@ 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.txt": "included", + }, + } + 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..d4b44e51 --- /dev/null +++ b/testdata/includes/Taskfile.yml @@ -0,0 +1,14 @@ +version: '2' + +includes: + included: ./included + +tasks: + default: + cmds: + - task: gen + - task: included:gen + + gen: + cmds: + - echo main > main.txt diff --git a/testdata/includes/included/Taskfile.yml b/testdata/includes/included/Taskfile.yml new file mode 100644 index 00000000..f0d19639 --- /dev/null +++ b/testdata/includes/included/Taskfile.yml @@ -0,0 +1,6 @@ +version: '2' + +tasks: + gen: + cmds: + - echo included > included.txt From 5eb1a1f7f5f3313342a1bc3b0c5b99472ae248a8 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Sat, 13 Oct 2018 17:52:09 -0300 Subject: [PATCH 3/6] Fixes to Taskfile including: - Disallow recursive Taskfile including (i.e. included Taskfile including other Taskfiles) - Write test for included a file instead of a directory --- internal/taskfile/read/taskfile.go | 7 +++++++ task_test.go | 5 +++-- testdata/includes/Taskfile.yml | 2 ++ testdata/includes/Taskfile2.yml | 6 ++++++ testdata/includes/included/Taskfile.yml | 2 +- 5 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 testdata/includes/Taskfile2.yml diff --git a/internal/taskfile/read/taskfile.go b/internal/taskfile/read/taskfile.go index a96e4a9e..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") @@ -35,6 +39,9 @@ func Taskfile(dir string) (*taskfile.Taskfile, error) { 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 } diff --git a/task_test.go b/task_test.go index fef7302c..9b0bbb1a 100644 --- a/task_test.go +++ b/task_test.go @@ -477,8 +477,9 @@ func TestIncludes(t *testing.T) { Target: "default", TrimSpace: true, Files: map[string]string{ - "main.txt": "main", - "included.txt": "included", + "main.txt": "main", + "included_directory.txt": "included_directory", + "included_taskfile.txt": "included_taskfile", }, } tt.Run(t) diff --git a/testdata/includes/Taskfile.yml b/testdata/includes/Taskfile.yml index d4b44e51..6b7f29ef 100644 --- a/testdata/includes/Taskfile.yml +++ b/testdata/includes/Taskfile.yml @@ -2,12 +2,14 @@ version: '2' includes: included: ./included + included_taskfile: ./Taskfile2.yml tasks: default: cmds: - task: gen - task: included:gen + - task: included_taskfile:gen gen: cmds: 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 index f0d19639..e8fe2ad2 100644 --- a/testdata/includes/included/Taskfile.yml +++ b/testdata/includes/included/Taskfile.yml @@ -3,4 +3,4 @@ version: '2' tasks: gen: cmds: - - echo included > included.txt + - echo included_directory > included_directory.txt From f519f56078c35a8e2c9ac749b0fb12630fd48993 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Sat, 13 Oct 2018 18:14:42 -0300 Subject: [PATCH 4/6] Add documentation for including other Taskfiles --- docs/usage.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) 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 From 5d9de14ca3a217c4013cbb412e722f24efec743a Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Sat, 13 Oct 2018 18:25:40 -0300 Subject: [PATCH 5/6] Increment the current Taskfile version to 2.2 --- docs/taskfile_versions.md | 20 ++++++++++++++++++-- internal/taskfile/version/version.go | 6 ++++++ task.go | 9 ++++++--- 3 files changed, 30 insertions(+), 5 deletions(-) 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/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{} From 5720936247d22724eb80db591a57255cc08acd5d Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Sat, 13 Oct 2018 18:29:23 -0300 Subject: [PATCH 6/6] Fix CI --- Taskfile.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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