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
124 changes: 124 additions & 0 deletions executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,130 @@ 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 unaffected without inherit"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition_failing"),
),
WithTask("inc:task"),
)
NewExecutorTest(t,
WithName("global precondition not met - task with own precondition"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition_failing"),
),
WithTask("task-with-own-precondition"),
WithRunError(),
)
// inherit: true
NewExecutorTest(t,
WithName("inherited precondition not met - root task fails"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition_inherit"),
),
WithTask("task"),
WithRunError(),
)
NewExecutorTest(t,
WithName("inherited precondition not met - included task fails"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition_inherit"),
),
WithTask("inc:task"),
WithRunError(),
)
NewExecutorTest(t,
WithName("skip_preconditions skips inherited precondition"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition_inherit"),
),
WithTask("inc:skip-task"),
)
NewExecutorTest(t,
WithName("skip_preconditions does not skip own file precondition"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition_inherit"),
),
WithTask("own:skip-task"),
WithRunError(),
)
// transitive inherit: root inherit:true reaches two levels deep
NewExecutorTest(t,
WithName("transitive inherit blocks direct include"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition_transitive"),
),
WithTask("mid:task"),
WithRunError(),
)
NewExecutorTest(t,
WithName("transitive inherit blocks two levels deep"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition_transitive"),
),
WithTask("mid:deep:task"),
WithRunError(),
)
// skip_preconditions on a root task: own-file preconditions still apply
NewExecutorTest(t,
WithName("skip_preconditions is a no-op on root tasks"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition_transitive"),
),
WithTask("skip-task"),
WithRunError(),
)
// mid-level inherit: an included file's inherit:true reaches its sub-includes
// but does NOT affect root tasks
NewExecutorTest(t,
WithName("mid-level inherit does not affect root tasks"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition_mid_inherit"),
),
WithTask("task"),
)
NewExecutorTest(t,
WithName("mid-level inherit blocks own tasks"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition_mid_inherit"),
),
WithTask("mid:task"),
WithRunError(),
)
NewExecutorTest(t,
WithName("mid-level inherit blocks sub-includes"),
WithExecutorOptions(
task.WithDir("testdata/global_precondition_mid_inherit"),
),
WithTask("mid:deep:task"),
WithRunError(),
)
}

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

Expand Down
21 changes: 21 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,25 @@ func (tfg *TaskfileGraph) Merge() (*Taskfile, error) {
return nil, err
}

// Apply the root taskfile's global preconditions to all tasks.
// Root's own tasks (Namespace == "") always receive all root preconditions.
// Tasks from included files only receive preconditions marked inherit:true,
// and only when the task has not opted out via skip_preconditions.
if len(rootVertex.Taskfile.Preconditions) > 0 {
var inherited []*Precondition
for _, p := range rootVertex.Taskfile.Preconditions {
if p.Inherit {
inherited = append(inherited, p)
}
}
for task := range rootVertex.Taskfile.Tasks.Values(nil) {
if task.Namespace == "" {
task.Preconditions = slices.Concat(rootVertex.Taskfile.Preconditions, task.Preconditions)
} else if !task.SkipPreconditions && len(inherited) > 0 {
task.Preconditions = slices.Concat(inherited, task.Preconditions)
}
}
}

