Skip to content

Commit e396f4d

Browse files
theunrepentantgeekandreynering
authored andcommitted
Resolve relative include paths relative to the including Taskfile
Closes #823 Closes #822
1 parent 47c1bb6 commit e396f4d

9 files changed

Lines changed: 140 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Always resolve relative include paths relative to the including Taskfile
6+
([#822](https://github.com/go-task/task/issues/822), [#823](https://github.com/go-task/task/pull/823)).
57
- Fix ZSH and PowerShell completions to consider all tasks instead of just the
68
public ones (those with descriptions)
79
([#803](https://github.com/go-task/task/pull/803)).

docs/docs/api_reference.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Some environment variables can be overriden to adjust Task behavior.
8181

8282
| Attribute | Type | Default | Description |
8383
| - | - | - | - |
84-
| `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. |
84+
| `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. |
8585
| `dir` | `string` | The parent Taskfile directory | The working directory of the included tasks when run. |
8686
| `optional` | `bool` | `false` | If `true`, no errors will be thrown if the specified file does not exist. |
8787

@@ -129,7 +129,7 @@ tasks:
129129
foobar:
130130
- echo "foo"
131131
- echo "bar"
132-
132+
133133
baz:
134134
cmd: echo "baz"
135135
```

docs/docs/usage.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ namespace. So, you'd call `task docs:serve` to run the `serve` task from
136136
`documentation/Taskfile.yml` or `task docker:build` to run the `build` task
137137
from the `DockerTasks.yml` file.
138138

139+
Relative paths are resolved relative to the directory containing the including Taskfile.
140+
139141
### OS-specific Taskfiles
140142

141143
With `version: '2'`, task automatically includes any `Taskfile_{{OS}}.yml`

task_test.go

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,14 @@ func (fct fileContentTest) Run(t *testing.T) {
5252

5353
for name, expectContent := range fct.Files {
5454
t.Run(fct.name(name), func(t *testing.T) {
55-
b, err := os.ReadFile(filepath.Join(fct.Dir, name))
55+
path := filepath.Join(fct.Dir, name)
56+
b, err := os.ReadFile(path)
5657
assert.NoError(t, err, "Error reading file")
5758
s := string(b)
5859
if fct.TrimSpace {
5960
s = strings.TrimSpace(s)
6061
}
61-
assert.Equal(t, expectContent, s, "unexpected file content")
62+
assert.Equal(t, expectContent, s, "unexpected file content in %s", path)
6263
})
6364
}
6465
}
@@ -774,7 +775,12 @@ func TestIncludesMultiLevel(t *testing.T) {
774775

775776
func TestIncludeCycle(t *testing.T) {
776777
const dir = "testdata/includes_cycle"
777-
expectedError := "task: include cycle detected between testdata/includes_cycle/Taskfile.yml <--> testdata/includes_cycle/one/two/Taskfile.yml"
778+
779+
wd, err := os.Getwd()
780+
assert.Nil(t, err)
781+
782+
message := "task: include cycle detected between %s/%s/one/Taskfile.yml <--> %s/%s/Taskfile.yml"
783+
expectedError := fmt.Sprintf(message, wd, dir, wd, dir)
778784

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

854860
func TestIncludesOptionalImplicitFalse(t *testing.T) {
861+
const dir = "testdata/includes_optional_implicit_false"
862+
wd, _ := os.Getwd()
863+
864+
message := "stat %s/%s/TaskfileOptional.yml: no such file or directory"
865+
expected := fmt.Sprintf(message, wd, dir)
866+
855867
e := task.Executor{
856-
Dir: "testdata/includes_optional_implicit_false",
868+
Dir: dir,
857869
Stdout: io.Discard,
858870
Stderr: io.Discard,
859871
}
860872

861873
err := e.Setup()
862874
assert.Error(t, err)
863-
assert.Equal(t, "stat testdata/includes_optional_implicit_false/TaskfileOptional.yml: no such file or directory", err.Error())
875+
assert.Equal(t, expected, err.Error())
864876
}
865877

866878
func TestIncludesOptionalExplicitFalse(t *testing.T) {
879+
const dir = "testdata/includes_optional_explicit_false"
880+
wd, _ := os.Getwd()
881+
882+
message := "stat %s/%s/TaskfileOptional.yml: no such file or directory"
883+
expected := fmt.Sprintf(message, wd, dir)
884+
867885
e := task.Executor{
868-
Dir: "testdata/includes_optional_explicit_false",
886+
Dir: dir,
869887
Stdout: io.Discard,
870888
Stderr: io.Discard,
871889
}
872890

873891
err := e.Setup()
874892
assert.Error(t, err)
875-
assert.Equal(t, "stat testdata/includes_optional_explicit_false/TaskfileOptional.yml: no such file or directory", err.Error())
893+
assert.Equal(t, expected, err.Error())
876894
}
877895

878896
func TestIncludesFromCustomTaskfile(t *testing.T) {
@@ -890,6 +908,26 @@ func TestIncludesFromCustomTaskfile(t *testing.T) {
890908
tt.Run(t)
891909
}
892910

911+
func TestIncludesRelativePath(t *testing.T) {
912+
const dir = "testdata/includes_rel_path"
913+
914+
var buff bytes.Buffer
915+
e := task.Executor{
916+
Dir: dir,
917+
Stdout: &buff,
918+
Stderr: &buff,
919+
}
920+
921+
assert.NoError(t, e.Setup())
922+
923+
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "common:pwd"}))
924+
assert.Contains(t, buff.String(), "testdata/includes_rel_path/common")
925+
926+
buff.Reset()
927+
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "included:common:pwd"}))
928+
assert.Contains(t, buff.String(), "testdata/includes_rel_path/common")
929+
}
930+
893931
func TestSupportedFileNames(t *testing.T) {
894932
fileNames := []string{
895933
"Taskfile.yml",

taskfile/included_taskfile.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@ package taskfile
22

33
import (
44
"errors"
5+
"fmt"
6+
"path/filepath"
7+
8+
"github.com/go-task/task/v3/internal/execext"
59

610
"gopkg.in/yaml.v3"
711
)
812

9-
// IncludedTaskfile represents information about included tasksfile
13+
// IncludedTaskfile represents information about included taskfiles
1014
type IncludedTaskfile struct {
1115
Taskfile string
1216
Dir string
1317
Optional bool
1418
AdvancedImport bool
1519
Vars *Vars
20+
BaseDir string // The directory from which the including taskfile was loaded; used to resolve relative paths
1621
}
1722

1823
// IncludedTaskfiles represents information about included tasksfiles
@@ -107,3 +112,31 @@ func (it *IncludedTaskfile) UnmarshalYAML(unmarshal func(interface{}) error) err
107112
it.Vars = includedTaskfile.Vars
108113
return nil
109114
}
115+
116+
// FullTaskfilePath returns the fully qualified path to the included taskfile
117+
func (it *IncludedTaskfile) FullTaskfilePath() (string, error) {
118+
return it.resolvePath(it.Taskfile)
119+
}
120+
121+
// FullDirPath returns the fully qualified path to the included taskfile's working directory
122+
func (it *IncludedTaskfile) FullDirPath() (string, error) {
123+
return it.resolvePath(it.Dir)
124+
}
125+
126+
func (it *IncludedTaskfile) resolvePath(path string) (string, error) {
127+
path, err := execext.Expand(path)
128+
if err != nil {
129+
return "", err
130+
}
131+
132+
if filepath.IsAbs(path) {
133+
return path, nil
134+
}
135+
136+
result, err := filepath.Abs(filepath.Join(it.BaseDir, path))
137+
if err != nil {
138+
return "", fmt.Errorf("task: error resolving path %s relative to %s: %w", path, it.BaseDir, err)
139+
}
140+
141+
return result, nil
142+
}

taskfile/read/taskfile.go

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99

1010
"gopkg.in/yaml.v3"
1111

12-
"github.com/go-task/task/v3/internal/execext"
1312
"github.com/go-task/task/v3/internal/templater"
1413
"github.com/go-task/task/v3/taskfile"
1514
)
@@ -44,6 +43,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
4443
}
4544
readerNode.Dir = d
4645
}
46+
4747
path, err := exists(filepath.Join(readerNode.Dir, readerNode.Entrypoint))
4848
if err != nil {
4949
return nil, err
@@ -60,6 +60,16 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
6060
return nil, err
6161
}
6262

63+
// Annotate any included Taskfile reference with a base directory for resolving relative paths
64+
_ = t.Includes.Range(func(key string, includedFile taskfile.IncludedTaskfile) error {
65+
// Set the base directory for resolving relative paths, but only if not already set
66+
if includedFile.BaseDir == "" {
67+
includedFile.BaseDir = readerNode.Dir
68+
t.Includes.Set(key, includedFile)
69+
}
70+
return nil
71+
})
72+
6373
err = t.Includes.Range(func(namespace string, includedTask taskfile.IncludedTaskfile) error {
6474
if v >= 3.0 {
6575
tr := templater.Templater{Vars: &taskfile.Vars{}, RemoveNoValue: true}
@@ -69,19 +79,18 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
6979
Optional: includedTask.Optional,
7080
AdvancedImport: includedTask.AdvancedImport,
7181
Vars: includedTask.Vars,
82+
BaseDir: includedTask.BaseDir,
7283
}
7384
if err := tr.Err(); err != nil {
7485
return err
7586
}
7687
}
7788

78-
path, err := execext.Expand(includedTask.Taskfile)
89+
path, err := includedTask.FullTaskfilePath()
7990
if err != nil {
8091
return err
8192
}
82-
if !filepath.IsAbs(path) {
83-
path = filepath.Join(readerNode.Dir, path)
84-
}
93+
8594
path, err = exists(path)
8695
if err != nil {
8796
if includedTask.Optional {
@@ -114,21 +123,27 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
114123
}
115124

116125
if includedTask.AdvancedImport {
126+
dir, err := includedTask.FullDirPath()
127+
if err != nil {
128+
return err
129+
}
130+
117131
for k, v := range includedTaskfile.Vars.Mapping {
118132
o := v
119-
o.Dir = filepath.Join(readerNode.Dir, includedTask.Dir)
133+
o.Dir = dir
120134
includedTaskfile.Vars.Mapping[k] = o
121135
}
122136
for k, v := range includedTaskfile.Env.Mapping {
123137
o := v
124-
o.Dir = filepath.Join(readerNode.Dir, includedTask.Dir)
138+
o.Dir = dir
125139
includedTaskfile.Env.Mapping[k] = o
126140
}
127141

128142
for _, task := range includedTaskfile.Tasks {
129143
if !filepath.IsAbs(task.Dir) {
130-
task.Dir = filepath.Join(includedTask.Dir, task.Dir)
144+
task.Dir = filepath.Join(dir, task.Dir)
131145
}
146+
132147
task.IncludeVars = includedTask.Vars
133148
task.IncludedTaskfileVars = includedTaskfile.Vars
134149
}
@@ -176,19 +191,29 @@ func readTaskfile(file string) (*taskfile.Taskfile, error) {
176191
return &t, yaml.NewDecoder(f).Decode(&t)
177192
}
178193

194+
// exists finds a Taskfile at the stated location, returning a fully qualified path to the file
179195
func exists(path string) (string, error) {
180196
fi, err := os.Stat(path)
181197
if err != nil {
182198
return "", err
183199
}
184200
if fi.Mode().IsRegular() {
185-
return path, nil
201+
// File exists, return a fully qualified path
202+
result, err := filepath.Abs(path)
203+
if err != nil {
204+
return "", err
205+
}
206+
return result, nil
186207
}
187208

188209
for _, n := range defaultTaskfiles {
189210
fpath := filepath.Join(path, n)
190211
if _, err := os.Stat(fpath); err == nil {
191-
return fpath, nil
212+
result, err := filepath.Abs(fpath)
213+
if err != nil {
214+
return "", err
215+
}
216+
return result, nil
192217
}
193218
}
194219

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: '3'
2+
3+
includes:
4+
included:
5+
taskfile: ./included
6+
dir: ./included
7+
8+
common:
9+
taskfile: ./common
10+
dir: ./common
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
version: '3'
2+
3+
tasks:
4+
pwd: pwd
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
version: '3'
2+
3+
includes:
4+
common:
5+
taskfile: ../common
6+
dir: ../common

0 commit comments

Comments
 (0)