Skip to content
Draft
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
42 changes: 42 additions & 0 deletions executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,48 @@ func TestPrecondition(t *testing.T) {
)
}

func TestGlobalPrecondition(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("global precondition met"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition"),
),
WithTask("passing"),
)
NewExecutorTest(t,
WithName("global precondition met - included task"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition"),
),
WithTask("inc:task"),
)
NewExecutorTest(t,
WithName("global precondition not met"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition_failing"),
),
WithTask("task"),
WithRunError(),
)
NewExecutorTest(t,
WithName("global precondition not met - included task"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition_failing"),
),
WithTask("inc:task"),
WithRunError(),
)
NewExecutorTest(t,
WithName("global precondition not met - task with own precondition"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition_failing"),
),
WithTask("task-with-own-precondition"),
WithRunError(),
)
}

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

Expand Down
8 changes: 8 additions & 0 deletions taskfile/ast/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ast
import (
"fmt"
"os"
"slices"
"sync"

"github.com/dominikbraun/graph"
Expand Down Expand Up @@ -116,5 +117,12 @@ func (tfg *TaskfileGraph) Merge() (*Taskfile, error) {
return nil, err
}

// Apply the root taskfile's global preconditions to all tasks.
if len(rootVertex.Taskfile.Preconditions) > 0 {
for task := range rootVertex.Taskfile.Tasks.Values(nil) {
task.Preconditions = slices.Concat(rootVertex.Taskfile.Preconditions, task.Preconditions)
}
}

return rootVertex.Taskfile, nil
}
59 changes: 31 additions & 28 deletions taskfile/ast/taskfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,21 @@ var ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles c

// 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
Tasks *Tasks
Silent bool
Dotenv []string
Run string
Interval time.Duration
Preconditions []*Precondition
}

// Merge merges the second Taskfile into the first
Expand Down Expand Up @@ -69,26 +70,27 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
}
t1.Vars.Merge(t2.Vars, include)
t1.Env.Merge(t2.Env, include)
return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
return t1.Tasks.Merge(t2.Tasks, include, t1.Vars, t2.Preconditions)
}

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
Set []string
Shopt []string
Vars *Vars
Env *Vars
Tasks *Tasks
Silent bool
Dotenv []string
Run string
Interval time.Duration
Preconditions []*Precondition
}
if err := node.Decode(&taskfile); err != nil {
return errors.NewTaskfileDecodeError(err, node)
Expand All @@ -106,6 +108,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 Down
4 changes: 3 additions & 1 deletion taskfile/ast/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,15 @@ func (t *Tasks) Values(sorter sort.Sorter) iter.Seq[*Task] {
}
}

func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars) error {
func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars, globalPreconditions []*Precondition) error {
defer t2.mutex.RUnlock()
t2.mutex.RLock()
for name, v := range t2.All(nil) {
// We do a deep copy of the task struct here to ensure that no data can
// be changed elsewhere once the taskfile is merged.
task := v.DeepCopy()
// Prepend the included taskfile's global preconditions to the task's own preconditions.
task.Preconditions = slices.Concat(globalPreconditions, task.Preconditions)
// Set the task to internal if EITHER the included task or the included
// taskfile are marked as internal
task.Internal = task.Internal || (include != nil && include.Internal)
Expand Down
17 changes: 17 additions & 0 deletions testdata/global_precondition/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: '3'

preconditions:
- sh: "[ 1 = 1 ]"
msg: "global precondition met"

includes:
inc: ./included

tasks:
passing:
cmds:
- echo "ran passing"

failing:
cmds:
- echo "ran failing"
6 changes: 6 additions & 0 deletions testdata/global_precondition/included/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: '3'

tasks:
task:
cmds:
- echo "ran included task"
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
task: [passing] echo "ran passing"
ran passing
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
task: [inc:task] echo "ran included task"
ran included task
20 changes: 20 additions & 0 deletions testdata/global_precondition_failing/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: '3'

preconditions:
- sh: "[ 1 = 0 ]"
msg: "global precondition failed"

includes:
inc: ./included

tasks:
task:
cmds:
- echo "should not run"

task-with-own-precondition:
preconditions:
- sh: "[ 1 = 1 ]"
msg: "own precondition"
cmds:
- echo "should not run either"
6 changes: 6 additions & 0 deletions testdata/global_precondition_failing/included/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: '3'

tasks:
task:
cmds:
- echo "should not run"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
task: Failed to run task "task": task: precondition not met
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
task: global precondition failed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
task: Failed to run task "inc:task": task: precondition not met
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
task: global precondition failed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
task: Failed to run task "task-with-own-precondition": task: precondition not met
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
task: global precondition failed
28 changes: 28 additions & 0 deletions website/src/docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,34 @@ tasks:
- echo "I will not run"
```

### Global preconditions

You can define preconditions at the Taskfile level that apply to every task,
including tasks from included Taskfiles. This is useful for enforcing
environment-wide requirements — such as ensuring a repository has been
initialized — without repeating the check on every task.

```yaml
version: '3'

preconditions:
- sh: test -f .env
msg: "Missing .env file. Run 'task init' to set up the project."

tasks:
build:
cmds:
- go build ./...

test:
cmds:
- go test ./...
```

Global preconditions are checked before the task's own preconditions. They are
also inherited by tasks in included Taskfiles — preconditions from a parent
Taskfile propagate downward through the include hierarchy.

### Conditional execution with `if`

The `if` attribute allows you to conditionally skip tasks or commands based on a
Expand Down
13 changes: 13 additions & 0 deletions website/src/docs/reference/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,19 @@ run: once
interval: 1s
```

### [`preconditions`](#precondition)

- **Type**: `[]Precondition`
- **Description**: Global preconditions that must be met before running any
task. These are prepended to each task's own preconditions and are inherited
by tasks in included Taskfiles.

```yaml
preconditions:
- sh: '[ "$CI" = "true" ]'
msg: "This Taskfile must be run in CI"
```

### `set`

- **Type**: `[]string`
Expand Down
7 changes: 7 additions & 0 deletions website/src/public/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,13 @@
"description": "Sets a different watch interval when using `--watch`, the default being 100 milliseconds. This string should be a valid Go duration: https://pkg.go.dev/time#ParseDuration.",
"type": "string",
"pattern": "^[0-9]+(?:m|s|ms)$"
},
"preconditions": {
"description": "Global preconditions that must be met before running any task. Prepended to each task's own preconditions and inherited by tasks in included Taskfiles.",
"type": "array",
"items": {
"$ref": "#/definitions/precondition"
}
}
},
"additionalProperties": false,
Expand Down