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:
parent
99d7338c29
commit
b3627fcb18
@ -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`
|
||||
|
@ -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`. |
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
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 {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
49
task_test.go
49
task_test.go
@ -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())
|
||||
}
|
||||
|
@ -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
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