1
0
mirror of https://github.com/go-task/task.git synced 2025-01-04 03:48:02 +02:00

Run Taskfiles from sub/child directories (#920)

This commit is contained in:
Pete Davison 2022-12-06 00:58:20 +00:00 committed by GitHub
parent 99d7338c29
commit b3627fcb18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 186 additions and 20 deletions

View File

@ -2,6 +2,9 @@
## Unreleased
- It's now possible to run Taskfiles from subdirectories! A new `USER_WORKING_DIR` special
variable was added to add even more flexibility for monorepos
([#289](https://github.com/go-task/task/issues/289), [#920](https://github.com/go-task/task/pull/920)).
- Add task-level `dotenv` support
([#389](https://github.com/go-task/task/issues/389), [#904](https://github.com/go-task/task/pull/904)).
- It's now possible to use global level variables on `includes`

View File

@ -55,6 +55,7 @@ There are some special variables that is available on the templating system:
| `TASK` | The name of the current task. |
| `ROOT_DIR` | The absolute path of the root Taskfile. |
| `TASKFILE_DIR` | The absolute path of the included Taskfile. |
| `USER_WORKING_DIR` | The absolute path of the directory `task` was called from. |
| `CHECKSUM` | The checksum of the files listed in `sources`. Only available within the `status` prop and if method is set to `checksum`. |
| `TIMESTAMP` | The date object of the greatest timestamp of the files listes in `sources`. Only available within the `status` prop and if method is set to `timestamp`. |

View File

@ -52,6 +52,35 @@ committed version (`.dist`) while still allowing individual users to override
the Taskfile by adding an additional `Taskfile.yml` (which would be on
`.gitignore`).
### Running a Taskfile from a subdirectory
If a Taskfile cannot be found in the current working directory, it will walk up
the file tree until it finds one (similar to how `git` works). When running Task
from a subdirectory like this, it will behave as if you ran it from the
directory containing the Taskfile.
You can use this functionality along with the special `{{.USER_WORKING_DIR}}`
variable to create some very useful reusable tasks. For example, if you have a
monorepo with directories for each microservice, you can `cd` into a
microservice directory and run a task command to bring it up without having to
create multiple tasks or Taskfiles with identical content. For example:
```yaml
version: '3'
tasks:
up:
dir: '{{.USER_WORKING_DIR}}'
preconditions:
- test -f docker-compose.yml
cmds:
- docker-compose up -d
```
In this example, we can run `cd <service>` and `task up` and as long as the
`<service>` directory contains a `docker-compose.yml`, the Docker composition will be
brought up.
## Environment variables
### Task

View File

@ -18,7 +18,8 @@ import (
var _ compiler.Compiler = &CompilerV3{}
type CompilerV3 struct {
Dir string
Dir string
UserWorkingDir string
TaskfileEnv *taskfile.Vars
TaskfileVars *taskfile.Vars
@ -179,9 +180,10 @@ func (c *CompilerV3) getSpecialVars(t *taskfile.Task) (map[string]string, error)
}
return map[string]string{
"TASK": t.Task,
"ROOT_DIR": c.Dir,
"TASKFILE_DIR": taskfileDir,
"TASK": t.Task,
"ROOT_DIR": c.Dir,
"TASKFILE_DIR": taskfileDir,
"USER_WORKING_DIR": c.UserWorkingDir,
}, nil
}

22
internal/sysinfo/uid.go Normal file
View File

@ -0,0 +1,22 @@
//go:build !windows
package sysinfo
import (
"os"
"syscall"
)
func Owner(path string) (int, error) {
info, err := os.Stat(path)
if err != nil {
return 0, err
}
var uid int
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
uid = int(stat.Uid)
} else {
uid = os.Getuid()
}
return uid, nil
}

View File

@ -0,0 +1,9 @@
//go:build windows
package sysinfo
// NOTE: This always returns -1 since there is currently no easy way to get
// file owner information on Windows.
func Owner(path string) (int, error) {
return -1, nil
}

View File

@ -80,7 +80,7 @@ func (e *Executor) setCurrentDir() error {
func (e *Executor) readTaskfile() error {
var err error
e.Taskfile, err = read.Taskfile(&read.ReaderNode{
e.Taskfile, e.Dir, err = read.Taskfile(&read.ReaderNode{
Dir: e.Dir,
Entrypoint: e.Entrypoint,
Parent: nil,
@ -179,11 +179,16 @@ func (e *Executor) setupCompiler(v float64) error {
Logger: e.Logger,
}
} else {
userWorkingDir, err := os.Getwd()
if err != nil {
return err
}
e.Compiler = &compilerv3.CompilerV3{
Dir: e.Dir,
TaskfileEnv: e.Taskfile.Env,
TaskfileVars: e.Taskfile.Vars,
Logger: e.Logger,
Dir: e.Dir,
UserWorkingDir: userWorkingDir,
TaskfileEnv: e.Taskfile.Env,
TaskfileVars: e.Taskfile.Vars,
Logger: e.Logger,
}
}

View File

@ -1703,3 +1703,52 @@ Hello, World!
err = os.RemoveAll(filepathext.SmartJoin(dir, "src"))
assert.NoError(t, err)
}
func TestTaskfileWalk(t *testing.T) {
tests := []struct {
name string
dir string
expected string
}{
{
name: "walk from root directory",
dir: "testdata/taskfile_walk",
expected: "foo\n",
}, {
name: "walk from sub directory",
dir: "testdata/taskfile_walk/foo",
expected: "foo\n",
}, {
name: "walk from sub sub directory",
dir: "testdata/taskfile_walk/foo/bar",
expected: "foo\n",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: test.dir,
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
assert.Equal(t, test.expected, buff.String())
})
}
}
func TestUserWorkingDirectory(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/user_working_dir",
Stdout: &buff,
Stderr: &buff,
}
wd, err := os.Getwd()
assert.NoError(t, err)
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String())
}

View File

@ -10,6 +10,7 @@ import (
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/sysinfo"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile"
)
@ -36,29 +37,30 @@ type ReaderNode struct {
// 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(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, string, error) {
if readerNode.Dir == "" {
d, err := os.Getwd()
if err != nil {
return nil, err
return nil, "", err
}
readerNode.Dir = d
}
path, err := exists(filepathext.SmartJoin(readerNode.Dir, readerNode.Entrypoint))
path, err := existsWalk(filepathext.SmartJoin(readerNode.Dir, readerNode.Entrypoint))
if err != nil {
return nil, err
return nil, "", err
}
readerNode.Dir = filepath.Dir(path)
readerNode.Entrypoint = filepath.Base(path)
t, err := readTaskfile(path)
if err != nil {
return nil, err
return nil, "", err
}
v, err := t.ParsedVersion()
if err != nil {
return nil, err
return nil, "", err
}
// Annotate any included Taskfile reference with a base directory for resolving relative paths
@ -113,7 +115,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
return err
}
includedTaskfile, err := Taskfile(includeReaderNode)
includedTaskfile, _, err := Taskfile(includeReaderNode)
if err != nil {
if includedTask.Optional {
return nil
@ -163,7 +165,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
return nil
})
if err != nil {
return nil, err
return nil, "", err
}
if v < 3.0 {
@ -171,10 +173,10 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
if _, err = os.Stat(path); err == nil {
osTaskfile, err := readTaskfile(path)
if err != nil {
return nil, err
return nil, "", err
}
if err = taskfile.Merge(t, osTaskfile, nil); err != nil {
return nil, err
return nil, "", err
}
}
}
@ -187,7 +189,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
task.Task = name
}
return t, nil
return t, readerNode.Dir, nil
}
func readTaskfile(file string) (*taskfile.Taskfile, error) {
@ -221,6 +223,36 @@ 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 existsWalk(path string) (string, error) {
origPath := path
owner, err := sysinfo.Owner(path)
if err != nil {
return "", err
}
for {
fpath, err := exists(path)
if err == nil {
return fpath, nil
}
// Get the parent path/user id
parentPath := filepath.Dir(path)
parentOwner, err := sysinfo.Owner(parentPath)
if err != nil {
return "", err
}
// Error if we reached the root directory and still haven't found a file
// OR if the user id of the directory changes
if path == parentPath || (parentOwner != owner) {
return "", fmt.Errorf(`task: No Taskfile found in "%s" (or any of the parent directories). Use "task --init" to create a new one`, origPath)
}
owner = parentOwner
path = parentPath
}
}
func checkCircularIncludes(node *ReaderNode) error {
if node == nil {
return errors.New("task: failed to check for include cycle: node was nil")

7
testdata/taskfile_walk/Taskfile.yml vendored Normal file
View File

@ -0,0 +1,7 @@
version: '3'
tasks:
default:
cmds:
- echo 'foo'
silent: true

View File

View File

@ -0,0 +1,7 @@
version: '3'
tasks:
default:
cmds:
- echo '{{.USER_WORKING_DIR}}'
silent: true