Skip to content

Commit

Permalink
Only run task once for #53
Browse files Browse the repository at this point in the history
  • Loading branch information
RossHammer authored and andreynering committed Jul 31, 2021
1 parent a7b59e5 commit 97c85e3
Show file tree
Hide file tree
Showing 12 changed files with 173 additions and 0 deletions.
35 changes: 35 additions & 0 deletions docs/usage.md
Expand Up @@ -453,6 +453,41 @@ tasks:
- 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

When doing interpolation of variables, Task will look for the below.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -5,6 +5,7 @@ require (
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0
github.com/joho/godotenv v1.3.0
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/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -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-zglob v0.0.3 h1:6Ry4EYsScDyt5di4OI6xw1bYhOqfE5S33Z1OPy+d+To=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down
28 changes: 28 additions & 0 deletions hash.go
@@ -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 changes: 23 additions & 0 deletions internal/hash/hash.go
@@ -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 changes: 41 additions & 0 deletions task.go
Expand Up @@ -58,6 +58,8 @@ type Executor struct {
concurrencySemaphore chan struct{}
taskCallCount map[string]*int32
mkdirMutexMap map[string]*sync.Mutex
execution map[string]context.Context
executionMutex sync.Mutex
}

// Run runs Task
Expand Down Expand Up @@ -225,6 +227,10 @@ func (e *Executor) Setup() error {
}
}

if e.Taskfile.Run == "" {
e.Taskfile.Run = "always"
}

if v <= 2.1 {
err := errors.New(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`)

Expand Down Expand Up @@ -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.mkdirMutexMap = make(map[string]*sync.Mutex, len(e.Taskfile.Tasks))
for k := range e.Taskfile.Tasks {
Expand All @@ -286,6 +294,17 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
release := e.acquireConcurrencyLimit()
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 {
return err
}
Expand Down Expand Up @@ -445,3 +464,25 @@ func getEnviron(t *taskfile.Task) []string {

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 changes: 11 additions & 0 deletions task_test.go
Expand Up @@ -979,3 +979,14 @@ func TestExitImmediately(t *testing.T) {
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`)
}

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)
}
3 changes: 3 additions & 0 deletions taskfile/task.go
Expand Up @@ -22,6 +22,7 @@ type Task struct {
Method string
Prefix string
IgnoreError bool
Run string
}

func (t *Task) Name() string {
Expand Down Expand Up @@ -61,6 +62,7 @@ func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error {
Method string
Prefix string
IgnoreError bool `yaml:"ignore_error"`
Run string
}
if err := unmarshal(&task); err != nil {
return err
Expand All @@ -81,5 +83,6 @@ func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error {
t.Method = task.Method
t.Prefix = task.Prefix
t.IgnoreError = task.IgnoreError
t.Run = task.Run
return nil
}
3 changes: 3 additions & 0 deletions taskfile/taskfile.go
Expand Up @@ -17,6 +17,7 @@ type Taskfile struct {
Tasks Tasks
Silent bool
Dotenv []string
Run string
}

// UnmarshalYAML implements yaml.Unmarshaler interface
Expand All @@ -32,6 +33,7 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
Tasks Tasks
Silent bool
Dotenv []string
Run string
}
if err := unmarshal(&taskfile); err != nil {
return err
Expand All @@ -46,6 +48,7 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
tf.Tasks = taskfile.Tasks
tf.Silent = taskfile.Silent
tf.Dotenv = taskfile.Dotenv
tf.Run = taskfile.Run
if tf.Expansions <= 0 {
tf.Expansions = 2
}
Expand Down
1 change: 1 addition & 0 deletions testdata/run/.gitignore
@@ -0,0 +1 @@
*.txt
24 changes: 24 additions & 0 deletions testdata/run/Taskfile.yml
@@ -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
1 change: 1 addition & 0 deletions variables.go
Expand Up @@ -59,6 +59,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
Method: r.Replace(origTask.Method),
Prefix: r.Replace(origTask.Prefix),
IgnoreError: origTask.IgnoreError,
Run: r.Replace(origTask.Run),
}
new.Dir, err = execext.Expand(new.Dir)
if err != nil {
Expand Down

0 comments on commit 97c85e3

Please sign in to comment.