Skip to content

Commit

Permalink
Resolve relative include paths relative to the including Taskfile
Browse files Browse the repository at this point in the history
Closes #823
Closes #822
  • Loading branch information
theunrepentantgeek authored and andreynering committed Aug 4, 2022
1 parent 47c1bb6 commit e396f4d
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 20 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Always resolve relative include paths relative to the including Taskfile
([#822](https://github.com/go-task/task/issues/822), [#823](https://github.com/go-task/task/pull/823)).
- Fix ZSH and PowerShell completions to consider all tasks instead of just the
public ones (those with descriptions)
([#803](https://github.com/go-task/task/pull/803)).
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Some environment variables can be overriden to adjust Task behavior.

| 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. |
| `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. |

Expand Down Expand Up @@ -129,7 +129,7 @@ tasks:
foobar:
- echo "foo"
- echo "bar"
baz:
cmd: echo "baz"
```
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ 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.

Relative paths are resolved relative to the directory containing the including Taskfile.

### OS-specific Taskfiles

With `version: '2'`, task automatically includes any `Taskfile_{{OS}}.yml`
Expand Down
52 changes: 45 additions & 7 deletions task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,14 @@ func (fct fileContentTest) Run(t *testing.T) {

for name, expectContent := range fct.Files {
t.Run(fct.name(name), func(t *testing.T) {
b, err := os.ReadFile(filepath.Join(fct.Dir, name))
path := filepath.Join(fct.Dir, name)
b, err := os.ReadFile(path)
assert.NoError(t, err, "Error reading file")
s := string(b)
if fct.TrimSpace {
s = strings.TrimSpace(s)
}
assert.Equal(t, expectContent, s, "unexpected file content")
assert.Equal(t, expectContent, s, "unexpected file content in %s", path)
})
}
}
Expand Down Expand Up @@ -774,7 +775,12 @@ func TestIncludesMultiLevel(t *testing.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"

wd, err := os.Getwd()
assert.Nil(t, err)

message := "task: include cycle detected between %s/%s/one/Taskfile.yml <--> %s/%s/Taskfile.yml"
expectedError := fmt.Sprintf(message, wd, dir, wd, dir)

var buff bytes.Buffer
e := task.Executor{
Expand Down Expand Up @@ -852,27 +858,39 @@ func TestIncludesOptional(t *testing.T) {
}

func TestIncludesOptionalImplicitFalse(t *testing.T) {
const dir = "testdata/includes_optional_implicit_false"
wd, _ := os.Getwd()

message := "stat %s/%s/TaskfileOptional.yml: no such file or directory"
expected := fmt.Sprintf(message, wd, dir)

e := task.Executor{
Dir: "testdata/includes_optional_implicit_false",
Dir: dir,
Stdout: io.Discard,
Stderr: io.Discard,
}

err := e.Setup()
assert.Error(t, err)
assert.Equal(t, "stat testdata/includes_optional_implicit_false/TaskfileOptional.yml: no such file or directory", err.Error())
assert.Equal(t, expected, err.Error())
}

func TestIncludesOptionalExplicitFalse(t *testing.T) {
const dir = "testdata/includes_optional_explicit_false"
wd, _ := os.Getwd()

message := "stat %s/%s/TaskfileOptional.yml: no such file or directory"
expected := fmt.Sprintf(message, wd, dir)

e := task.Executor{
Dir: "testdata/includes_optional_explicit_false",
Dir: dir,
Stdout: io.Discard,
Stderr: io.Discard,
}

err := e.Setup()
assert.Error(t, err)
assert.Equal(t, "stat testdata/includes_optional_explicit_false/TaskfileOptional.yml: no such file or directory", err.Error())
assert.Equal(t, expected, err.Error())
}

func TestIncludesFromCustomTaskfile(t *testing.T) {
Expand All @@ -890,6 +908,26 @@ func TestIncludesFromCustomTaskfile(t *testing.T) {
tt.Run(t)
}

func TestIncludesRelativePath(t *testing.T) {
const dir = "testdata/includes_rel_path"

var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
}

assert.NoError(t, e.Setup())

assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "common:pwd"}))
assert.Contains(t, buff.String(), "testdata/includes_rel_path/common")

buff.Reset()
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "included:common:pwd"}))
assert.Contains(t, buff.String(), "testdata/includes_rel_path/common")
}

