mirror of
https://github.com/go-task/task.git
synced 2025-01-06 03:53:54 +02:00
Run Taskfiles from sub/child directories (#920)
This commit is contained in:
parent
99d7338c29
commit
b3627fcb18
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
## Unreleased
|
## 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
|
- Add task-level `dotenv` support
|
||||||
([#389](https://github.com/go-task/task/issues/389), [#904](https://github.com/go-task/task/pull/904)).
|
([#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`
|
- It's now possible to use global level variables on `includes`
|
||||||
|
@ -55,6 +55,7 @@ There are some special variables that is available on the templating system:
|
|||||||
| `TASK` | The name of the current task. |
|
| `TASK` | The name of the current task. |
|
||||||
| `ROOT_DIR` | The absolute path of the root Taskfile. |
|
| `ROOT_DIR` | The absolute path of the root Taskfile. |
|
||||||
| `TASKFILE_DIR` | The absolute path of the included 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`. |
|
| `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`. |
|
| `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`. |
|
||||||
|
|
||||||
|
@ -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
|
the Taskfile by adding an additional `Taskfile.yml` (which would be on
|
||||||
`.gitignore`).
|
`.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
|
## Environment variables
|
||||||
|
|
||||||
### Task
|
### Task
|
||||||
|
@ -18,7 +18,8 @@ import (
|
|||||||
var _ compiler.Compiler = &CompilerV3{}
|
var _ compiler.Compiler = &CompilerV3{}
|
||||||
|
|
||||||
type CompilerV3 struct {
|
type CompilerV3 struct {
|
||||||
Dir string
|
Dir string
|
||||||
|
UserWorkingDir string
|
||||||
|
|
||||||
TaskfileEnv *taskfile.Vars
|
TaskfileEnv *taskfile.Vars
|
||||||
TaskfileVars *taskfile.Vars
|
TaskfileVars *taskfile.Vars
|
||||||
@ -179,9 +180,10 @@ func (c *CompilerV3) getSpecialVars(t *taskfile.Task) (map[string]string, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
"TASK": t.Task,
|
"TASK": t.Task,
|
||||||
"ROOT_DIR": c.Dir,
|
"ROOT_DIR": c.Dir,
|
||||||
"TASKFILE_DIR": taskfileDir,
|
"TASKFILE_DIR": taskfileDir,
|
||||||
|
"USER_WORKING_DIR": c.UserWorkingDir,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
22
internal/sysinfo/uid.go
Normal file
22
internal/sysinfo/uid.go
Normal 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
|
||||||
|
}
|
9
internal/sysinfo/uid_win.go
Normal file
9
internal/sysinfo/uid_win.go
Normal 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
|
||||||
|
}
|
15
setup.go
15
setup.go
@ -80,7 +80,7 @@ func (e *Executor) setCurrentDir() error {
|
|||||||
|
|
||||||
func (e *Executor) readTaskfile() error {
|
func (e *Executor) readTaskfile() error {
|
||||||
var err error
|
var err error
|
||||||
e.Taskfile, err = read.Taskfile(&read.ReaderNode{
|
e.Taskfile, e.Dir, err = read.Taskfile(&read.ReaderNode{
|
||||||
Dir: e.Dir,
|
Dir: e.Dir,
|
||||||
Entrypoint: e.Entrypoint,
|
Entrypoint: e.Entrypoint,
|
||||||
Parent: nil,
|
Parent: nil,
|
||||||
@ -179,11 +179,16 @@ func (e *Executor) setupCompiler(v float64) error {
|
|||||||
Logger: e.Logger,
|
Logger: e.Logger,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
userWorkingDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
e.Compiler = &compilerv3.CompilerV3{
|
e.Compiler = &compilerv3.CompilerV3{
|
||||||
Dir: e.Dir,
|
Dir: e.Dir,
|
||||||
TaskfileEnv: e.Taskfile.Env,
|
UserWorkingDir: userWorkingDir,
|
||||||
TaskfileVars: e.Taskfile.Vars,
|
TaskfileEnv: e.Taskfile.Env,
|
||||||
Logger: e.Logger,
|
TaskfileVars: e.Taskfile.Vars,
|
||||||
|
Logger: e.Logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
49
task_test.go
49
task_test.go
@ -1703,3 +1703,52 @@ Hello, World!
|
|||||||
err = os.RemoveAll(filepathext.SmartJoin(dir, "src"))
|
err = os.RemoveAll(filepathext.SmartJoin(dir, "src"))
|
||||||
assert.NoError(t, err)
|
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())
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/go-task/task/v3/internal/filepathext"
|
"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/internal/templater"
|
||||||
"github.com/go-task/task/v3/taskfile"
|
"github.com/go-task/task/v3/taskfile"
|
||||||
)
|
)
|
||||||
@ -36,29 +37,30 @@ type ReaderNode struct {
|
|||||||
// Taskfile reads a Taskfile for a given directory
|
// Taskfile reads a Taskfile for a given directory
|
||||||
// Uses current dir when dir is left empty. Uses Taskfile.yml
|
// Uses current dir when dir is left empty. Uses Taskfile.yml
|
||||||
// or Taskfile.yaml when entrypoint is left empty
|
// 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 == "" {
|
if readerNode.Dir == "" {
|
||||||
d, err := os.Getwd()
|
d, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
readerNode.Dir = d
|
readerNode.Dir = d
|
||||||
}
|
}
|
||||||
|
|
||||||
path, err := exists(filepathext.SmartJoin(readerNode.Dir, readerNode.Entrypoint))
|
path, err := existsWalk(filepathext.SmartJoin(readerNode.Dir, readerNode.Entrypoint))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
readerNode.Dir = filepath.Dir(path)
|
||||||
readerNode.Entrypoint = filepath.Base(path)
|
readerNode.Entrypoint = filepath.Base(path)
|
||||||
|
|
||||||
t, err := readTaskfile(path)
|
t, err := readTaskfile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := t.ParsedVersion()
|
v, err := t.ParsedVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annotate any included Taskfile reference with a base directory for resolving relative paths
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
includedTaskfile, err := Taskfile(includeReaderNode)
|
includedTaskfile, _, err := Taskfile(includeReaderNode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if includedTask.Optional {
|
if includedTask.Optional {
|
||||||
return nil
|
return nil
|
||||||
@ -163,7 +165,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if v < 3.0 {
|
if v < 3.0 {
|
||||||
@ -171,10 +173,10 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
|
|||||||
if _, err = os.Stat(path); err == nil {
|
if _, err = os.Stat(path); err == nil {
|
||||||
osTaskfile, err := readTaskfile(path)
|
osTaskfile, err := readTaskfile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
if err = taskfile.Merge(t, osTaskfile, nil); err != nil {
|
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
|
task.Task = name
|
||||||
}
|
}
|
||||||
|
|
||||||
return t, nil
|
return t, readerNode.Dir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readTaskfile(file string) (*taskfile.Taskfile, error) {
|
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)
|
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 {
|
func checkCircularIncludes(node *ReaderNode) error {
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return errors.New("task: failed to check for include cycle: node was nil")
|
return errors.New("task: failed to check for include cycle: node was nil")
|
||||||
|
7
testdata/taskfile_walk/Taskfile.yml
vendored
Normal file
7
testdata/taskfile_walk/Taskfile.yml
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
default:
|
||||||
|
cmds:
|
||||||
|
- echo 'foo'
|
||||||
|
silent: true
|
0
testdata/taskfile_walk/foo/bar/.gitkeep
vendored
Normal file
0
testdata/taskfile_walk/foo/bar/.gitkeep
vendored
Normal file
7
testdata/user_working_dir/Taskfile.yml
vendored
Normal file
7
testdata/user_working_dir/Taskfile.yml
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
default:
|
||||||
|
cmds:
|
||||||
|
- echo '{{.USER_WORKING_DIR}}'
|
||||||
|
silent: true
|
Loading…
Reference in New Issue
Block a user