diff --git a/task_test.go b/task_test.go
index 7b986662cd..79b4b0d544 100644
--- a/task_test.go
+++ b/task_test.go
@@ -2566,6 +2566,184 @@ func TestWildcard(t *testing.T) {
}
}
+func TestOverrides(t *testing.T) {
+ t.Parallel()
+
+ t.Run("basic_override", func(t *testing.T) {
+ t.Parallel()
+
+ var buff bytes.Buffer
+ e := task.NewExecutor(
+ task.WithDir("testdata/overrides"),
+ task.WithStdout(&buff),
+ task.WithStderr(&buff),
+ task.WithSilent(true),
+ )
+ require.NoError(t, e.Setup())
+ require.NoError(t, e.Run(context.Background(), &task.Call{Task: "greet"}))
+ assert.Equal(t, "Overridden!\n", buff.String())
+ })
+}
+
+func TestOverridesFlatten(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ task string
+ expectedOutput string
+ }{
+ {"overridden_task", "from_entrypoint", "overridden from included\n"},
+ {"new_task_from_override", "from_included", "from included\n"},
+ {"default_task_from_override", "default", "default from with_default\n"},
+ {"new_task_from_with_default", "from_with_default", "from with_default\n"},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
+
+ var buff bytes.Buffer
+ e := task.NewExecutor(
+ task.WithDir("testdata/overrides_flatten"),
+ task.WithStdout(&buff),
+ task.WithStderr(&buff),
+ task.WithSilent(true),
+ )
+ require.NoError(t, e.Setup())
+ require.NoError(t, e.Run(context.Background(), &task.Call{Task: test.task}))
+ assert.Equal(t, test.expectedOutput, buff.String())
+ })
+ }
+}
+
+func TestOverridesNested(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ task string
+ expectedOutput string
+ }{
+ {"base_task", "base", "base\n"},
+ {"level1_task", "level1", "level1\n"},
+ {"level2_task", "level2", "level2\n"},
+ {"shared_task_final_override", "shared", "shared from level2 - final override\n"},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
+
+ var buff bytes.Buffer
+ e := task.NewExecutor(
+ task.WithDir("testdata/overrides_nested"),
+ task.WithStdout(&buff),
+ task.WithStderr(&buff),
+ task.WithSilent(true),
+ )
+ require.NoError(t, e.Setup())
+ require.NoError(t, e.Run(context.Background(), &task.Call{Task: test.task}))
+ assert.Equal(t, test.expectedOutput, buff.String())
+ })
+ }
+}
+
+func TestOverridesWithIncludes(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ task string
+ expectedOutput string
+ }{
+ {"main_task", "main", "main task\n"},
+ {"included_task", "lib:lib_task", "lib task\n"},
+ {"overridden_shared_task", "shared", "shared from override - this should win\n"},
+ {"new_task_from_override", "new_task", "new task from override\n"},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
+
+ var buff bytes.Buffer
+ e := task.NewExecutor(
+ task.WithDir("testdata/overrides_with_includes"),
+ task.WithStdout(&buff),
+ task.WithStderr(&buff),
+ task.WithSilent(true),
+ )
+ require.NoError(t, e.Setup())
+ require.NoError(t, e.Run(context.Background(), &task.Call{Task: test.task}))
+ assert.Equal(t, test.expectedOutput, buff.String())
+ })
+ }
+}
+
+func TestOverridesCycle(t *testing.T) {
+ t.Parallel()
+
+ const dir = "testdata/overrides_cycle"
+
+ var buff bytes.Buffer
+ e := task.NewExecutor(
+ task.WithDir(dir),
+ task.WithStdout(&buff),
+ task.WithStderr(&buff),
+ task.WithSilent(true),
+ )
+
+ err := e.Setup()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "task: include cycle detected between")
+}
+
+func TestOverridesOptional(t *testing.T) {
+ t.Parallel()
+
+ var buff bytes.Buffer
+ e := task.NewExecutor(
+ task.WithDir("testdata/overrides_optional"),
+ task.WithStdout(&buff),
+ task.WithStderr(&buff),
+ task.WithSilent(true),
+ )
+ require.NoError(t, e.Setup())
+ require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"}))
+ assert.Equal(t, "overridden_from_existing\n", buff.String())
+}
+
+func TestOverridesWithVars(t *testing.T) {
+ t.Parallel()
+
+ var buff bytes.Buffer
+ e := task.NewExecutor(
+ task.WithDir("testdata/overrides_with_vars"),
+ task.WithStdout(&buff),
+ task.WithStderr(&buff),
+ task.WithSilent(true),
+ )
+ require.NoError(t, e.Setup())
+ require.NoError(t, e.Run(context.Background(), &task.Call{Task: "test"}))
+ assert.Equal(t, "override_value-global\n", buff.String())
+}
+
+func TestOverridesInterpolation(t *testing.T) {
+ t.Parallel()
+
+ var buff bytes.Buffer
+ e := task.NewExecutor(
+ task.WithDir("testdata/overrides_interpolation"),
+ task.WithStdout(&buff),
+ task.WithStderr(&buff),
+ task.WithSilent(true),
+ )
+ require.NoError(t, e.Setup())
+ require.NoError(t, e.Run(context.Background(), &task.Call{Task: "test"}))
+ assert.Equal(t, "interpolated override\n", buff.String())
+}
+
// enableExperimentForTest enables the experiment behind pointer e for the duration of test t and sub-tests,
// with the experiment being restored to its previous state when tests complete.
//
diff --git a/taskfile/ast/graph.go b/taskfile/ast/graph.go
index cb30093d88..2ead3fbf4f 100644
--- a/taskfile/ast/graph.go
+++ b/taskfile/ast/graph.go
@@ -81,20 +81,29 @@ func (tfg *TaskfileGraph) Merge() (*Taskfile, error) {
return err
}
- // Get the merge options
- includes, ok := edge.Properties.Data.([]*Include)
- if !ok {
- return fmt.Errorf("task: Failed to get merge options")
- }
-
- // Merge the included Taskfiles into the parent Taskfile
- for _, include := range includes {
- if err := vertex.Taskfile.Merge(
- includedVertex.Taskfile,
- include,
- ); err != nil {
- return err
+ // Get the merge options - could be includes or overrides
+ if includes, ok := edge.Properties.Data.([]*Include); ok {
+ // Merge the included Taskfiles into the parent Taskfile
+ for _, include := range includes {
+ if err := vertex.Taskfile.Merge(
+ includedVertex.Taskfile,
+ include,
+ ); err != nil {
+ return err
+ }
}
+ } else if overrides, ok := edge.Properties.Data.([]*Override); ok {
+ // Merge the overridden Taskfiles into the parent Taskfile
+ for _, override := range overrides {
+ if err := vertex.Taskfile.MergeOverride(
+ includedVertex.Taskfile,
+ override,
+ ); err != nil {
+ return err
+ }
+ }
+ } else {
+ return fmt.Errorf("task: Failed to get merge options")
}
return nil
diff --git a/taskfile/ast/override.go b/taskfile/ast/override.go
new file mode 100644
index 0000000000..e6fc8f8321
--- /dev/null
+++ b/taskfile/ast/override.go
@@ -0,0 +1,212 @@
+package ast
+
+import (
+ "iter"
+ "sync"
+
+ "github.com/elliotchance/orderedmap/v3"
+ "gopkg.in/yaml.v3"
+
+ "github.com/go-task/task/v3/errors"
+ "github.com/go-task/task/v3/internal/deepcopy"
+)
+
+type (
+ // Override represents information about overridden taskfiles
+ Override struct {
+ Namespace string
+ Taskfile string
+ Dir string
+ Optional bool
+ Internal bool
+ Aliases []string
+ Excludes []string
+ AdvancedImport bool
+ Vars *Vars
+ Flatten bool
+ Checksum string
+ }
+ // Overrides is an ordered map of namespaces to overrides.
+ Overrides struct {
+ om *orderedmap.OrderedMap[string, *Override]
+ mutex sync.RWMutex
+ }
+ // An OverrideElement is a key-value pair that is used for initializing an
+ // Overrides structure.
+ OverrideElement orderedmap.Element[string, *Override]
+)
+
+// NewOverrides creates a new instance of Overrides and initializes it with the
+// provided set of elements, if any. The elements are added in the order they
+// are passed.
+func NewOverrides(els ...*OverrideElement) *Overrides {
+ overrides := &Overrides{
+ om: orderedmap.NewOrderedMap[string, *Override](),
+ }
+ for _, el := range els {
+ overrides.Set(el.Key, el.Value)
+ }
+ return overrides
+}
+
+// Len returns the number of overrides in the Overrides map.
+func (overrides *Overrides) Len() int {
+ if overrides == nil || overrides.om == nil {
+ return 0
+ }
+ defer overrides.mutex.RUnlock()
+ overrides.mutex.RLock()
+ return overrides.om.Len()
+}
+
+// Get returns the value the the override with the provided key and a boolean
+// that indicates if the value was found or not. If the value is not found, the
+// returned override is a zero value and the bool is false.
+func (overrides *Overrides) Get(key string) (*Override, bool) {
+ if overrides == nil || overrides.om == nil {
+ return &Override{}, false
+ }
+ defer overrides.mutex.RUnlock()
+ overrides.mutex.RLock()
+ return overrides.om.Get(key)
+}
+
+// Set sets the value of the override with the provided key to the provided
+// value. If the override already exists, its value is updated. If the override
+// does not exist, it is created.
+func (overrides *Overrides) Set(key string, value *Override) bool {
+ if overrides == nil {
+ overrides = NewOverrides()
+ }
+ if overrides.om == nil {
+ overrides.om = orderedmap.NewOrderedMap[string, *Override]()
+ }
+ defer overrides.mutex.Unlock()
+ overrides.mutex.Lock()
+ return overrides.om.Set(key, value)
+}
+
+// All returns an iterator that loops over all task key-value pairs.
+// Range calls the provided function for each override in the map. The function
+// receives the override's key and value as arguments. If the function returns
+// an error, the iteration stops and the error is returned.
+func (overrides *Overrides) All() iter.Seq2[string, *Override] {
+ if overrides == nil || overrides.om == nil {
+ return func(yield func(string, *Override) bool) {}
+ }
+ return overrides.om.AllFromFront()
+}
+
+// Keys returns an iterator that loops over all task keys.
+func (overrides *Overrides) Keys() iter.Seq[string] {
+ if overrides == nil || overrides.om == nil {
+ return func(yield func(string) bool) {}
+ }
+ return overrides.om.Keys()
+}
+
+// Values returns an iterator that loops over all task values.
+func (overrides *Overrides) Values() iter.Seq[*Override] {
+ if overrides == nil || overrides.om == nil {
+ return func(yield func(*Override) bool) {}
+ }
+ return overrides.om.Values()
+}
+
+// UnmarshalYAML implements the yaml.Unmarshaler interface.
+func (overrides *Overrides) UnmarshalYAML(node *yaml.Node) error {
+ if overrides == nil || overrides.om == nil {
+ *overrides = *NewOverrides()
+ }
+ switch node.Kind {
+ case yaml.MappingNode:
+ // NOTE: orderedmap does not have an unmarshaler, so we have to decode
+ // the map manually. We increment over 2 values at a time and assign
+ // them as a key-value pair.
+ for i := 0; i < len(node.Content); i += 2 {
+ keyNode := node.Content[i]
+ valueNode := node.Content[i+1]
+
+ // Decode the value node into an Override struct
+ var v Override
+ if err := valueNode.Decode(&v); err != nil {
+ return errors.NewTaskfileDecodeError(err, node)
+ }
+
+ // Set the override namespace
+ v.Namespace = keyNode.Value
+
+ // Add the override to the ordered map
+ overrides.Set(keyNode.Value, &v)
+ }
+ return nil
+ }
+
+ return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("overrides")
+}
+
+func (override *Override) UnmarshalYAML(node *yaml.Node) error {
+ switch node.Kind {
+
+ case yaml.ScalarNode:
+ var str string
+ if err := node.Decode(&str); err != nil {
+ return errors.NewTaskfileDecodeError(err, node)
+ }
+ override.Taskfile = str
+ // Overrides always flatten automatically
+ override.Flatten = true
+ return nil
+
+ case yaml.MappingNode:
+ var overrideTaskfile struct {
+ Taskfile string
+ Dir string
+ Optional bool
+ Internal bool
+ Flatten bool
+ Aliases []string
+ Excludes []string
+ Vars *Vars
+ Checksum string
+ }
+ if err := node.Decode(&overrideTaskfile); err != nil {
+ return errors.NewTaskfileDecodeError(err, node)
+ }
+ override.Taskfile = overrideTaskfile.Taskfile
+ override.Dir = overrideTaskfile.Dir
+ override.Optional = overrideTaskfile.Optional
+ override.Internal = overrideTaskfile.Internal
+ override.Aliases = overrideTaskfile.Aliases
+ override.Excludes = overrideTaskfile.Excludes
+ override.AdvancedImport = true
+ override.Vars = overrideTaskfile.Vars
+ // Overrides always flatten automatically, ignore the flatten setting from YAML
+ override.Flatten = true
+ override.Checksum = overrideTaskfile.Checksum
+ return nil
+ }
+
+ return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("override")
+}
+
+// DeepCopy creates a new instance of OverriddenTaskfile and copies
+// data by value from the source struct.
+func (override *Override) DeepCopy() *Override {
+ if override == nil {
+ return nil
+ }
+ return &Override{
+ Namespace: override.Namespace,
+ Taskfile: override.Taskfile,
+ Dir: override.Dir,
+ Optional: override.Optional,
+ Internal: override.Internal,
+ Excludes: deepcopy.Slice(override.Excludes),
+ AdvancedImport: override.AdvancedImport,
+ Vars: override.Vars.DeepCopy(),
+ Flatten: override.Flatten,
+ Aliases: deepcopy.Slice(override.Aliases),
+ Checksum: override.Checksum,
+ }
+}
diff --git a/taskfile/ast/taskfile.go b/taskfile/ast/taskfile.go
index 4aad932da7..cb8e2f9042 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
+ Overrides *Overrides
+ Set []string
+ Shopt []string
+ Vars *Vars
+ Env *Vars
+ Tasks *Tasks
+ Silent bool
+ Dotenv []string
+ Run string
+ Interval time.Duration
}
// Merge merges the second Taskfile into the first
@@ -50,6 +51,9 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
if t1.Includes == nil {
t1.Includes = NewIncludes()
}
+ if t1.Overrides == nil {
+ t1.Overrides = NewOverrides()
+ }
if t1.Vars == nil {
t1.Vars = NewVars()
}
@@ -64,23 +68,55 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
}
+// MergeOverride merges the second Taskfile into the first using override semantics
+func (t1 *Taskfile) MergeOverride(t2 *Taskfile, override *Override) error {
+ if !t1.Version.Equal(t2.Version) {
+ return fmt.Errorf(`task: Taskfiles versions should match. First is "%s" but second is "%s"`, t1.Version, t2.Version)
+ }
+ if len(t2.Dotenv) > 0 {
+ return ErrIncludedTaskfilesCantHaveDotenvs
+ }
+ if t2.Output.IsSet() {
+ t1.Output = t2.Output
+ }
+ if t1.Includes == nil {
+ t1.Includes = NewIncludes()
+ }
+ if t1.Overrides == nil {
+ t1.Overrides = NewOverrides()
+ }
+ if t1.Vars == nil {
+ t1.Vars = NewVars()
+ }
+ if t1.Env == nil {
+ t1.Env = NewVars()
+ }
+ if t1.Tasks == nil {
+ t1.Tasks = NewTasks()
+ }
+ t1.Vars.Merge(t2.Vars, nil)
+ t1.Env.Merge(t2.Env, nil)
+ return t1.Tasks.MergeOverride(t2.Tasks, override, t1.Vars)
+}
+
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
+ Overrides *Overrides
+ 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)
@@ -89,6 +125,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
tf.Output = taskfile.Output
tf.Method = taskfile.Method
tf.Includes = taskfile.Includes
+ tf.Overrides = taskfile.Overrides
tf.Set = taskfile.Set
tf.Shopt = taskfile.Shopt
tf.Vars = taskfile.Vars
@@ -101,6 +138,9 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
if tf.Includes == nil {
tf.Includes = NewIncludes()
}
+ if tf.Overrides == nil {
+ tf.Overrides = NewOverrides()
+ }
if tf.Vars == nil {
tf.Vars = NewVars()
}
diff --git a/taskfile/ast/tasks.go b/taskfile/ast/tasks.go
index dd499b85c7..58d6391e39 100644
--- a/taskfile/ast/tasks.go
+++ b/taskfile/ast/tasks.go
@@ -208,6 +208,43 @@ func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars)
return nil
}
+func (t1 *Tasks) MergeOverride(t2 *Tasks, override *Override, includedTaskfileVars *Vars) 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()
+ // Set the task to internal if EITHER the overridden task or the overridden
+ // taskfile are marked as internal
+ task.Internal = task.Internal || (override != nil && override.Internal)
+ taskName := name
+
+ // if the task is in the exclude list, don't add it to the merged taskfile
+ if slices.Contains(override.Excludes, name) {
+ continue
+ }
+
+ // Overrides always flatten, so we don't need the namespace logic
+ // but we still need to handle variables and directory resolution
+
+ if override.AdvancedImport {
+ task.Dir = filepathext.SmartJoin(override.Dir, task.Dir)
+ if task.IncludeVars == nil {
+ task.IncludeVars = NewVars()
+ }
+ task.IncludeVars.Merge(override.Vars, nil)
+ task.IncludedTaskfileVars = includedTaskfileVars.DeepCopy()
+ }
+
+ // For overrides, we simply replace any existing task instead of erroring
+ // This is the key difference from includes
+ t1.Set(taskName, task)
+ }
+
+ return nil
+}
+
func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
if t == nil || t.om == nil {
*t = *NewTasks()
diff --git a/taskfile/reader.go b/taskfile/reader.go
index 3f36ad62b2..04fabfcc39 100644
--- a/taskfile/reader.go
+++ b/taskfile/reader.go
@@ -314,6 +314,88 @@ func (r *Reader) include(ctx context.Context, node Node) error {
})
}
+ // Loop over each overridden taskfile
+ for _, override := range vertex.Taskfile.Overrides.All() {
+ vars := env.GetEnviron()
+ vars.Merge(vertex.Taskfile.Vars, nil)
+ // Start a goroutine to process each overridden Taskfile
+ g.Go(func() error {
+ cache := &templater.Cache{Vars: vars}
+ override = &ast.Override{
+ Namespace: override.Namespace,
+ Taskfile: templater.Replace(override.Taskfile, cache),
+ Dir: templater.Replace(override.Dir, cache),
+ Optional: override.Optional,
+ Internal: override.Internal,
+ Flatten: override.Flatten,
+ Aliases: override.Aliases,
+ AdvancedImport: override.AdvancedImport,
+ Excludes: override.Excludes,
+ Vars: override.Vars,
+ Checksum: override.Checksum,
+ }
+ if err := cache.Err(); err != nil {
+ return err
+ }
+
+ entrypoint, err := node.ResolveEntrypoint(override.Taskfile)
+ if err != nil {
+ return err
+ }
+
+ override.Dir, err = node.ResolveDir(override.Dir)
+ if err != nil {
+ return err
+ }
+
+ overrideNode, err := NewNode(entrypoint, override.Dir, r.insecure,
+ WithParent(node),
+ WithChecksum(override.Checksum),
+ )
+ if err != nil {
+ if override.Optional {
+ return nil
+ }
+ return err
+ }
+
+ // Recurse into the overridden Taskfile
+ if err := r.include(ctx, overrideNode); err != nil {
+ return err
+ }
+
+ // Create an edge between the Taskfiles
+ r.graph.Lock()
+ defer r.graph.Unlock()
+ edge, err := r.graph.Edge(node.Location(), overrideNode.Location())
+ if err == graph.ErrEdgeNotFound {
+ // If the edge doesn't exist, create it
+ err = r.graph.AddEdge(
+ node.Location(),
+ overrideNode.Location(),
+ graph.EdgeData([]*ast.Override{override}),
+ graph.EdgeWeight(1),
+ )
+ } else {
+ // If the edge already exists
+ edgeData := append(edge.Properties.Data.([]*ast.Override), override)
+ err = r.graph.UpdateEdge(
+ node.Location(),
+ overrideNode.Location(),
+ graph.EdgeData(edgeData),
+ graph.EdgeWeight(len(edgeData)),
+ )
+ }
+ if errors.Is(err, graph.ErrEdgeCreatesCycle) {
+ return errors.TaskfileCycleError{
+ Source: node.Location(),
+ Destination: overrideNode.Location(),
+ }
+ }
+ return err
+ })
+ }
+
// Wait for all the go routines to finish
return g.Wait()
}
diff --git a/testdata/overrides/Taskfile.yml b/testdata/overrides/Taskfile.yml
new file mode 100644
index 0000000000..7969789661
--- /dev/null
+++ b/testdata/overrides/Taskfile.yml
@@ -0,0 +1,10 @@
+version: '3'
+
+overrides:
+ lib:
+ taskfile: ./overrides.yml
+
+tasks:
+ greet:
+ cmds:
+ - echo "Greet"
\ No newline at end of file
diff --git a/testdata/overrides/overrides.yml b/testdata/overrides/overrides.yml
new file mode 100644
index 0000000000..c56123f768
--- /dev/null
+++ b/testdata/overrides/overrides.yml
@@ -0,0 +1,6 @@
+version: '3'
+
+tasks:
+ greet:
+ cmds:
+ - echo "Overridden!"
\ No newline at end of file
diff --git a/testdata/overrides_cycle/Taskfile.yml b/testdata/overrides_cycle/Taskfile.yml
new file mode 100644
index 0000000000..5095d785ad
--- /dev/null
+++ b/testdata/overrides_cycle/Taskfile.yml
@@ -0,0 +1,9 @@
+version: '3'
+
+overrides:
+ one: ./one.yml
+
+tasks:
+ default:
+ cmds:
+ - echo "default"
\ No newline at end of file
diff --git a/testdata/overrides_cycle/one.yml b/testdata/overrides_cycle/one.yml
new file mode 100644
index 0000000000..3da52dab37
--- /dev/null
+++ b/testdata/overrides_cycle/one.yml
@@ -0,0 +1,9 @@
+version: '3'
+
+overrides:
+ two: ./two.yml
+
+tasks:
+ one:
+ cmds:
+ - echo "one"
\ No newline at end of file
diff --git a/testdata/overrides_cycle/two.yml b/testdata/overrides_cycle/two.yml
new file mode 100644
index 0000000000..c29d6b6679
--- /dev/null
+++ b/testdata/overrides_cycle/two.yml
@@ -0,0 +1,9 @@
+version: '3'
+
+overrides:
+ one: ./one.yml
+
+tasks:
+ two:
+ cmds:
+ - echo "two"
\ No newline at end of file
diff --git a/testdata/overrides_flatten/Taskfile.with_default.yml b/testdata/overrides_flatten/Taskfile.with_default.yml
new file mode 100644
index 0000000000..30637b3760
--- /dev/null
+++ b/testdata/overrides_flatten/Taskfile.with_default.yml
@@ -0,0 +1,9 @@
+version: '3'
+
+tasks:
+ default:
+ cmds:
+ - echo "default from with_default"
+ from_with_default:
+ cmds:
+ - echo "from with_default"
\ No newline at end of file
diff --git a/testdata/overrides_flatten/Taskfile.yml b/testdata/overrides_flatten/Taskfile.yml
new file mode 100644
index 0000000000..98896d5c6f
--- /dev/null
+++ b/testdata/overrides_flatten/Taskfile.yml
@@ -0,0 +1,10 @@
+version: '3'
+
+overrides:
+ included:
+ taskfile: ./included/Taskfile.yml
+ dir: ./included
+ with_default: ./Taskfile.with_default.yml
+
+tasks:
+ from_entrypoint: echo "from entrypoint"
\ No newline at end of file
diff --git a/testdata/overrides_flatten/included/Taskfile.yml b/testdata/overrides_flatten/included/Taskfile.yml
new file mode 100644
index 0000000000..9c9d6803c3
--- /dev/null
+++ b/testdata/overrides_flatten/included/Taskfile.yml
@@ -0,0 +1,9 @@
+version: '3'
+
+tasks:
+ from_entrypoint:
+ cmds:
+ - echo "overridden from included"
+ from_included:
+ cmds:
+ - echo "from included"
\ No newline at end of file
diff --git a/testdata/overrides_interpolation/Taskfile.yml b/testdata/overrides_interpolation/Taskfile.yml
new file mode 100644
index 0000000000..13fc0e93ab
--- /dev/null
+++ b/testdata/overrides_interpolation/Taskfile.yml
@@ -0,0 +1,12 @@
+version: '3'
+
+vars:
+ FILE: "override"
+
+overrides:
+ lib: ./{{.FILE}}.yml
+
+tasks:
+ test:
+ cmds:
+ - echo "base"
\ No newline at end of file
diff --git a/testdata/overrides_interpolation/override.yml b/testdata/overrides_interpolation/override.yml
new file mode 100644
index 0000000000..9ed83f6d72
--- /dev/null
+++ b/testdata/overrides_interpolation/override.yml
@@ -0,0 +1,6 @@
+version: '3'
+
+tasks:
+ test:
+ cmds:
+ - echo "interpolated override"
\ No newline at end of file
diff --git a/testdata/overrides_nested/Taskfile.yml b/testdata/overrides_nested/Taskfile.yml
new file mode 100644
index 0000000000..9e132391a6
--- /dev/null
+++ b/testdata/overrides_nested/Taskfile.yml
@@ -0,0 +1,12 @@
+version: '3'
+
+overrides:
+ level1: ./level1.yml
+
+tasks:
+ base:
+ cmds:
+ - echo "base"
+ shared:
+ cmds:
+ - echo "shared from base"
\ No newline at end of file
diff --git a/testdata/overrides_nested/level1.yml b/testdata/overrides_nested/level1.yml
new file mode 100644
index 0000000000..baef68625a
--- /dev/null
+++ b/testdata/overrides_nested/level1.yml
@@ -0,0 +1,12 @@
+version: '3'
+
+overrides:
+ level2: ./level2.yml
+
+tasks:
+ level1:
+ cmds:
+ - echo "level1"
+ shared:
+ cmds:
+ - echo "shared from level1"
\ No newline at end of file
diff --git a/testdata/overrides_nested/level2.yml b/testdata/overrides_nested/level2.yml
new file mode 100644
index 0000000000..0b25556518
--- /dev/null
+++ b/testdata/overrides_nested/level2.yml
@@ -0,0 +1,9 @@
+version: '3'
+
+tasks:
+ level2:
+ cmds:
+ - echo "level2"
+ shared:
+ cmds:
+ - echo "shared from level2 - final override"
\ No newline at end of file
diff --git a/testdata/overrides_optional/Taskfile.yml b/testdata/overrides_optional/Taskfile.yml
new file mode 100644
index 0000000000..8e5548014d
--- /dev/null
+++ b/testdata/overrides_optional/Taskfile.yml
@@ -0,0 +1,14 @@
+version: '3'
+
+overrides:
+ existing:
+ taskfile: ./existing.yml
+ optional: true
+ missing:
+ taskfile: ./missing.yml
+ optional: true
+
+tasks:
+ default:
+ cmds:
+ - echo "called_dep"
\ No newline at end of file
diff --git a/testdata/overrides_optional/existing.yml b/testdata/overrides_optional/existing.yml
new file mode 100644
index 0000000000..1932e0e5e0
--- /dev/null
+++ b/testdata/overrides_optional/existing.yml
@@ -0,0 +1,6 @@
+version: '3'
+
+tasks:
+ default:
+ cmds:
+ - echo "overridden_from_existing"
\ No newline at end of file
diff --git a/testdata/overrides_with_includes/Taskfile.yml b/testdata/overrides_with_includes/Taskfile.yml
new file mode 100644
index 0000000000..90ec5f5727
--- /dev/null
+++ b/testdata/overrides_with_includes/Taskfile.yml
@@ -0,0 +1,15 @@
+version: '3'
+
+includes:
+ lib: ./lib.yml
+
+overrides:
+ override_lib: ./override_lib.yml
+
+tasks:
+ main:
+ cmds:
+ - echo "main task"
+ shared:
+ cmds:
+ - echo "shared from main"
\ No newline at end of file
diff --git a/testdata/overrides_with_includes/lib.yml b/testdata/overrides_with_includes/lib.yml
new file mode 100644
index 0000000000..406bf2b279
--- /dev/null
+++ b/testdata/overrides_with_includes/lib.yml
@@ -0,0 +1,6 @@
+version: '3'
+
+tasks:
+ lib_task:
+ cmds:
+ - echo "lib task"
\ No newline at end of file
diff --git a/testdata/overrides_with_includes/override_lib.yml b/testdata/overrides_with_includes/override_lib.yml
new file mode 100644
index 0000000000..7c318077b5
--- /dev/null
+++ b/testdata/overrides_with_includes/override_lib.yml
@@ -0,0 +1,9 @@
+version: '3'
+
+tasks:
+ shared:
+ cmds:
+ - echo "shared from override - this should win"
+ new_task:
+ cmds:
+ - echo "new task from override"
\ No newline at end of file
diff --git a/testdata/overrides_with_vars/Taskfile.yml b/testdata/overrides_with_vars/Taskfile.yml
new file mode 100644
index 0000000000..183b5da07e
--- /dev/null
+++ b/testdata/overrides_with_vars/Taskfile.yml
@@ -0,0 +1,16 @@
+version: '3'
+
+vars:
+ GLOBAL_VAR: "global"
+
+overrides:
+ lib:
+ taskfile: ./lib.yml
+ vars:
+ OVERRIDE_VAR: "override_value"
+ GLOBAL_VAR: "overridden_global"
+
+tasks:
+ test:
+ cmds:
+ - echo "{{.GLOBAL_VAR}}"
\ No newline at end of file
diff --git a/testdata/overrides_with_vars/lib.yml b/testdata/overrides_with_vars/lib.yml
new file mode 100644
index 0000000000..759ce66643
--- /dev/null
+++ b/testdata/overrides_with_vars/lib.yml
@@ -0,0 +1,6 @@
+version: '3'
+
+tasks:
+ test:
+ cmds:
+ - echo "{{.OVERRIDE_VAR}}-{{.GLOBAL_VAR}}"
\ No newline at end of file
diff --git a/website/docs/reference/schema.mdx b/website/docs/reference/schema.mdx
index 8339f5100e..a1e6d16621 100644
--- a/website/docs/reference/schema.mdx
+++ b/website/docs/reference/schema.mdx
@@ -7,21 +7,22 @@ toc_max_heading_level: 5
# Schema Reference
-| Attribute | Type | Default | Description |
-|------------|------------------------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `version` | `string` | | Version of the Taskfile. The current version is `3`. |
-| `output` | `string` | `interleaved` | Output mode. Available options: `interleaved`, `group` and `prefixed`. |
-| `method` | `string` | `checksum` | Default method in this Taskfile. Can be overridden in a task by task basis. Available options: `checksum`, `timestamp` and `none`. |
-| `includes` | [`map[string]Include`](#include) | | Additional Taskfiles to be included. |
-| `vars` | [`map[string]Variable`](#variable) | | A set of global variables. |
-| `env` | [`map[string]Variable`](#variable) | | A set of global environment variables. |
-| `tasks` | [`map[string]Task`](#task) | | A set of task definitions. |
-| `silent` | `bool` | `false` | Default 'silent' options for this Taskfile. If `false`, can be overridden with `true` in a task by task basis. |
-| `dotenv` | `[]string` | | A list of `.env` file paths to be parsed. |
-| `run` | `string` | `always` | Default 'run' option for this Taskfile. Available options: `always`, `once` and `when_changed`. |
-| `interval` | `string` | `5s` | Sets a different watch interval when using `--watch`, the default being 5 seconds. This string should be a valid [Go Duration](https://pkg.go.dev/time#ParseDuration). |
-| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
-| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
+| Attribute | Type | Default | Description |
+|-------------|------------------------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `version` | `string` | | Version of the Taskfile. The current version is `3`. |
+| `output` | `string` | `interleaved` | Output mode. Available options: `interleaved`, `group` and `prefixed`. |
+| `method` | `string` | `checksum` | Default method in this Taskfile. Can be overridden in a task by task basis. Available options: `checksum`, `timestamp` and `none`. |
+| `includes` | [`map[string]Include`](#include) | | Additional Taskfiles to be included. |
+| `overrides` | [`map[string]Override`](#override) | | Additional Taskfiles to be included with override semantics (automatic flattening and task replacement). |
+| `vars` | [`map[string]Variable`](#variable) | | A set of global variables. |
+| `env` | [`map[string]Variable`](#variable) | | A set of global environment variables. |
+| `tasks` | [`map[string]Task`](#task) | | A set of task definitions. |
+| `silent` | `bool` | `false` | Default 'silent' options for this Taskfile. If `false`, can be overridden with `true` in a task by task basis. |
+| `dotenv` | `[]string` | | A list of `.env` file paths to be parsed. |
+| `run` | `string` | `always` | Default 'run' option for this Taskfile. Available options: `always`, `once` and `when_changed`. |
+| `interval` | `string` | `5s` | Sets a different watch interval when using `--watch`, the default being 5 seconds. This string should be a valid [Go Duration](https://pkg.go.dev/time#ParseDuration). |
+| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
+| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
## Include
@@ -38,8 +39,7 @@ toc_max_heading_level: 5
:::info
-Informing only a string like below is equivalent to setting that value to the
-`taskfile` attribute.
+Informing only a string like below is equivalent to setting that value to the `taskfile` attribute.
```yaml
includes:
@@ -48,10 +48,40 @@ includes:
:::
+## Override
+
+| Attribute | Type | Default | Description |
+|------------|-----------------------|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `taskfile` | `string` | | The path for the Taskfile or directory to be overridden. If a directory, Task will look for files named `Taskfile.yml` or `Taskfile.yaml` inside that directory. If a relative path, resolved relative to the directory containing the overriding Taskfile. |
+| `dir` | `string` | The parent Taskfile directory | The working directory of the overridden tasks when run. |
+| `optional` | `bool` | `false` | If `true`, no errors will be thrown if the specified file does not exist. |
+| `flatten` | `bool` | `true` | Always `true` for overrides. Tasks from the overridden Taskfile are automatically flattened into the overriding Taskfile. If a task with the same name exists, the overridden version replaces the original. |
+| `internal` | `bool` | `false` | Stops any task in the overridden Taskfile from being callable on the command line. These commands will also be omitted from the output when used with `--list`. |
+| `aliases` | `[]string` | | Alternative names for the namespace of the overridden Taskfile (mainly for organizational purposes since overrides are flattened). |
+| `vars` | `map[string]Variable` | | A set of variables to apply to the overridden Taskfile. |
+| `excludes` | `[]string` | | A list of task names to exclude from being overridden. |
+| `checksum` | `string` | | The checksum of the file you expect to override. If the checksum does not match, the file will not be overridden. |
+
+:::info
+
+Overrides work similarly to includes but with automatic flattening and task replacement behavior:
+- Tasks are always flattened into the local namespace (no namespace prefix needed)
+- Duplicate task names replace existing tasks instead of causing errors
+- Later overrides take precedence over earlier ones
+
+Informing only a string like below is equivalent to setting that value to the `taskfile` attribute.
+
+```yaml
+overrides:
+ lib: ./path
+```
+
+:::
+
## Variable
| Attribute | Type | Default | Description |
-| --------- | -------- | ------- | ------------------------------------------------------------------------ |
+|-----------|----------|---------|--------------------------------------------------------------------------|
| _itself_ | `string` | | A static value that will be set to the variable. |
| `sh` | `string` | | A shell command. The output (`STDOUT`) will be assigned to the variable. |
@@ -84,7 +114,7 @@ vars:
## Task
| Attribute | Type | Default | Description |
-| --------------- | ---------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+|-----------------|------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `cmds` | [`[]Command`](#command) | | A list of shell commands to be executed. |
| `deps` | [`[]Dependency`](#dependency) | | A list of dependencies of this task. Tasks defined here will run in parallel before this task. |
| `label` | `string` | | Overrides the name of the task in the output when a task is run. Supports variables. |
@@ -108,7 +138,7 @@ vars:
| `prefix` | `string` | | Defines a string to prefix the output of tasks running in parallel. Only used when the output mode is `prefixed`. |
| `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing commands. |
| `run` | `string` | The one declared globally in the Taskfile or `always` | Specifies whether the task should run again or not if called more than once. Available options: `always`, `once` and `when_changed`. |
-| `platforms` | `[]string` | All platforms | Specifies which platforms the task should be run on. [Valid GOOS and GOARCH values allowed](https://github.com/golang/go/blob/master/src/internal/syslist/syslist.go). Task will be skipped otherwise. |
+| `platforms` | `[]string` | All platforms | Specifies which platforms the task should be run on. [Valid GOOS and GOARCH values allowed](https://github.com/golang/go/blob/master/src/internal/syslist/syslist.go). Task will be skipped otherwise. |
| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
@@ -133,18 +163,18 @@ tasks:
### Command
-| Attribute | Type | Default | Description |
-| -------------- | ---------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `cmd` | `string` | | The shell command to be executed. |
-| `task` | `string` | | Set this to trigger execution of another task instead of running a command. This cannot be set together with `cmd`. |
-| `for` | [`For`](#for) | | Runs the command once for each given value. |
-| `silent` | `bool` | `false` | Skips some output for this command. Note that STDOUT and STDERR of the commands will still be redirected. |
-| `vars` | [`map[string]Variable`](#variable) | | Optional additional variables to be passed to the referenced task. Only relevant when setting `task` instead of `cmd`. |
-| `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing the command. |
-| `defer` | [`Defer`](#defer) | | Alternative to `cmd`, but schedules the command or a task to be executed at the end of this task instead of immediately. This cannot be used together with `cmd`. |
+| Attribute | Type | Default | Description |
+|----------------|------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `cmd` | `string` | | The shell command to be executed. |
+| `task` | `string` | | Set this to trigger execution of another task instead of running a command. This cannot be set together with `cmd`. |
+| `for` | [`For`](#for) | | Runs the command once for each given value. |
+| `silent` | `bool` | `false` | Skips some output for this command. Note that STDOUT and STDERR of the commands will still be redirected. |
+| `vars` | [`map[string]Variable`](#variable) | | Optional additional variables to be passed to the referenced task. Only relevant when setting `task` instead of `cmd`. |
+| `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing the command. |
+| `defer` | [`Defer`](#defer) | | Alternative to `cmd`, but schedules the command or a task to be executed at the end of this task instead of immediately. This cannot be used together with `cmd`. |
| `platforms` | `[]string` | All platforms | Specifies which platforms the command should be run on. [Valid GOOS and GOARCH values allowed](https://github.com/golang/go/blob/master/src/internal/syslist/syslist.go). Command will be skipped otherwise. |
-| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
-| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
+| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
+| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
:::info
@@ -163,7 +193,7 @@ tasks:
### Dependency
| Attribute | Type | Default | Description |
-| --------- | ---------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------- |
+|-----------|------------------------------------|---------|------------------------------------------------------------------------------------------------------------------|
| `task` | `string` | | The task to be execute as a dependency. |
| `vars` | [`map[string]Variable`](#variable) | | Optional additional variables to be passed to this task. |
| `silent` | `bool` | `false` | Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`. |
@@ -186,10 +216,10 @@ tasks:
The `defer` parameter defines a shell command to run, or a task to trigger, at the end of the current task instead of immediately.
If defined as a string this is a shell command, otherwise it is a map defining a task to call:
-| Attribute | Type | Default | Description |
-| --------- | ---------------------------------- | ------- | ----------------------------------------------------------------- |
-| `task` | `string` | | The deferred task to trigger. |
-| `vars` | [`map[string]Variable`](#variable) | | Optional additional variables to be passed to the deferred task. |
+| Attribute | Type | Default | Description |
+|-----------|------------------------------------|---------|------------------------------------------------------------------------------------------------------------------|
+| `task` | `string` | | The deferred task to trigger. |
+| `vars` | [`map[string]Variable`](#variable) | | Optional additional variables to be passed to the deferred task. |
| `silent` | `bool` | `false` | Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`. |
### For
@@ -210,7 +240,7 @@ Finally, the `for` parameter can be defined as a map when you want to use a
variable to define the values to loop over:
| Attribute | Type | Default | Description |
-| --------- | -------- | ---------------- | -------------------------------------------- |
+|-----------|----------|------------------|----------------------------------------------|
| `var` | `string` | | The name of the variable to use as an input. |
| `split` | `string` | (any whitespace) | What string the variable should be split on. |
| `as` | `string` | `ITEM` | The name of the iterator variable. |
@@ -218,7 +248,7 @@ variable to define the values to loop over:
### Precondition
| Attribute | Type | Default | Description |
-| --------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------ |
+|-----------|----------|---------|--------------------------------------------------------------------------------------------------------------|
| `sh` | `string` | | Command to be executed. If a non-zero exit code is returned, the task errors without executing its commands. |
| `msg` | `string` | | Optional message to print if the precondition isn't met. |
@@ -238,5 +268,5 @@ tasks:
### Requires
| Attribute | Type | Default | Description |
-| --------- | ---------- | ------- | -------------------------------------------------------------------------------------------------- |
+|-----------|------------|---------|----------------------------------------------------------------------------------------------------|
| `vars` | `[]string` | | List of variable or environment variable names that must be set if this task is to execute and run |
diff --git a/website/docs/usage.mdx b/website/docs/usage.mdx
index 9590cf4f25..e2ff49fc05 100644
--- a/website/docs/usage.mdx
+++ b/website/docs/usage.mdx
@@ -480,6 +480,239 @@ overridable, use the
:::
+## Overriding tasks from other Taskfiles
+
+Task supports overriding tasks from other Taskfiles using the `overrides` keyword.
+This is similar to includes, but with a key difference: instead of erroring when
+duplicate task names are found, overrides will replace existing tasks with the
+overridden version. Overrides are automatically flattened into the local namespace.
+
+```yaml
+version: '3'
+
+overrides:
+ lib:
+ taskfile: ./overrides.yml
+
+tasks:
+ greet:
+ cmds:
+ - echo "Original"
+```
+
+If `./overrides.yml` contains a task named `greet`, it will replace the original
+`greet` task in the main Taskfile.
+
+
+
+
+```yaml
+version: '3'
+
+overrides:
+ lib:
+ taskfile: ./overrides.yml
+
+tasks:
+ greet:
+ cmds:
+ - echo "Original"
+```
+
+
+
+
+```yaml
+version: '3'
+
+tasks:
+ greet:
+ cmds:
+ - echo "Overridden!"
+ new_task:
+ cmds:
+ - echo "New task from override"
+```
+
+
+
+
+Running `task greet` will output "Overridden!" instead of "Original". The
+`new_task` will also be available directly without any namespace prefix.
+
+### Key differences from includes
+
+- **Automatic flattening**: Overrides are always flattened into the local namespace
+- **Task replacement**: Duplicate task names replace existing tasks instead of causing errors
+- **Order matters**: Later overrides take precedence over earlier ones
+
+### Nested overrides
+
+Overrides can be nested multiple levels deep, with the final override taking precedence:
+
+
+
+
+```yaml
+version: '3'
+
+overrides:
+ level1: ./level1.yml
+
+tasks:
+ shared:
+ cmds:
+ - echo "base"
+```
+
+
+
+
+```yaml
+version: '3'
+
+overrides:
+ level2: ./level2.yml
+
+tasks:
+ shared:
+ cmds:
+ - echo "level1"
+```
+
+
+
+
+```yaml
+version: '3'
+
+tasks:
+ shared:
+ cmds:
+ - echo "level2 - final"
+```
+
+
+
+
+Running `task shared` will output "level2 - final" as it's the final override in the chain.
+
+### Optional overrides
+
+Like includes, overrides can be marked as optional:
+
+```yaml
+version: '3'
+
+overrides:
+ optional_lib:
+ taskfile: ./optional_overrides.yml
+ optional: true
+
+tasks:
+ greet:
+ cmds:
+ - echo "This will work even if ./optional_overrides.yml doesn't exist"
+```
+
+### Internal overrides
+
+Overrides marked as internal will set all overridden tasks to be internal as well:
+
+```yaml
+version: '3'
+
+overrides:
+ utils:
+ taskfile: ./utils.yml
+ internal: true
+```
+
+### Variables in overrides
+
+You can specify variables when overriding, just like with includes:
+
+```yaml
+version: '3'
+
+overrides:
+ customized:
+ taskfile: ./base.yml
+ vars:
+ ENVIRONMENT: "production"
+ VERSION: "2.1.0"
+```
+
+### Directory for overridden tasks
+
+By default, overridden tasks run in the current directory, but you can specify
+a different directory:
+
+```yaml
+version: '3'
+
+overrides:
+ backend:
+ taskfile: ./backend/tasks.yml
+ dir: ./backend
+```
+
+### Excluding tasks from overrides
+
+You can exclude specific tasks from being overridden:
+
+```yaml
+version: '3'
+
+overrides:
+ lib:
+ taskfile: ./lib.yml
+ excludes: [internal_task, helper]
+```
+
+### Namespace aliases for overrides
+
+Even though overrides are automatically flattened, you can still use aliases
+for organizational purposes:
+
+```yaml
+version: '3'
+
+overrides:
+ library:
+ taskfile: ./library.yml
+ aliases: [lib]
+```
+
+### Combining overrides with includes
+
+Overrides work seamlessly with includes. Includes preserve namespaces while
+overrides flatten and replace:
+
+```yaml
+version: '3'
+
+includes:
+ utils: ./utils.yml # Available as utils:task-name
+
+overrides:
+ customizations: ./custom.yml # Available directly as task-name
+
+tasks:
+ main:
+ cmds:
+ - task: utils:helper # Included task with namespace
+ - task: custom_task # Overridden task without namespace
+```
+
+:::info
+
+Like includes, overridden Taskfiles must use the same schema version as the main
+Taskfile. Variables declared in overridden Taskfiles take preference over
+variables in the overriding Taskfile.
+
+:::
+
## Internal tasks
Internal tasks are tasks that cannot be called directly by the user. They will
diff --git a/website/static/next-schema.json b/website/static/next-schema.json
index 0a229bedc9..28b30c863d 100644
--- a/website/static/next-schema.json
+++ b/website/static/next-schema.json
@@ -695,6 +695,67 @@
}
}
},
+ "overrides": {
+ "description": "Imports tasks from the specified taskfiles with override semantics. Tasks are automatically flattened and duplicate names replace existing tasks instead of causing errors.",
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "taskfile": {
+ "description": "The path for the Taskfile or directory to be overridden. If a directory, Task will look for files named `Taskfile.yml` or `Taskfile.yaml` inside that directory. If a relative path, resolved relative to the directory containing the overriding Taskfile.",
+ "type": "string"
+ },
+ "dir": {
+ "description": "The working directory of the overridden tasks when run.",
+ "type": "string"
+ },
+ "optional": {
+ "description": "If `true`, no errors will be thrown if the specified file does not exist.",
+ "type": "boolean"
+ },
+ "flatten": {
+ "description": "Always `true` for overrides. Tasks from the overridden Taskfile are automatically flattened into the overriding Taskfile.",
+ "type": "boolean",
+ "default": true
+ },
+ "internal": {
+ "description": "Stops any task in the overridden Taskfile from being callable on the command line. These commands will also be omitted from the output when used with `--list`.",
+ "type": "boolean"
+ },
+ "aliases": {
+ "description": "Alternative names for the namespace of the overridden Taskfile (mainly for organizational purposes since overrides are flattened).",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "excludes": {
+ "description": "A list of task names to exclude from being overridden.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "vars": {
+ "description": "A set of variables to apply to the overridden Taskfile.",
+ "$ref": "#/definitions/vars"
+ },
+ "checksum": {
+ "description": "The checksum of the file you expect to override. If the checksum does not match, the file will not be overridden.",
+ "type": "string"
+ }
+ }
+ }
+ ]
+ }
+ }
+ },
"vars": {
"description": "A set of global variables.",
"$ref": "#/definitions/vars"
@@ -748,11 +809,23 @@
{
"required": ["includes"]
},
+ {
+ "required": ["overrides"]
+ },
{
"required": ["tasks"]
},
{
"required": ["includes", "tasks"]
+ },
+ {
+ "required": ["overrides", "tasks"]
+ },
+ {
+ "required": ["includes", "overrides"]
+ },
+ {
+ "required": ["includes", "overrides", "tasks"]
}
]
}