func TestSupportedFileNames(t *testing.T) {
fileNames := []string{
"Taskfile.yml",
Expand Down
35 changes: 34 additions & 1 deletion taskfile/included_taskfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@ package taskfile

import (
"errors"
"fmt"
"path/filepath"

"github.com/go-task/task/v3/internal/execext"

"gopkg.in/yaml.v3"
)

// IncludedTaskfile represents information about included tasksfile
// IncludedTaskfile represents information about included taskfiles
type IncludedTaskfile struct {
Taskfile string
Dir string
Optional bool
AdvancedImport bool
Vars *Vars
BaseDir string // The directory from which the including taskfile was loaded; used to resolve relative paths
}

// IncludedTaskfiles represents information about included tasksfiles
Expand Down Expand Up @@ -107,3 +112,31 @@ func (it *IncludedTaskfile) UnmarshalYAML(unmarshal func(interface{}) error) err
it.Vars = includedTaskfile.Vars
return nil
}

// FullTaskfilePath returns the fully qualified path to the included taskfile
func (it *IncludedTaskfile) FullTaskfilePath() (string, error) {
return it.resolvePath(it.Taskfile)
}

// FullDirPath returns the fully qualified path to the included taskfile's working directory
func (it *IncludedTaskfile) FullDirPath() (string, error) {
return it.resolvePath(it.Dir)
}

func (it *IncludedTaskfile) resolvePath(path string) (string, error) {
path, err := execext.Expand(path)
if err != nil {
return "", err
}

if filepath.IsAbs(path) {
return path, nil
}

result, err := filepath.Abs(filepath.Join(it.BaseDir, path))
if err != nil {
return "", fmt.Errorf("task: error resolving path %s relative to %s: %w", path, it.BaseDir, err)
}

return result, nil
}
45 changes: 35 additions & 10 deletions taskfile/read/taskfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (

"gopkg.in/yaml.v3"

"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile"
)
Expand Down Expand Up @@ -44,6 +43,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
}
readerNode.Dir = d
}

path, err := exists(filepath.Join(readerNode.Dir, readerNode.Entrypoint))
if err != nil {
return nil, err
Expand All @@ -60,6 +60,16 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
return nil, err
}

// Annotate any included Taskfile reference with a base directory for resolving relative paths
_ = t.Includes.Range(func(key string, includedFile taskfile.IncludedTaskfile) error {
// Set the base directory for resolving relative paths, but only if not already set
if includedFile.BaseDir == "" {
includedFile.BaseDir = readerNode.Dir
t.Includes.Set(key, includedFile)
}
return nil
})

err = t.Includes.Range(func(namespace string, includedTask taskfile.IncludedTaskfile) error {
if v >= 3.0 {
tr := templater.Templater{Vars: &taskfile.Vars{}, RemoveNoValue: true}
Expand All @@ -69,19 +79,18 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
Optional: includedTask.Optional,
AdvancedImport: includedTask.AdvancedImport,
Vars: includedTask.Vars,
BaseDir: includedTask.BaseDir,
}
if err := tr.Err(); err != nil {
return err
}
}

path, err := execext.Expand(includedTask.Taskfile)
path, err := includedTask.FullTaskfilePath()
if err != nil {
return err
}
if !filepath.IsAbs(path) {
path = filepath.Join(readerNode.Dir, path)
}

path, err = exists(path)
if err != nil {
if includedTask.Optional {
Expand Down Expand Up @@ -114,21 +123,27 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
}

if includedTask.AdvancedImport {
dir, err := includedTask.FullDirPath()
if err != nil {
return err
}

for k, v := range includedTaskfile.Vars.Mapping {
o := v
o.Dir = filepath.Join(readerNode.Dir, includedTask.Dir)
o.Dir = dir
includedTaskfile.Vars.Mapping[k] = o
}
for k, v := range includedTaskfile.Env.Mapping {
o := v
o.Dir = filepath.Join(readerNode.Dir, includedTask.Dir)
o.Dir = dir
includedTaskfile.Env.Mapping[k] = o
}

for _, task := range includedTaskfile.Tasks {
if !filepath.IsAbs(task.Dir) {
task.Dir = filepath.Join(includedTask.Dir, task.Dir)
task.Dir = filepath.Join(dir, task.Dir)
}

task.IncludeVars = includedTask.Vars
task.IncludedTaskfileVars = includedTaskfile.Vars
}
Expand Down Expand Up @@ -176,19 +191,29 @@ func readTaskfile(file string) (*taskfile.Taskfile, error) {
return &t, yaml.NewDecoder(f).Decode(&t)
}

// exists finds a Taskfile at the stated location, returning a fully qualified path to the file
func exists(path string) (string, error) {
fi, err := os.Stat(path)
if err != nil {
return "", err
}
if fi.Mode().IsRegular() {
return path, nil
// File exists, return a fully qualified path
result, err := filepath.Abs(path)
if err != nil {
return "", err
}
return result, nil
}

for _, n := range defaultTaskfiles {
fpath := filepath.Join(path, n)
if _, err := os.Stat(fpath); err == nil {
return fpath, nil
result, err := filepath.Abs(fpath)
if err != nil {
return "", err
}
return result, nil
}
}

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

includes:
included:
taskfile: ./included
dir: ./included

common:
taskfile: ./common
dir: ./common
4 changes: 4 additions & 0 deletions testdata/includes_rel_path/common/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
version: '3'

tasks:
pwd: pwd
6 changes: 6 additions & 0 deletions testdata/includes_rel_path/included/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: '3'

includes:
common:
taskfile: ../common
dir: ../common

0 comments on commit e396f4d

Please sign in to comment.