Repository: go-task/task

Issue:
Included Taskfiles have wrong working directory with USER_WORKING_DIR
I am having trouble executing tasks from an included file in the current working directory while using Taskfile.yml. 

My goal is to be able to navigate to any sub-folder, run a task command, which would use the Taskfile.yml at the root, which in turn includes utilities and commands from other folders. 

Here's a minimal example:

Taskfile.yml:
```
version: '3'

includes:
  info:
    taskfile: 'subfolder/Taskfile.yml'
    # dir: '{{.USER_WORKING_DIR}}

tasks:
  default: 
    cmds:
      - echo {{.USER_WORKING_DIR}}
      - pwd

```

subfolder/Taskfile.info.yml:

```
version: '3'

tasks:
  default: 
    cmds:
      - echo {{.USER_WORKING_DIR}}
      - pwd
    # dir: '{{.USER_WORKING_DIR}}'


```

When I'm inside the subfolder and run `task info`, the output is (as expected):

```
/home/user/ws/subfolder
/home/user/ws
```

Adding `dir: '{{.USER_WORKING_DIR}}'` to the include statement in Taskfile.yml, has no effect, while I would have expected it to resolve to /home/user/ws/subfolder.

Adding `dir: '{{.USER_WORKING_DIR}}'` to the task in subfolder/Taskfile.info.yml yields:

```
/home/user/ws/subfolder
/home/user/ws/home/user/ws/subfolder   (?)
```

It seems that `USER_WORKING_DIR` resolves to the correct directory, but the commands are not running there. Perhaps I'm using the functionality incorrectly?

A use-case is where I'm using the "--global" flag and have the aforementioned structure in my $HOME folder. 
I can only get tasks in the 'root' Taskfile to execute in the current working context, but not the included ones. 


- Task version: v3.26.0
- Operating system: Linux / Debian Bookworm
- Experiments enabled: No


Discussion:
@idelchi: The combination 

Taskfile.yml
```
includes:
  info:
    taskfile: 'subfolder/Taskfile.yml'
    dir: '{{.USER_WORKING_DIR}}'
```

subfolder/Taskfile.yml
```
tasks:
  default:
    cmds:
      - echo {{.USER_WORKING_DIR}}
      - pwd
```

Will still run the `cmds` in the top-level `Taskfile.yml` folder, and not in the current working directory.

```
user@host:/var$ task -g info
task: [info:default] echo /var
/var
task: [info:default] pwd
/home/user
```

I'm no longer sure if this is the intended behavior though! I can work around this by assigning `dir: $PWD`

Although I guess the issue is that `USER_WORKING_DIR` variable is not available in the `include` statement. @lucasfcnunes: @idelchi I too want to do something similar.


```yaml
# <top_level_folder>/Taskfile.yaml
version: '3'
...
includes:
  current:
    # internal: true
    dir: '{{.USER_WORKING_DIR}}'
    taskfile: '{{.USER_WORKING_DIR}}/_Taskfile.yaml'
    # optional: true
...
```

