Skip to content

Commit

Permalink
Add Preconditions to Tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
stephenprater committed May 17, 2019
1 parent 6ff9ba9 commit d533aca
Show file tree
Hide file tree
Showing 14 changed files with 373 additions and 21 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ dist/

# intellij idea/goland
.idea/

# exuberant ctags
tags
15 changes: 15 additions & 0 deletions docs/taskfile_versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,21 @@ includes:
docker: ./DockerTasks.yml
```

## Version 2.3

Version 2.3 comes with `preconditions` stanza in tasks.

```yaml
version: '2'
tasks:
upload_environment:
preconditions:
- test -f .env
cmds:
- aws s3 cp .env s3://myenvironment
```

Please check the [documentation][includes]

[output]: usage.md#output-syntax
Expand Down
47 changes: 47 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,53 @@ up-to-date.
Also, `task --status [tasks]...` will exit with a non-zero exit code if any of
the tasks are not up-to-date.

If you need a certain set of conditions to be _true_ you can use the
`preconditions` stanza. `preconditions` are very similar to `status`
lines except they support `sh` expansion and they SHOULD all return 0

```yaml
version: '2'
tasks:
generate-files:
cmds:
- mkdir directory
- touch directory/file1.txt
- touch directory/file2.txt
# test existence of files
preconditions:
- test -f .env
- sh: "[ 1 = 0 ]"
msg: "One doesn't equal Zero, Halting"
```

Preconditions can set specific failure messages that can tell
a user what to do using the `msg` field.

If a task has a dependency on a sub-task with a precondition, and that
precondition is not met - the calling task will fail. Adding `ignore_errors`
to the precondition will cause parent tasks to execute even if the sub task
can not run. Note that a task executed directly with a failing precondition
will not run unless `--force` is given.

```yaml
version: '2'
tasks:
task_will_fail:
preconditions:
- sh: "exit 1"
ignore_errors: true
task_will_succeed:
deps:
- task_will_fail
task_will_succeed:
cmds:
- task: task_will_fail
- echo "I will run"
```

## Variables

When doing interpolation of variables, Task will look for the below.
Expand Down
51 changes: 51 additions & 0 deletions internal/taskfile/precondition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package taskfile

import (
"errors"
"fmt"
)

var (
// ErrCantUnmarshalPrecondition is returned for invalid precond YAML.
ErrCantUnmarshalPrecondition = errors.New("task: can't unmarshal precondition value")
)

// Precondition represents a precondition necessary for a task to run
type Precondition struct {
Sh string
Msg string
IgnoreError bool
}

// UnmarshalYAML implements yaml.Unmarshaler interface.
func (p *Precondition) UnmarshalYAML(unmarshal func(interface{}) error) error {
var cmd string

if err := unmarshal(&cmd); err == nil {
p.Sh = cmd
p.Msg = fmt.Sprintf("`%s` failed", cmd)
p.IgnoreError = false
return nil
}

var sh struct {
Sh string
Msg string
IgnoreError bool `yaml:"ignore_error"`
}

err := unmarshal(&sh)

if err == nil {
p.Sh = sh.Sh
p.Msg = sh.Msg
if p.Msg == "" {
p.Msg = fmt.Sprintf("%s failed", sh.Sh)
}

p.IgnoreError = sh.IgnoreError
return nil
}

return err
}
49 changes: 49 additions & 0 deletions internal/taskfile/precondition_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package taskfile_test

import (
"testing"

"github.com/go-task/task/v2/internal/taskfile"

"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
)

func TestPreconditionParse(t *testing.T) {
tests := []struct {
content string
v interface{}
expected interface{}
}{
{
"test -f foo.txt",
&taskfile.Precondition{},
&taskfile.Precondition{Sh: `test -f foo.txt`, Msg: "`test -f foo.txt` failed", IgnoreError: false},
},
{
"sh: '[ 1 = 0 ]'",
&taskfile.Precondition{},
&taskfile.Precondition{Sh: "[ 1 = 0 ]", Msg: "[ 1 = 0 ] failed", IgnoreError: false},
},
{`
sh: "[ 1 = 2 ]"
msg: "1 is not 2"
`,
&taskfile.Precondition{},
&taskfile.Precondition{Sh: "[ 1 = 2 ]", Msg: "1 is not 2", IgnoreError: false},
},
{`
sh: "[ 1 = 2 ]"
msg: "1 is not 2"
ignore_error: true
`,
&taskfile.Precondition{},
&taskfile.Precondition{Sh: "[ 1 = 2 ]", Msg: "1 is not 2", IgnoreError: true},
},
}
for _, test := range tests {
err := yaml.Unmarshal([]byte(test.content), test.v)
assert.NoError(t, err)
assert.Equal(t, test.expected, test.v)
}
}
31 changes: 16 additions & 15 deletions internal/taskfile/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@ type Tasks map[string]*Task

// Task represents a task
type Task struct {
Task string
Cmds []*Cmd
Deps []*Dep
Desc string
Summary string
Sources []string
Generates []string
Status []string
Dir string
Vars Vars
Env Vars
Silent bool
Method string
Prefix string
IgnoreError bool `yaml:"ignore_error"`
Task string
Cmds []*Cmd
Deps []*Dep
Desc string
Summary string
Sources []string
Generates []string
Status []string
Precondition []*Precondition
Dir string
Vars Vars
Env Vars
Silent bool
Method string
Prefix string
IgnoreError bool `yaml:"ignore_error"`
}
12 changes: 12 additions & 0 deletions internal/taskfile/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ var (
v21 = mustVersion("2.1")
v22 = mustVersion("2.2")
v23 = mustVersion("2.3")
v24 = mustVersion("2.4")
v25 = mustVersion("2.5")
)

// IsV1 returns if is a given Taskfile version is version 1
Expand Down Expand Up @@ -37,6 +39,16 @@ func IsV23(v *semver.Constraints) bool {
return v.Check(v23)
}

// IsV24 returns if is a given Taskfile version is at least version 2.4
func IsV24(v *semver.Constraints) bool {
return v.Check(v24)
}

// IsV25 returns if is a given Taskfile version is at least version 2.5
func IsV25(v *semver.Constraints) bool {
return v.Check(v25)
}

func mustVersion(s string) *semver.Version {
v, err := semver.NewVersion(s)
if err != nil {
Expand Down
42 changes: 42 additions & 0 deletions precondition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Package task provides ...
package task

import (
"context"
"errors"

"github.com/go-task/task/v2/internal/execext"
"github.com/go-task/task/v2/internal/taskfile"
)

var (
// ErrPreconditionFailed is returned when a precondition fails
ErrNecessaryPreconditionFailed = errors.New("task: precondition not met")
ErrOptionalPreconditionFailed = errors.New("task: optional precondition not met")
)

func (e *Executor) areTaskPreconditionsMet(ctx context.Context, t *taskfile.Task) (bool, error) {
var optionalPreconditionFailed bool
for _, p := range t.Precondition {
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: p.Sh,
Dir: t.Dir,
Env: getEnviron(t),
})

if err != nil {
e.Logger.Outf(p.Msg)
if p.IgnoreError == true {
optionalPreconditionFailed = true
} else {
return false, ErrNecessaryPreconditionFailed
}
}
}

if optionalPreconditionFailed == true {
return true, ErrOptionalPreconditionFailed
}

return true, nil
}
2 changes: 2 additions & 0 deletions status.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ func (e *Executor) isTaskUpToDateStatus(ctx context.Context, t *taskfile.Task) (
Env: getEnviron(t),
})
if err != nil {
e.Logger.VerboseOutf("task: status command %s exited non-zero: %s", s, err)
return false, nil
}
e.Logger.VerboseOutf("task: status command %s exited zero", s)
}
return true, nil
}
35 changes: 29 additions & 6 deletions task.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,17 @@ func (e *Executor) Setup() error {
Vars: e.taskvars,
Logger: e.Logger,
}
case version.IsV2(v), version.IsV21(v), version.IsV22(v):
case version.IsV2(v), version.IsV21(v), version.IsV22(v), version.IsV23(v):
e.Compiler = &compilerv2.CompilerV2{
Dir: e.Dir,
Taskvars: e.taskvars,
TaskfileVars: e.Taskfile.Vars,
Expansions: e.Taskfile.Expansions,
Logger: e.Logger,
}
case version.IsV23(v):
return fmt.Errorf(`task: Taskfile versions greater than v2.3 not implemented in the version of Task`)

case version.IsV24(v):
return fmt.Errorf(`task: Taskfile versions greater than v2.4 not implemented in the version of Task`)
}

if !version.IsV21(v) && e.Taskfile.Output != "" {
Expand Down Expand Up @@ -192,7 +193,13 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
if err != nil {
return err
}
if upToDate {

preCondMet, err := e.areTaskPreconditionsMet(ctx, t)
if err != nil {
return err
}

if upToDate && preCondMet {
if !e.Silent {
e.Logger.Errf(`task: Task "%s" is up to date`, t.Task)
}
Expand Down Expand Up @@ -224,7 +231,15 @@ func (e *Executor) runDeps(ctx context.Context, t *taskfile.Task) error {
d := d

g.Go(func() error {
return e.RunTask(ctx, taskfile.Call{Task: d.Task, Vars: d.Vars})
err := e.RunTask(ctx, taskfile.Call{Task: d.Task, Vars: d.Vars})
if err != nil {
if err == ErrOptionalPreconditionFailed {
e.Logger.Errf("%s", err)
} else {
return err
}
}
return nil
})
}

Expand All @@ -236,7 +251,15 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi

switch {
case cmd.Task != "":
return e.RunTask(ctx, taskfile.Call{Task: cmd.Task, Vars: cmd.Vars})
err := e.RunTask(ctx, taskfile.Call{Task: cmd.Task, Vars: cmd.Vars})
if err != nil {
if err == ErrOptionalPreconditionFailed {
e.Logger.Errf("%s", err)
} else {
return err
}
}
return nil
case cmd.Cmd != "":
if e.Verbose || (!cmd.Silent && !t.Silent && !e.Silent) {
e.Logger.Errf(cmd.Cmd)
Expand Down
Loading

0 comments on commit d533aca

Please sign in to comment.