1
0
mirror of https://github.com/go-task/task.git synced 2025-04-13 11:50:50 +02:00

Merge pull request #656 from tylermmorton/master

Add support for multi-level includes
This commit is contained in:
Andrey Nering 2022-03-31 21:12:15 -03:00 committed by GitHub
commit 41cd7acc87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 152 additions and 20 deletions

View File

@ -164,10 +164,6 @@ includes:
> 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. =)
### Optional includes
Includes marked as optional will allow Task to continue execution as normal if

View File

@ -107,7 +107,12 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
// Setup setups Executor's internal state
func (e *Executor) Setup() error {
var err error
e.Taskfile, err = read.Taskfile(e.Dir, e.Entrypoint)
e.Taskfile, err = read.Taskfile(&read.ReaderNode{
Dir: e.Dir,
Entrypoint: e.Entrypoint,
Parent: nil,
Optional: false,
})
if err != nil {
return err
}

View File

@ -753,6 +753,35 @@ func TestIncludes(t *testing.T) {
tt.Run(t)
}
func TestIncludesMultiLevel(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/includes_multi_level",
Target: "default",
TrimSpace: true,
Files: map[string]string{
"called_one.txt": "one",
"called_two.txt": "two",
"called_three.txt": "three",
},
}
tt.Run(t)
}
func TestIncludeCycle(t *testing.T) {
const dir = "testdata/includes_cycle"
expectedError := "task: include cycle detected between testdata/includes_cycle/Taskfile.yml <--> testdata/includes_cycle/one/two/Taskfile.yml"
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
Silent: true,
}
assert.EqualError(t, e.Setup(), expectedError)
}
func TestIncorrectVersionIncludes(t *testing.T) {
const dir = "testdata/incorrect_includes"
expectedError := "task: Import with additional parameters is only available starting on Taskfile version v3"

View File

@ -15,8 +15,6 @@ import (
)
var (
// ErrIncludedTaskfilesCantHaveIncludes is returned when a included Taskfile contains includes
ErrIncludedTaskfilesCantHaveIncludes = errors.New("task: Included Taskfiles can't have includes. Please, move the include to the main Taskfile")
// ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs
ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile")
@ -28,21 +26,29 @@ var (
}
)
type ReaderNode struct {
Dir string
Entrypoint string
Optional bool
Parent *ReaderNode
}
// Taskfile reads a Taskfile for a given directory
// Uses current dir when dir is left empty. Uses Taskfile.yml
// or Taskfile.yaml when entrypoint is left empty
func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
if dir == "" {
func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
if readerNode.Dir == "" {
d, err := os.Getwd()
if err != nil {
return nil, err
}
dir = d
readerNode.Dir = d
}
path, err := exists(filepath.Join(dir, entrypoint))
path, err := exists(filepath.Join(readerNode.Dir, readerNode.Entrypoint))
if err != nil {
return nil, err
}
readerNode.Entrypoint = filepath.Base(path)
t, err := readTaskfile(path)
if err != nil {
@ -74,9 +80,8 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
return err
}
if !filepath.IsAbs(path) {
path = filepath.Join(dir, path)
path = filepath.Join(readerNode.Dir, path)
}
path, err = exists(path)
if err != nil {
if includedTask.Optional {
@ -85,12 +90,23 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
return err
}
includedTaskfile, err := readTaskfile(path)
if err != nil {
includeReaderNode := &ReaderNode{
Dir: filepath.Dir(path),
Entrypoint: filepath.Base(path),
Parent: readerNode,
Optional: includedTask.Optional,
}
if err := checkCircularIncludes(includeReaderNode); err != nil {
return err
}
if includedTaskfile.Includes.Len() > 0 {
return ErrIncludedTaskfilesCantHaveIncludes
includedTaskfile, err := Taskfile(includeReaderNode)
if err != nil {
if includedTask.Optional {
return nil
}
return err
}
if v >= 3.0 && len(includedTaskfile.Dotenv) > 0 {
@ -100,12 +116,12 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
if includedTask.AdvancedImport {
for k, v := range includedTaskfile.Vars.Mapping {
o := v
o.Dir = filepath.Join(dir, includedTask.Dir)
o.Dir = filepath.Join(readerNode.Dir, includedTask.Dir)
includedTaskfile.Vars.Mapping[k] = o
}
for k, v := range includedTaskfile.Env.Mapping {
o := v
o.Dir = filepath.Join(dir, includedTask.Dir)
o.Dir = filepath.Join(readerNode.Dir, includedTask.Dir)
includedTaskfile.Env.Mapping[k] = o
}
@ -128,7 +144,7 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
}
if v < 3.0 {
path = filepath.Join(dir, fmt.Sprintf("Taskfile_%s.yml", runtime.GOOS))
path = filepath.Join(readerNode.Dir, fmt.Sprintf("Taskfile_%s.yml", runtime.GOOS))
if _, err = os.Stat(path); err == nil {
osTaskfile, err := readTaskfile(path)
if err != nil {
@ -178,3 +194,25 @@ func exists(path string) (string, error) {
return "", fmt.Errorf(`task: No Taskfile found in "%s". Use "task --init" to create a new one`, path)
}
func checkCircularIncludes(node *ReaderNode) error {
if node == nil {
return errors.New("task: failed to check for include cycle: node was nil")
}
if node.Parent == nil {
return errors.New("task: failed to check for include cycle: node.Parent was nil")
}
var curNode = node
var basePath = filepath.Join(node.Dir, node.Entrypoint)
for curNode.Parent != nil {
curNode = curNode.Parent
curPath := filepath.Join(curNode.Dir, curNode.Entrypoint)
if curPath == basePath {
return fmt.Errorf("task: include cycle detected between %s <--> %s",
curPath,
filepath.Join(node.Parent.Dir, node.Parent.Entrypoint),
)
}
}
return nil
}

12
testdata/includes_cycle/Taskfile.yml vendored Normal file
View File

@ -0,0 +1,12 @@
version: '3'
includes:
'one': ./one/Taskfile.yml
tasks:
default:
cmds:
- echo "called_dep" > called_dep.txt
level1:
cmds:
- echo "hello level 1"

View File

@ -0,0 +1,9 @@
version: '3'
includes:
'two': ./two/Taskfile.yml
tasks:
level2:
cmds:
- echo "hello level 2"

View File

@ -0,0 +1,9 @@
version: '3'
includes:
bad: "../../Taskfile.yml"
tasks:
level3:
cmds:
- echo "hello level 3"

View File

@ -0,0 +1,11 @@
version: '3'
includes:
'one': ./one/
tasks:
default:
cmds:
- task: one:default
- task: one:two:default
- task: one:two:three:default

View File

@ -0,0 +1 @@
one

View File

@ -0,0 +1 @@
three

View File

@ -0,0 +1 @@
two

View File

@ -0,0 +1,8 @@
version: '3'
includes:
'two': ./two/
tasks:
default: echo one > called_one.txt

View File

@ -0,0 +1,7 @@
version: '3'
includes:
'three': ./three/Taskfile.yml
tasks:
default: echo two > called_two.txt

View File

@ -0,0 +1,5 @@
version: '3'
tasks:
default: echo three > called_three.txt