```yaml
# <top_level_folder>/<...>/_Taskfile.yaml
version: '3'
...
tasks:
  pwd: pwd
...
```@jdorel: I believe I'm experiencing this bug in version `3.45.4`.
Substituting `dir: '{{.USER_WORKING_DIR}}'` with `dir: $PWD` works as a workaround.@adlion: +1

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4877766..d3e668da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,9 @@
 - Fix a missing a line break on log when using `--watch` mode (#1285, #1297 by
   @FilipSolich).
 - Fix `defer` on JSON Schema (#1288 by @calvinmclean and @andreynering).
+- Fix bug in usage of special variables like `{{.USER_WORKING_DIR}}` in
+  combination with `includes` (#1046, #1205, #1250, #1293, #1312, #1274 by
+  @andarto, #1309 by @andreynering).
 
 ## v3.28.0 - 2023-07-24
 
diff --git a/internal/filepathext/filepathext.go b/internal/filepathext/filepathext.go
index 16ed5d0a..db64695c 100644
--- a/internal/filepathext/filepathext.go
+++ b/internal/filepathext/filepathext.go
@@ -3,17 +3,43 @@ package filepathext
 import (
 	"os"
 	"path/filepath"
+	"strings"
 )
 
 // SmartJoin joins two paths, but only if the second is not already an
 // absolute path.
 func SmartJoin(a, b string) string {
-	if filepath.IsAbs(b) {
+	if IsAbs(b) {
 		return b
 	}
 	return filepath.Join(a, b)
 }
 
+func IsAbs(path string) bool {
+	// NOTE(@andreynering): If the path contains any if the special
+	// variables that we know are absolute, return true.
+	if isSpecialDir(path) {
+		return true
+	}
+
+	return filepath.IsAbs(path)
+}
+
+var knownAbsDirs = []string{
+	".ROOT_DIR",
+	".TASKFILE_DIR",
+	".USER_WORKING_DIR",
+}
+
+func isSpecialDir(dir string) bool {
+	for _, d := range knownAbsDirs {
+		if strings.Contains(dir, d) {
+			return true
+		}
+	}
+	return false
+}
+
 // TryAbsToRel tries to convert an absolute path to relative based on the
 // process working directory. If it can't, it returns the absolute path.
 func TryAbsToRel(abs string) string {
diff --git a/setup.go b/setup.go
index 060cc782..2d1b3e6e 100644
--- a/setup.go
+++ b/setup.go
@@ -175,13 +175,17 @@ func (e *Executor) setupCompiler() error {
 			Logger:       e.Logger,
 		}
 	} else {
-		userWorkingDir, err := os.Getwd()
-		if err != nil {
-			return err
+		if e.UserWorkingDir == "" {
+			var err error
+			e.UserWorkingDir, err = os.Getwd()
+			if err != nil {
+				return err
+			}
 		}
+
 		e.Compiler = &compilerv3.CompilerV3{
 			Dir:            e.Dir,
-			UserWorkingDir: userWorkingDir,
+			UserWorkingDir: e.UserWorkingDir,
 			TaskfileEnv:    e.Taskfile.Env,
 			TaskfileVars:   e.Taskfile.Vars,
 			Logger:         e.Logger,
diff --git a/task.go b/task.go
index cb559554..dd540a41 100644
--- a/task.go
+++ b/task.go
@@ -67,11 +67,12 @@ type Executor struct {
 	Stdout io.Writer
 	Stderr io.Writer
 
-	Logger      *logger.Logger
-	Compiler    compiler.Compiler
-	Output      output.Output
-	OutputStyle taskfile.Output
-	TaskSorter  sort.TaskSorter
+	Logger         *logger.Logger
+	Compiler       compiler.Compiler
+	Output         output.Output
+	OutputStyle    taskfile.Output
+	TaskSorter     sort.TaskSorter
+	UserWorkingDir string
 
 	taskvars   *taskfile.Vars
 	fuzzyModel *fuzzy.Model
diff --git a/taskfile/included_taskfile.go b/taskfile/included_taskfile.go
index 6137344f..c409052e 100644
--- a/taskfile/included_taskfile.go
+++ b/taskfile/included_taskfile.go
@@ -153,7 +153,7 @@ func (it *IncludedTaskfile) resolvePath(path string) (string, error) {
 		return "", err
 	}
 
-	if filepath.IsAbs(path) {
+	if filepathext.IsAbs(path) {
 		return path, nil
 	}
 



Test patch:
diff --git a/task_test.go b/task_test.go
index 5c5a153a..a589629c 100644
--- a/task_test.go
+++ b/task_test.go
@@ -1940,6 +1940,26 @@ func TestUserWorkingDirectory(t *testing.T) {
 	assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String())
 }
 
+func TestUserWorkingDirectoryWithIncluded(t *testing.T) {
+	wd, err := os.Getwd()
+	require.NoError(t, err)
+
+	wd = filepathext.SmartJoin(wd, "testdata/user_working_dir_with_includes/somedir")
+
+	var buff bytes.Buffer
+	e := task.Executor{
+		UserWorkingDir: wd,
+		Dir:            "testdata/user_working_dir_with_includes",
+		Stdout:         &buff,
+		Stderr:         &buff,
+	}
+
+	require.NoError(t, err)
+	require.NoError(t, e.Setup())
+	require.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "included:echo"}))
+	assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String())
+}
+
 func TestPlatforms(t *testing.T) {
 	var buff bytes.Buffer
 	e := task.Executor{
diff --git a/testdata/user_working_dir_with_includes/Taskfile.yml b/testdata/user_working_dir_with_includes/Taskfile.yml
new file mode 100644
index 00000000..2e369b6e
--- /dev/null
+++ b/testdata/user_working_dir_with_includes/Taskfile.yml
@@ -0,0 +1,5 @@
+version: '3'
+
+includes:
+  included:
+    taskfile: ./included/Taskfile.yml
diff --git a/testdata/user_working_dir_with_includes/included/Taskfile.yml b/testdata/user_working_dir_with_includes/included/Taskfile.yml
new file mode 100644
index 00000000..ac853b76
--- /dev/null
+++ b/testdata/user_working_dir_with_includes/included/Taskfile.yml
@@ -0,0 +1,8 @@
+version: '3'
+
+tasks:
+  echo:
+    dir: '{{.USER_WORKING_DIR}}'
+    cmds:
+      - pwd
+    silent: true
diff --git a/testdata/user_working_dir_with_includes/somedir/.keep b/testdata/user_working_dir_with_includes/somedir/.keep
new file mode 100644
index 00000000..e69de29b