return rootVertex.Taskfile, nil
}
16 changes: 10 additions & 6 deletions taskfile/ast/precondition.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,19 @@ import (

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

func (p *Precondition) DeepCopy() *Precondition {
if p == nil {
return nil
}
return &Precondition{
Sh: p.Sh,
Msg: p.Msg,
Sh: p.Sh,
Msg: p.Msg,
Inherit: p.Inherit,
}
}

Expand All @@ -39,14 +41,16 @@ func (p *Precondition) UnmarshalYAML(node *yaml.Node) error {

case yaml.MappingNode:
var sh struct {
Sh string
Msg string
Sh string
Msg string
Inherit bool
}
if err := node.Decode(&sh); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
p.Sh = sh.Sh
p.Msg = sh.Msg
p.Inherit = sh.Inherit
if p.Msg == "" {
p.Msg = fmt.Sprintf("%s failed", sh.Sh)
}
Expand Down
126 changes: 65 additions & 61 deletions taskfile/ast/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,38 @@ import (

// Task represents a task
type Task struct {
Task string `hash:"ignore"`
Cmds []*Cmd
Deps []*Dep
Label string
Desc string
Prompt Prompt
Summary string
Requires *Requires
Aliases []string
Sources []*Glob
Generates []*Glob
Status []string
Preconditions []*Precondition
Dir string
Set []string
Shopt []string
Vars *Vars
Env *Vars
Dotenv []string
Silent *bool
Interactive bool
Internal bool
Method string
Prefix string `hash:"ignore"`
IgnoreError bool
Run string
Platforms []*Platform
If string
Watch bool
Location *Location
Failfast bool
Task string `hash:"ignore"`
Cmds []*Cmd
Deps []*Dep
Label string
Desc string
Prompt Prompt
Summary string
Requires *Requires
Aliases []string
Sources []*Glob
Generates []*Glob
Status []string
Preconditions []*Precondition
Dir string
Set []string
Shopt []string
Vars *Vars
Env *Vars
Dotenv []string
Silent *bool
Interactive bool
Internal bool
Method string
Prefix string `hash:"ignore"`
IgnoreError bool
SkipPreconditions bool `yaml:"skip_preconditions"`
Run string
Platforms []*Platform
If string
Watch bool
Location *Location
Failfast bool
// Populated during merging
Namespace string `hash:"ignore"`
IncludeVars *Vars
Expand Down Expand Up @@ -126,36 +127,37 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
// Full task object
case yaml.MappingNode:
var task struct {
Cmds []*Cmd
Cmd *Cmd
Deps []*Dep
Label string
Desc string
Prompt Prompt
Summary string
Aliases []string
Sources []*Glob
Generates []*Glob
Status []string
Preconditions []*Precondition
Dir string
Set []string
Shopt []string
Vars *Vars
Env *Vars
Dotenv []string
Silent *bool `yaml:"silent,omitempty"`
Interactive bool
Internal bool
Method string
Prefix string
IgnoreError bool `yaml:"ignore_error"`
Run string
Platforms []*Platform
If string
Requires *Requires
Watch bool
Failfast bool
Cmds []*Cmd
Cmd *Cmd
Deps []*Dep
Label string
Desc string
Prompt Prompt
Summary string
Aliases []string
Sources []*Glob
Generates []*Glob
Status []string
Preconditions []*Precondition
Dir string
Set []string
Shopt []string
Vars *Vars
Env *Vars
Dotenv []string
Silent *bool `yaml:"silent,omitempty"`
Interactive bool
Internal bool
Method string
Prefix string
IgnoreError bool `yaml:"ignore_error"`
SkipPreconditions bool `yaml:"skip_preconditions"`
Run string
Platforms []*Platform
If string
Requires *Requires
Watch bool
Failfast bool
}
if err := node.Decode(&task); err != nil {
return errors.NewTaskfileDecodeError(err, node)
Expand Down Expand Up @@ -190,6 +192,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
t.Method = task.Method
t.Prefix = task.Prefix
t.IgnoreError = task.IgnoreError
t.SkipPreconditions = task.SkipPreconditions
t.Run = task.Run
t.Platforms = task.Platforms
t.If = task.If
Expand Down Expand Up @@ -233,6 +236,7 @@ func (t *Task) DeepCopy() *Task {
Method: t.Method,
Prefix: t.Prefix,
IgnoreError: t.IgnoreError,
SkipPreconditions: t.SkipPreconditions,
Run: t.Run,
IncludeVars: t.IncludeVars.DeepCopy(),
IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(),
Expand Down
Loading