Skip to content

feat: add global precondition #1993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion precondition.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package task

import (
"context"
"slices"

"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env"
Expand All @@ -14,7 +15,7 @@ import (
var ErrPreconditionFailed = errors.New("task: precondition not met")

func (e *Executor) areTaskPreconditionsMet(ctx context.Context, t *ast.Task) (bool, error) {
for _, p := range t.Preconditions {
for _, p := range slices.Concat(e.Taskfile.Preconditions.Values, t.Preconditions) {
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: p.Sh,
Dir: t.Dir,
Expand Down
60 changes: 58 additions & 2 deletions task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,10 +456,10 @@ func TestStatus(t *testing.T) {
buff.Reset()
}

func TestPrecondition(t *testing.T) {
func TestPreconditionLocal(t *testing.T) {
t.Parallel()

const dir = "testdata/precondition"
const dir = "testdata/precondition/local"

var buff bytes.Buffer
e := &task.Executor{
Expand Down Expand Up @@ -499,6 +499,62 @@ func TestPrecondition(t *testing.T) {
buff.Reset()
}

func TestPreconditionGlobal(t *testing.T) {
t.Parallel()

var buff bytes.Buffer
e := &task.Executor{
Dir: "testdata/precondition/global",
Stdout: &buff,
Stderr: &buff,
}

require.NoError(t, e.Setup())

// A global precondition that was not met
require.Error(t, e.Run(context.Background(), &ast.Call{Task: "impossible"}))

if buff.String() != "task: 1 != 0 obviously!\n" {
t.Errorf("Wrong output message: %s", buff.String())
}
buff.Reset()

e = &task.Executor{
Dir: "testdata/precondition/global/with_local",
Stdout: &buff,
Stderr: &buff,
}

require.NoError(t, e.Setup())

// A global precondition that was met
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "foo"}))
if buff.String() != "" {
t.Errorf("Got Output when none was expected: %s", buff.String())
}

// A local precondition that was not met
require.Error(t, e.Run(context.Background(), &ast.Call{Task: "impossible"}))

if buff.String() != "task: 1 != 0 obviously!\n" {
t.Errorf("Wrong output message: %s", buff.String())
}

buff.Reset()

e = &task.Executor{
Dir: "testdata/precondition/global/included",
Stdout: &buff,
Stderr: &buff,
}

err := e.Setup()
require.Error(t, err)

assert.Equal(t, "task: Included Taskfiles can't have preconditions declarations. Please, move the preconditions declaration to the main Taskfile", err.Error())
buff.Reset()
}

func TestGenerates(t *testing.T) {
t.Parallel()

Expand Down
10 changes: 6 additions & 4 deletions taskfile/ast/precondition.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import (
)

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

func (p *Precondition) DeepCopy() *Precondition {
if p == nil {
Expand Down
47 changes: 47 additions & 0 deletions taskfile/ast/preconditions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package ast

import (
"sync"

"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"

"gopkg.in/yaml.v3"
)

// Precondition represents a precondition necessary for a task to run
type (
Preconditions struct {
Values []*Precondition
mutex sync.RWMutex
}
)

func NewPreconditions() *Preconditions {
return &Preconditions{
Values: make([]*Precondition, 0),
}
}

func (p *Preconditions) DeepCopy() *Preconditions {
if p == nil {
return nil
}
defer p.mutex.RUnlock()
p.mutex.RLock()
return &Preconditions{
Values: deepcopy.Slice(p.Values),
}
}

func (p *Preconditions) UnmarshalYAML(node *yaml.Node) error {
if p == nil || p.Values == nil {
*p = *NewPreconditions()
}

if err := node.Decode(&p.Values); err != nil {
return errors.NewTaskfileDecodeError(err, node).WithTypeMessage("preconditions")
}

return nil
}
69 changes: 42 additions & 27 deletions taskfile/ast/taskfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,26 @@ var V3 = semver.MustParse("3")
// ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs
var ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile")

// ErrIncludedTaskfilesCantHavePreconditions is returned when a included Taskfile contains Preconditions
var ErrIncludedTaskfilesCantHavePreconditions = errors.New("task: Included Taskfiles can't have preconditions declarations. Please, move the preconditions declaration to the main Taskfile")

// Taskfile is the abstract syntax tree for a Taskfile
type Taskfile struct {
Location string
Version *semver.Version
Output Output
Method string
Includes *Includes
Set []string
Shopt []string
Vars *Vars
Env *Vars
Tasks *Tasks
Silent bool
Dotenv []string
Run string
Interval time.Duration
Location string
Version *semver.Version
Output Output
Method string
Includes *Includes
Set []string
Shopt []string
Vars *Vars
Env *Vars
Preconditions *Preconditions
Tasks *Tasks
Silent bool
Dotenv []string
Run string
Interval time.Duration
}

// Merge merges the second Taskfile into the first
Expand All @@ -44,6 +48,9 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
if len(t2.Dotenv) > 0 {
return ErrIncludedTaskfilesCantHaveDotenvs
}
if len(t2.Preconditions.Values) > 0 {
return ErrIncludedTaskfilesCantHavePreconditions
}
if t2.Output.IsSet() {
t1.Output = t2.Output
}
Expand All @@ -59,6 +66,9 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
if t1.Tasks == nil {
t1.Tasks = NewTasks()
}
if t1.Preconditions == nil {
t1.Preconditions = NewPreconditions()
}
t1.Vars.Merge(t2.Vars, include)
t1.Env.Merge(t2.Env, include)
return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
Expand All @@ -68,19 +78,20 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.MappingNode:
var taskfile struct {
Version *semver.Version
Output Output
Method string
Includes *Includes
Set []string
Shopt []string
Vars *Vars
Env *Vars
Tasks *Tasks
Silent bool
Dotenv []string
Run string
Interval time.Duration
Version *semver.Version
Output Output
Method string
Includes *Includes
Preconditions *Preconditions
Set []string
Shopt []string
Vars *Vars
Env *Vars
Tasks *Tasks
Silent bool
Dotenv []string
Run string
Interval time.Duration
}
if err := node.Decode(&taskfile); err != nil {
return errors.NewTaskfileDecodeError(err, node)
Expand All @@ -98,6 +109,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
tf.Dotenv = taskfile.Dotenv
tf.Run = taskfile.Run
tf.Interval = taskfile.Interval
tf.Preconditions = taskfile.Preconditions
if tf.Includes == nil {
tf.Includes = NewIncludes()
}
Expand All @@ -110,6 +122,9 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
if tf.Tasks == nil {
tf.Tasks = NewTasks()
}
if tf.Preconditions == nil {
tf.Preconditions = NewPreconditions()
}
return nil
}

Expand Down
9 changes: 9 additions & 0 deletions testdata/precondition/global/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: '3'

preconditions:
- sh: "[ 1 = 0 ]"
msg: "1 != 0 obviously!"

tasks:
impossible:
cmd: echo "won't run"
8 changes: 8 additions & 0 deletions testdata/precondition/global/included/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: 3

includes:
included: included.yml

preconditions:
- sh: "[ 1 = 0 ]"
msg: "1 != 0 obviously!"
5 changes: 5 additions & 0 deletions testdata/precondition/global/included/included.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
version: 3

preconditions:
- sh: "[ 1 = 0 ]"
msg: "1 != 0 obviously!"
12 changes: 12 additions & 0 deletions testdata/precondition/global/with_local/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: '3'

preconditions:
- test -f foo.txt

tasks:
foo:

impossible:
preconditions:
- sh: "[ 1 = 0 ]"
msg: "1 != 0 obviously!"
Empty file.
22 changes: 22 additions & 0 deletions website/docs/usage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,28 @@ tasks:
- echo "I will not run"
```

They can be defined at two levels:

- Global Level: Applies to all tasks.
- Task Level: Applies only to a specific task.

```yaml
version: '3'

preconditions:
- sh: 'exit 1'

tasks:
task-will-fail: echo "I will not run"
```

:::info

Please note that you are not currently able to use the `preconditions` key inside
included Taskfiles. It'll produce an error.

:::

### Limiting when tasks run

If a task executed by multiple `cmds` or multiple `deps` you can control when it
Expand Down
7 changes: 7 additions & 0 deletions website/static/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,13 @@
"description": "A set of global environment variables.",
"$ref": "#/definitions/env"
},
"preconditions": {
"description": "A list of commands to check if any task should run. If a condition is not met, the task will return an error.",
"type": "array",
"items": {
"$ref": "#/definitions/precondition"
}
},
"tasks": {
"description": "A set of task definitions.",
"$ref": "#/definitions/tasks"
Expand Down
Loading