mirror of
https://github.com/go-task/task.git
synced 2025-06-08 23:56:21 +02:00
Only run task once for #53
This commit is contained in:
parent
a7b59e5b12
commit
97c85e39c3
@ -453,6 +453,41 @@ tasks:
|
|||||||
- echo "I will not run"
|
- echo "I will not run"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Limiting when tasks run
|
||||||
|
|
||||||
|
If a task executed by multiple `cmds` or multiple `deps` you can limit
|
||||||
|
how many times it is executed each invocation of the `task` command using `run`. `run` can also be set at the root of the taskfile to change the behavior of all the tasks unless explicitly overridden
|
||||||
|
|
||||||
|
Supported values for `run`
|
||||||
|
* `always` (default) always attempt to invoke the task regardless of the number of previous executions
|
||||||
|
* `once` only invoke this task once regardless of the number of times referred to
|
||||||
|
* `when_changed` only invokes the task once for a set of variables passed into the task
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
tasks:
|
||||||
|
default:
|
||||||
|
cmds:
|
||||||
|
- task: generate-file
|
||||||
|
vars: { CONTENT: '1' }
|
||||||
|
- task: generate-file
|
||||||
|
vars: { CONTENT: '2' }
|
||||||
|
- task: generate-file
|
||||||
|
vars: { CONTENT: '2' }
|
||||||
|
|
||||||
|
generate-file:
|
||||||
|
run: when_changed
|
||||||
|
deps:
|
||||||
|
- install-deps
|
||||||
|
cmds:
|
||||||
|
- echo {{.CONTENT}}
|
||||||
|
|
||||||
|
install-deps:
|
||||||
|
run: once
|
||||||
|
cmds:
|
||||||
|
- sleep 5 # long operation like installing packages
|
||||||
|
```
|
||||||
|
|
||||||
## Variables
|
## Variables
|
||||||
|
|
||||||
When doing interpolation of variables, Task will look for the below.
|
When doing interpolation of variables, Task will look for the below.
|
||||||
|
1
go.mod
1
go.mod
@ -5,6 +5,7 @@ require (
|
|||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0
|
||||||
github.com/joho/godotenv v1.3.0
|
github.com/joho/godotenv v1.3.0
|
||||||
github.com/mattn/go-zglob v0.0.3
|
github.com/mattn/go-zglob v0.0.3
|
||||||
|
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||||
github.com/radovskyb/watcher v1.0.7
|
github.com/radovskyb/watcher v1.0.7
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
|
2
go.sum
2
go.sum
@ -27,6 +27,8 @@ github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHX
|
|||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-zglob v0.0.3 h1:6Ry4EYsScDyt5di4OI6xw1bYhOqfE5S33Z1OPy+d+To=
|
github.com/mattn/go-zglob v0.0.3 h1:6Ry4EYsScDyt5di4OI6xw1bYhOqfE5S33Z1OPy+d+To=
|
||||||
github.com/mattn/go-zglob v0.0.3/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
github.com/mattn/go-zglob v0.0.3/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
||||||
|
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||||
|
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
28
hash.go
Normal file
28
hash.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-task/task/v3/internal/hash"
|
||||||
|
"github.com/go-task/task/v3/taskfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *Executor) GetHash(t *taskfile.Task) (string, error) {
|
||||||
|
r := t.Run
|
||||||
|
if r == "" {
|
||||||
|
r = e.Taskfile.Run
|
||||||
|
}
|
||||||
|
|
||||||
|
var h hash.HashFunc
|
||||||
|
switch r {
|
||||||
|
case "always":
|
||||||
|
h = hash.Empty
|
||||||
|
case "once":
|
||||||
|
h = hash.Name
|
||||||
|
case "when_changed":
|
||||||
|
h = hash.Hash
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf(`task: invalid run "%s"`, r)
|
||||||
|
}
|
||||||
|
return h(t)
|
||||||
|
}
|
23
internal/hash/hash.go
Normal file
23
internal/hash/hash.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package hash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-task/task/v3/taskfile"
|
||||||
|
"github.com/mitchellh/hashstructure/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HashFunc func(*taskfile.Task) (string, error)
|
||||||
|
|
||||||
|
func Empty(*taskfile.Task) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Name(t *taskfile.Task) (string, error) {
|
||||||
|
return t.Task, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hash(t *taskfile.Task) (string, error) {
|
||||||
|
h, err := hashstructure.Hash(t, hashstructure.FormatV2, nil)
|
||||||
|
return fmt.Sprintf("%s:%d", t.Task, h), err
|
||||||
|
}
|
41
task.go
41
task.go
@ -58,6 +58,8 @@ type Executor struct {
|
|||||||
concurrencySemaphore chan struct{}
|
concurrencySemaphore chan struct{}
|
||||||
taskCallCount map[string]*int32
|
taskCallCount map[string]*int32
|
||||||
mkdirMutexMap map[string]*sync.Mutex
|
mkdirMutexMap map[string]*sync.Mutex
|
||||||
|
execution map[string]context.Context
|
||||||
|
executionMutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run runs Task
|
// Run runs Task
|
||||||
@ -225,6 +227,10 @@ func (e *Executor) Setup() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e.Taskfile.Run == "" {
|
||||||
|
e.Taskfile.Run = "always"
|
||||||
|
}
|
||||||
|
|
||||||
if v <= 2.1 {
|
if v <= 2.1 {
|
||||||
err := errors.New(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`)
|
err := errors.New(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`)
|
||||||
|
|
||||||
@ -260,6 +266,8 @@ func (e *Executor) Setup() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.execution = make(map[string]context.Context)
|
||||||
|
|
||||||
e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks))
|
e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks))
|
||||||
e.mkdirMutexMap = make(map[string]*sync.Mutex, len(e.Taskfile.Tasks))
|
e.mkdirMutexMap = make(map[string]*sync.Mutex, len(e.Taskfile.Tasks))
|
||||||
for k := range e.Taskfile.Tasks {
|
for k := range e.Taskfile.Tasks {
|
||||||
@ -286,6 +294,17 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
|
|||||||
release := e.acquireConcurrencyLimit()
|
release := e.acquireConcurrencyLimit()
|
||||||
defer release()
|
defer release()
|
||||||
|
|
||||||
|
started, ctx, cancel, err := e.startExecution(ctx, t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if !started {
|
||||||
|
<-ctx.Done()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := e.runDeps(ctx, t); err != nil {
|
if err := e.runDeps(ctx, t); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -445,3 +464,25 @@ func getEnviron(t *taskfile.Task) []string {
|
|||||||
|
|
||||||
return environ
|
return environ
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Executor) startExecution(innerCtx context.Context, t *taskfile.Task) (bool, context.Context, context.CancelFunc, error) {
|
||||||
|
h, err := e.GetHash(t)
|
||||||
|
if err != nil {
|
||||||
|
return true, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if h == "" {
|
||||||
|
return true, innerCtx, func() {}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
e.executionMutex.Lock()
|
||||||
|
defer e.executionMutex.Unlock()
|
||||||
|
ctx, ok := e.execution[h]
|
||||||
|
if ok {
|
||||||
|
return false, ctx, func() {}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(innerCtx)
|
||||||
|
e.execution[h] = ctx
|
||||||
|
return true, ctx, cancel, nil
|
||||||
|
}
|
||||||
|
11
task_test.go
11
task_test.go
@ -979,3 +979,14 @@ func TestExitImmediately(t *testing.T) {
|
|||||||
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
|
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
|
||||||
assert.Contains(t, buff.String(), `"this_should_fail": executable file not found in $PATH`)
|
assert.Contains(t, buff.String(), `"this_should_fail": executable file not found in $PATH`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunOnlyRunsJobsHashOnce(t *testing.T) {
|
||||||
|
tt := fileContentTest{
|
||||||
|
Dir: "testdata/run",
|
||||||
|
Target: "generate-hash",
|
||||||
|
Files: map[string]string{
|
||||||
|
"hash.txt": "starting 1\n1\n2\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tt.Run(t)
|
||||||
|
}
|
||||||
|
@ -22,6 +22,7 @@ type Task struct {
|
|||||||
Method string
|
Method string
|
||||||
Prefix string
|
Prefix string
|
||||||
IgnoreError bool
|
IgnoreError bool
|
||||||
|
Run string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) Name() string {
|
func (t *Task) Name() string {
|
||||||
@ -61,6 +62,7 @@ func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||||||
Method string
|
Method string
|
||||||
Prefix string
|
Prefix string
|
||||||
IgnoreError bool `yaml:"ignore_error"`
|
IgnoreError bool `yaml:"ignore_error"`
|
||||||
|
Run string
|
||||||
}
|
}
|
||||||
if err := unmarshal(&task); err != nil {
|
if err := unmarshal(&task); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -81,5 +83,6 @@ func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||||||
t.Method = task.Method
|
t.Method = task.Method
|
||||||
t.Prefix = task.Prefix
|
t.Prefix = task.Prefix
|
||||||
t.IgnoreError = task.IgnoreError
|
t.IgnoreError = task.IgnoreError
|
||||||
|
t.Run = task.Run
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ type Taskfile struct {
|
|||||||
Tasks Tasks
|
Tasks Tasks
|
||||||
Silent bool
|
Silent bool
|
||||||
Dotenv []string
|
Dotenv []string
|
||||||
|
Run string
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML implements yaml.Unmarshaler interface
|
// UnmarshalYAML implements yaml.Unmarshaler interface
|
||||||
@ -32,6 +33,7 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||||||
Tasks Tasks
|
Tasks Tasks
|
||||||
Silent bool
|
Silent bool
|
||||||
Dotenv []string
|
Dotenv []string
|
||||||
|
Run string
|
||||||
}
|
}
|
||||||
if err := unmarshal(&taskfile); err != nil {
|
if err := unmarshal(&taskfile); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -46,6 +48,7 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||||||
tf.Tasks = taskfile.Tasks
|
tf.Tasks = taskfile.Tasks
|
||||||
tf.Silent = taskfile.Silent
|
tf.Silent = taskfile.Silent
|
||||||
tf.Dotenv = taskfile.Dotenv
|
tf.Dotenv = taskfile.Dotenv
|
||||||
|
tf.Run = taskfile.Run
|
||||||
if tf.Expansions <= 0 {
|
if tf.Expansions <= 0 {
|
||||||
tf.Expansions = 2
|
tf.Expansions = 2
|
||||||
}
|
}
|
||||||
|
1
testdata/run/.gitignore
vendored
Normal file
1
testdata/run/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.txt
|
24
testdata/run/Taskfile.yml
vendored
Normal file
24
testdata/run/Taskfile.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
version: '3'
|
||||||
|
run: when_changed
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
generate-hash:
|
||||||
|
- rm -f hash.txt
|
||||||
|
- task: input-content
|
||||||
|
vars: { CONTENT: '1' }
|
||||||
|
- task: input-content
|
||||||
|
vars: { CONTENT: '2' }
|
||||||
|
- task: input-content
|
||||||
|
vars: { CONTENT: '2' }
|
||||||
|
|
||||||
|
input-content:
|
||||||
|
deps:
|
||||||
|
- task: create-output
|
||||||
|
vars: { CONTENT: '1' }
|
||||||
|
cmds:
|
||||||
|
- echo {{.CONTENT}} >> hash.txt
|
||||||
|
|
||||||
|
create-output:
|
||||||
|
run: once
|
||||||
|
cmds:
|
||||||
|
- echo starting {{.CONTENT}} >> hash.txt
|
@ -59,6 +59,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
|
|||||||
Method: r.Replace(origTask.Method),
|
Method: r.Replace(origTask.Method),
|
||||||
Prefix: r.Replace(origTask.Prefix),
|
Prefix: r.Replace(origTask.Prefix),
|
||||||
IgnoreError: origTask.IgnoreError,
|
IgnoreError: origTask.IgnoreError,
|
||||||
|
Run: r.Replace(origTask.Run),
|
||||||
}
|
}
|
||||||
new.Dir, err = execext.Expand(new.Dir)
|
new.Dir, err = execext.Expand(new.Dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user