diff --git a/executor_test.go b/executor_test.go index 6e3ff3e1ec..65bac549d6 100644 --- a/executor_test.go +++ b/executor_test.go @@ -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() diff --git a/taskfile/ast/graph.go b/taskfile/ast/graph.go index cb30093d88..30a2082fda 100644 --- a/taskfile/ast/graph.go +++ b/taskfile/ast/graph.go @@ -3,6 +3,7 @@ package ast import ( "fmt" "os" + "slices" "sync" "github.com/dominikbraun/graph" @@ -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 } diff --git a/taskfile/ast/precondition.go b/taskfile/ast/precondition.go index ea6682bb2a..546529166a 100644 --- a/taskfile/ast/precondition.go +++ b/taskfile/ast/precondition.go @@ -10,8 +10,9 @@ 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 { @@ -19,8 +20,9 @@ func (p *Precondition) DeepCopy() *Precondition { return nil } return &Precondition{ - Sh: p.Sh, - Msg: p.Msg, + Sh: p.Sh, + Msg: p.Msg, + Inherit: p.Inherit, } } @@ -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) } diff --git a/taskfile/ast/task.go b/taskfile/ast/task.go index 0e9893943e..877cca164f 100644 --- a/taskfile/ast/task.go +++ b/taskfile/ast/task.go @@ -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 @@ -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) @@ -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 @@ -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(), diff --git a/taskfile/ast/taskfile.go b/taskfile/ast/taskfile.go index 4e3a3e4255..c49d725aac 100644 --- a/taskfile/ast/taskfile.go +++ b/taskfile/ast/taskfile.go @@ -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 @@ -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) @@ -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() } diff --git a/taskfile/ast/tasks.go b/taskfile/ast/tasks.go index 62aa53a6b4..188114c3b5 100644 --- a/taskfile/ast/tasks.go +++ b/taskfile/ast/tasks.go @@ -118,13 +118,29 @@ 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() + // Inject global preconditions from the included taskfile. + // Tasks native to t2 (Namespace == "") always receive all of t2's global + // preconditions. Tasks from t2's own transitive includes (Namespace != "") + // only receive preconditions marked inherit:true, and only when the task + // has not opted out via skip_preconditions. + if task.Namespace == "" { + task.Preconditions = slices.Concat(globalPreconditions, task.Preconditions) + } else if !task.SkipPreconditions { + var inherited []*Precondition + for _, p := range globalPreconditions { + if p.Inherit { + inherited = append(inherited, p) + } + } + task.Preconditions = slices.Concat(inherited, 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) diff --git a/testdata/global_precondition/Taskfile.yml b/testdata/global_precondition/Taskfile.yml new file mode 100644 index 0000000000..51768ad10a --- /dev/null +++ b/testdata/global_precondition/Taskfile.yml @@ -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" diff --git a/testdata/global_precondition/included/Taskfile.yml b/testdata/global_precondition/included/Taskfile.yml new file mode 100644 index 0000000000..2d3b6099a0 --- /dev/null +++ b/testdata/global_precondition/included/Taskfile.yml @@ -0,0 +1,6 @@ +version: '3' + +tasks: + task: + cmds: + - echo "ran included task" diff --git a/testdata/global_precondition/testdata/TestGlobalPrecondition-global_precondition_met.golden b/testdata/global_precondition/testdata/TestGlobalPrecondition-global_precondition_met.golden new file mode 100644 index 0000000000..34ba3ec32e --- /dev/null +++ b/testdata/global_precondition/testdata/TestGlobalPrecondition-global_precondition_met.golden @@ -0,0 +1,2 @@ +task: [passing] echo "ran passing" +ran passing diff --git a/testdata/global_precondition/testdata/TestGlobalPrecondition-global_precondition_met_-_included_task.golden b/testdata/global_precondition/testdata/TestGlobalPrecondition-global_precondition_met_-_included_task.golden new file mode 100644 index 0000000000..ba1720bf6c --- /dev/null +++ b/testdata/global_precondition/testdata/TestGlobalPrecondition-global_precondition_met_-_included_task.golden @@ -0,0 +1,2 @@ +task: [inc:task] echo "ran included task" +ran included task diff --git a/testdata/global_precondition_failing/Taskfile.yml b/testdata/global_precondition_failing/Taskfile.yml new file mode 100644 index 0000000000..73ab1a9ef9 --- /dev/null +++ b/testdata/global_precondition_failing/Taskfile.yml @@ -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" diff --git a/testdata/global_precondition_failing/included/Taskfile.yml b/testdata/global_precondition_failing/included/Taskfile.yml new file mode 100644 index 0000000000..9d0dcf14e6 --- /dev/null +++ b/testdata/global_precondition_failing/included/Taskfile.yml @@ -0,0 +1,6 @@ +version: '3' + +tasks: + task: + cmds: + - echo "should not run" diff --git a/testdata/global_precondition_failing/testdata/TestGlobalPrecondition-global_precondition_not_met-err-run.golden b/testdata/global_precondition_failing/testdata/TestGlobalPrecondition-global_precondition_not_met-err-run.golden new file mode 100644 index 0000000000..bbfbd18c53 --- /dev/null +++ b/testdata/global_precondition_failing/testdata/TestGlobalPrecondition-global_precondition_not_met-err-run.golden @@ -0,0 +1 @@ +task: Failed to run task "task": task: precondition not met \ No newline at end of file diff --git a/testdata/global_precondition_failing/testdata/TestGlobalPrecondition-global_precondition_not_met.golden b/testdata/global_precondition_failing/testdata/TestGlobalPrecondition-global_precondition_not_met.golden new file mode 100644 index 0000000000..259c7301cf --- /dev/null +++ b/testdata/global_precondition_failing/testdata/TestGlobalPrecondition-global_precondition_not_met.golden @@ -0,0 +1 @@ +task: global precondition failed diff --git a/testdata/global_precondition_failing/testdata/TestGlobalPrecondition-global_precondition_not_met_-_included_task_unaffected_without_inherit.golden b/testdata/global_precondition_failing/testdata/TestGlobalPrecondition-global_precondition_not_met_-_included_task_unaffected_without_inherit.golden new file mode 100644 index 0000000000..241ad20710 --- /dev/null +++ b/testdata/global_precondition_failing/testdata/TestGlobalPrecondition-global_precondition_not_met_-_included_task_unaffected_without_inherit.golden @@ -0,0 +1,2 @@ +task: [inc:task] echo "should not run" +should not run diff --git a/testdata/global_precondition_failing/testdata/TestGlobalPrecondition-global_precondition_not_met_-_task_with_own_precondition-err-run.golden b/testdata/global_precondition_failing/testdata/TestGlobalPrecondition-global_precondition_not_met_-_task_with_own_precondition-err-run.golden new file mode 100644 index 0000000000..bd7b34a852 --- /dev/null +++ b/testdata/global_precondition_failing/testdata/TestGlobalPrecondition-global_precondition_not_met_-_task_with_own_precondition-err-run.golden @@ -0,0 +1 @@ +task: Failed to run task "task-with-own-precondition": task: precondition not met \ No newline at end of file diff --git a/testdata/global_precondition_failing/testdata/TestGlobalPrecondition-global_precondition_not_met_-_task_with_own_precondition.golden b/testdata/global_precondition_failing/testdata/TestGlobalPrecondition-global_precondition_not_met_-_task_with_own_precondition.golden new file mode 100644 index 0000000000..259c7301cf --- /dev/null +++ b/testdata/global_precondition_failing/testdata/TestGlobalPrecondition-global_precondition_not_met_-_task_with_own_precondition.golden @@ -0,0 +1 @@ +task: global precondition failed diff --git a/testdata/global_precondition_inherit/Taskfile.yml b/testdata/global_precondition_inherit/Taskfile.yml new file mode 100644 index 0000000000..806463652f --- /dev/null +++ b/testdata/global_precondition_inherit/Taskfile.yml @@ -0,0 +1,15 @@ +version: '3' + +preconditions: + - sh: "[ 1 = 0 ]" + msg: "inherited precondition failed" + inherit: true + +includes: + inc: ./included + own: ./included_with_own + +tasks: + task: + cmds: + - echo "should not run" diff --git a/testdata/global_precondition_inherit/included/Taskfile.yml b/testdata/global_precondition_inherit/included/Taskfile.yml new file mode 100644 index 0000000000..3f17a12430 --- /dev/null +++ b/testdata/global_precondition_inherit/included/Taskfile.yml @@ -0,0 +1,11 @@ +version: '3' + +tasks: + task: + cmds: + - echo "should not run - blocked by inherited precondition" + + skip-task: + skip_preconditions: true + cmds: + - echo "ran skip-task" diff --git a/testdata/global_precondition_inherit/included_with_own/Taskfile.yml b/testdata/global_precondition_inherit/included_with_own/Taskfile.yml new file mode 100644 index 0000000000..1d17c96b86 --- /dev/null +++ b/testdata/global_precondition_inherit/included_with_own/Taskfile.yml @@ -0,0 +1,11 @@ +version: '3' + +preconditions: + - sh: "[ 1 = 0 ]" + msg: "own file precondition failed" + +tasks: + skip-task: + skip_preconditions: true + cmds: + - echo "should not run - own file precondition still applies" diff --git a/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-inherited_precondition_not_met_-_included_task_fails-err-run.golden b/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-inherited_precondition_not_met_-_included_task_fails-err-run.golden new file mode 100644 index 0000000000..06be420d05 --- /dev/null +++ b/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-inherited_precondition_not_met_-_included_task_fails-err-run.golden @@ -0,0 +1 @@ +task: Failed to run task "inc:task": task: precondition not met \ No newline at end of file diff --git a/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-inherited_precondition_not_met_-_included_task_fails.golden b/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-inherited_precondition_not_met_-_included_task_fails.golden new file mode 100644 index 0000000000..4b259f3491 --- /dev/null +++ b/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-inherited_precondition_not_met_-_included_task_fails.golden @@ -0,0 +1 @@ +task: inherited precondition failed diff --git a/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-inherited_precondition_not_met_-_root_task_fails-err-run.golden b/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-inherited_precondition_not_met_-_root_task_fails-err-run.golden new file mode 100644 index 0000000000..bbfbd18c53 --- /dev/null +++ b/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-inherited_precondition_not_met_-_root_task_fails-err-run.golden @@ -0,0 +1 @@ +task: Failed to run task "task": task: precondition not met \ No newline at end of file diff --git a/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-inherited_precondition_not_met_-_root_task_fails.golden b/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-inherited_precondition_not_met_-_root_task_fails.golden new file mode 100644 index 0000000000..4b259f3491 --- /dev/null +++ b/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-inherited_precondition_not_met_-_root_task_fails.golden @@ -0,0 +1 @@ +task: inherited precondition failed diff --git a/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-skip_preconditions_does_not_skip_own_file_precondition-err-run.golden b/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-skip_preconditions_does_not_skip_own_file_precondition-err-run.golden new file mode 100644 index 0000000000..3349fd042d --- /dev/null +++ b/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-skip_preconditions_does_not_skip_own_file_precondition-err-run.golden @@ -0,0 +1 @@ +task: Failed to run task "own:skip-task": task: precondition not met \ No newline at end of file diff --git a/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-skip_preconditions_does_not_skip_own_file_precondition.golden b/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-skip_preconditions_does_not_skip_own_file_precondition.golden new file mode 100644 index 0000000000..9602c3f44c --- /dev/null +++ b/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-skip_preconditions_does_not_skip_own_file_precondition.golden @@ -0,0 +1 @@ +task: own file precondition failed diff --git a/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-skip_preconditions_skips_inherited_precondition.golden b/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-skip_preconditions_skips_inherited_precondition.golden new file mode 100644 index 0000000000..cd65d47cea --- /dev/null +++ b/testdata/global_precondition_inherit/testdata/TestGlobalPrecondition-skip_preconditions_skips_inherited_precondition.golden @@ -0,0 +1,2 @@ +task: [inc:skip-task] echo "ran skip-task" +ran skip-task diff --git a/testdata/global_precondition_mid_inherit/Taskfile.yml b/testdata/global_precondition_mid_inherit/Taskfile.yml new file mode 100644 index 0000000000..22577d8152 --- /dev/null +++ b/testdata/global_precondition_mid_inherit/Taskfile.yml @@ -0,0 +1,9 @@ +version: '3' + +includes: + mid: ./mid + +tasks: + task: + cmds: + - echo "ran root task" diff --git a/testdata/global_precondition_mid_inherit/mid/Taskfile.yml b/testdata/global_precondition_mid_inherit/mid/Taskfile.yml new file mode 100644 index 0000000000..3da3052867 --- /dev/null +++ b/testdata/global_precondition_mid_inherit/mid/Taskfile.yml @@ -0,0 +1,14 @@ +version: '3' + +preconditions: + - sh: "[ 1 = 0 ]" + msg: "mid precondition failed" + inherit: true + +includes: + deep: ./deep + +tasks: + task: + cmds: + - echo "should not run" diff --git a/testdata/global_precondition_mid_inherit/mid/deep/Taskfile.yml b/testdata/global_precondition_mid_inherit/mid/deep/Taskfile.yml new file mode 100644 index 0000000000..9d0dcf14e6 --- /dev/null +++ b/testdata/global_precondition_mid_inherit/mid/deep/Taskfile.yml @@ -0,0 +1,6 @@ +version: '3' + +tasks: + task: + cmds: + - echo "should not run" diff --git a/testdata/global_precondition_mid_inherit/testdata/TestGlobalPrecondition-mid-level_inherit_blocks_own_tasks-err-run.golden b/testdata/global_precondition_mid_inherit/testdata/TestGlobalPrecondition-mid-level_inherit_blocks_own_tasks-err-run.golden new file mode 100644 index 0000000000..7fa18ee0ed --- /dev/null +++ b/testdata/global_precondition_mid_inherit/testdata/TestGlobalPrecondition-mid-level_inherit_blocks_own_tasks-err-run.golden @@ -0,0 +1 @@ +task: Failed to run task "mid:task": task: precondition not met \ No newline at end of file diff --git a/testdata/global_precondition_mid_inherit/testdata/TestGlobalPrecondition-mid-level_inherit_blocks_own_tasks.golden b/testdata/global_precondition_mid_inherit/testdata/TestGlobalPrecondition-mid-level_inherit_blocks_own_tasks.golden new file mode 100644 index 0000000000..6e1e22af04 --- /dev/null +++ b/testdata/global_precondition_mid_inherit/testdata/TestGlobalPrecondition-mid-level_inherit_blocks_own_tasks.golden @@ -0,0 +1 @@ +task: mid precondition failed diff --git a/testdata/global_precondition_mid_inherit/testdata/TestGlobalPrecondition-mid-level_inherit_blocks_sub-includes-err-run.golden b/testdata/global_precondition_mid_inherit/testdata/TestGlobalPrecondition-mid-level_inherit_blocks_sub-includes-err-run.golden new file mode 100644 index 0000000000..2472e56e99 --- /dev/null +++ b/testdata/global_precondition_mid_inherit/testdata/TestGlobalPrecondition-mid-level_inherit_blocks_sub-includes-err-run.golden @@ -0,0 +1 @@ +task: Failed to run task "mid:deep:task": task: precondition not met \ No newline at end of file diff --git a/testdata/global_precondition_mid_inherit/testdata/TestGlobalPrecondition-mid-level_inherit_blocks_sub-includes.golden b/testdata/global_precondition_mid_inherit/testdata/TestGlobalPrecondition-mid-level_inherit_blocks_sub-includes.golden new file mode 100644 index 0000000000..6e1e22af04 --- /dev/null +++ b/testdata/global_precondition_mid_inherit/testdata/TestGlobalPrecondition-mid-level_inherit_blocks_sub-includes.golden @@ -0,0 +1 @@ +task: mid precondition failed diff --git a/testdata/global_precondition_mid_inherit/testdata/TestGlobalPrecondition-mid-level_inherit_does_not_affect_root_tasks.golden b/testdata/global_precondition_mid_inherit/testdata/TestGlobalPrecondition-mid-level_inherit_does_not_affect_root_tasks.golden new file mode 100644 index 0000000000..7efb6d3b70 --- /dev/null +++ b/testdata/global_precondition_mid_inherit/testdata/TestGlobalPrecondition-mid-level_inherit_does_not_affect_root_tasks.golden @@ -0,0 +1,2 @@ +task: [task] echo "ran root task" +ran root task diff --git a/testdata/global_precondition_transitive/Taskfile.yml b/testdata/global_precondition_transitive/Taskfile.yml new file mode 100644 index 0000000000..957ef99631 --- /dev/null +++ b/testdata/global_precondition_transitive/Taskfile.yml @@ -0,0 +1,19 @@ +version: '3' + +preconditions: + - sh: "[ 1 = 0 ]" + msg: "root precondition failed" + inherit: true + +includes: + mid: ./mid + +tasks: + task: + cmds: + - echo "should not run" + + skip-task: + skip_preconditions: true + cmds: + - echo "should not run - skip_preconditions has no effect on root tasks" diff --git a/testdata/global_precondition_transitive/mid/Taskfile.yml b/testdata/global_precondition_transitive/mid/Taskfile.yml new file mode 100644 index 0000000000..2c3d746bd7 --- /dev/null +++ b/testdata/global_precondition_transitive/mid/Taskfile.yml @@ -0,0 +1,9 @@ +version: '3' + +includes: + deep: ./deep + +tasks: + task: + cmds: + - echo "should not run" diff --git a/testdata/global_precondition_transitive/mid/deep/Taskfile.yml b/testdata/global_precondition_transitive/mid/deep/Taskfile.yml new file mode 100644 index 0000000000..9d0dcf14e6 --- /dev/null +++ b/testdata/global_precondition_transitive/mid/deep/Taskfile.yml @@ -0,0 +1,6 @@ +version: '3' + +tasks: + task: + cmds: + - echo "should not run" diff --git a/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-skip_preconditions_is_a_no-op_on_root_tasks-err-run.golden b/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-skip_preconditions_is_a_no-op_on_root_tasks-err-run.golden new file mode 100644 index 0000000000..92c9a4bb69 --- /dev/null +++ b/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-skip_preconditions_is_a_no-op_on_root_tasks-err-run.golden @@ -0,0 +1 @@ +task: Failed to run task "skip-task": task: precondition not met \ No newline at end of file diff --git a/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-skip_preconditions_is_a_no-op_on_root_tasks.golden b/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-skip_preconditions_is_a_no-op_on_root_tasks.golden new file mode 100644 index 0000000000..0904701bcb --- /dev/null +++ b/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-skip_preconditions_is_a_no-op_on_root_tasks.golden @@ -0,0 +1 @@ +task: root precondition failed diff --git a/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-transitive_inherit_blocks_direct_include-err-run.golden b/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-transitive_inherit_blocks_direct_include-err-run.golden new file mode 100644 index 0000000000..7fa18ee0ed --- /dev/null +++ b/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-transitive_inherit_blocks_direct_include-err-run.golden @@ -0,0 +1 @@ +task: Failed to run task "mid:task": task: precondition not met \ No newline at end of file diff --git a/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-transitive_inherit_blocks_direct_include.golden b/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-transitive_inherit_blocks_direct_include.golden new file mode 100644 index 0000000000..0904701bcb --- /dev/null +++ b/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-transitive_inherit_blocks_direct_include.golden @@ -0,0 +1 @@ +task: root precondition failed diff --git a/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-transitive_inherit_blocks_two_levels_deep-err-run.golden b/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-transitive_inherit_blocks_two_levels_deep-err-run.golden new file mode 100644 index 0000000000..2472e56e99 --- /dev/null +++ b/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-transitive_inherit_blocks_two_levels_deep-err-run.golden @@ -0,0 +1 @@ +task: Failed to run task "mid:deep:task": task: precondition not met \ No newline at end of file diff --git a/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-transitive_inherit_blocks_two_levels_deep.golden b/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-transitive_inherit_blocks_two_levels_deep.golden new file mode 100644 index 0000000000..0904701bcb --- /dev/null +++ b/testdata/global_precondition_transitive/testdata/TestGlobalPrecondition-transitive_inherit_blocks_two_levels_deep.golden @@ -0,0 +1 @@ +task: root precondition failed diff --git a/website/src/docs/guide.md b/website/src/docs/guide.md index 6c3eb912bf..f37e237a52 100644 --- a/website/src/docs/guide.md +++ b/website/src/docs/guide.md @@ -1020,6 +1020,70 @@ tasks: - echo "I will not run" ``` +### Global preconditions + +You can define preconditions at the Taskfile level that apply to every task in +that file. This is useful for enforcing file-wide requirements 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. + +#### Inheriting preconditions into included Taskfiles + +By default, global preconditions are scoped to the file they are defined in and +do not affect tasks in included Taskfiles. To propagate a precondition into +included files, set `inherit: true`: + +```yaml +version: '3' + +preconditions: + - sh: test -f .env + msg: "Missing .env file. Run 'task init' to set up the project." + inherit: true # also applies to tasks in included Taskfiles + +includes: + tools: ./tools +``` + +Inherited preconditions propagate transitively through the include hierarchy. + +#### Skipping inherited preconditions + +A task can opt out of inherited preconditions with `skip_preconditions: true`. +This only skips preconditions inherited from parent Taskfiles — global +preconditions defined in the same file as the task still apply. + +```yaml +# tools/Taskfile.yml +preconditions: + - sh: test -f tools.lock + msg: "Run 'task tools:init' first" + +tasks: + init: + skip_preconditions: true # skips parent's inherited preconditions + cmds: # but tools.lock check still applies + - touch tools.lock +``` + ### Conditional execution with `if` The `if` attribute allows you to conditionally skip tasks or commands based on a diff --git a/website/src/docs/reference/schema.md b/website/src/docs/reference/schema.md index f70f336bcf..7800d2e69c 100644 --- a/website/src/docs/reference/schema.md +++ b/website/src/docs/reference/schema.md @@ -187,6 +187,22 @@ run: once interval: 1s ``` +### [`preconditions`](#precondition) + +- **Type**: `[]Precondition` +- **Description**: Global preconditions that must be met before running any task + in this file. Prepended to each task's own preconditions. Preconditions with + `inherit: true` also apply to tasks in included Taskfiles. + +```yaml +preconditions: + - sh: '[ "$USER" != "root" ]' + msg: "Don't run as root" + inherit: true + - sh: test -f .env + msg: "Run 'task init' first" +``` + ### `set` - **Type**: `[]string` @@ -698,6 +714,22 @@ tasks: See [Prompting for missing variables interactively](/docs/guide#prompting-for-missing-variables-interactively) for information on enabling interactive prompts for missing required variables. +#### `skip_preconditions` + +- **Type**: `bool` +- **Default**: `false` +- **Description**: When `true`, the task will not receive preconditions inherited + from parent Taskfiles (via `inherit: true`). Global preconditions defined in + the same file as the task still apply. + +```yaml +tasks: + init: + skip_preconditions: true # skips parent's inherited preconditions + cmds: + - cp .env.template .env +``` + #### `watch` - **Type**: `bool` @@ -921,3 +953,37 @@ tasks: # Command level shopt: [globstar] ``` + +## Precondition + +A precondition is a shell command that must exit 0 before a task (or any task, +when used at the taskfile level) is allowed to run. + +### `sh` + +- **Type**: `string` +- **Description**: Shell command to run as the precondition check + +### `msg` + +- **Type**: `string` +- **Description**: Message to display when the precondition is not met + +### `inherit` + +- **Type**: `bool` +- **Default**: `false` +- **Description**: When `true` and the precondition is defined at the taskfile + level, it propagates to tasks in included Taskfiles. Has no effect on + task-level preconditions. + +```yaml +preconditions: + # Shorthand (no msg or inherit) + - test -f .env + + # Full form + - sh: '[ "$USER" != "root" ]' + msg: "Don't run as root" + inherit: true +``` diff --git a/website/src/public/schema.json b/website/src/public/schema.json index 28ae66110b..85ae99bc5c 100644 --- a/website/src/public/schema.json +++ b/website/src/public/schema.json @@ -210,6 +210,11 @@ "description": "When running tasks in parallel, stop all tasks if one fails.", "type": "boolean", "default": false + }, + "skip_preconditions": { + "description": "When true, the task will not receive preconditions inherited from parent Taskfiles. Global preconditions defined in the same file as the task still apply.", + "type": "boolean", + "default": false } } }, @@ -565,6 +570,11 @@ "msg": { "description": "Failure message to display when the condition fails", "type": "string" + }, + "inherit": { + "description": "When true and defined at the taskfile level, this precondition also applies to tasks in included Taskfiles.", + "type": "boolean", + "default": false } }, "additionalProperties": false @@ -780,6 +790,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,