mirror of
https://github.com/go-task/task.git
synced 2026-06-11 09:51:50 +00:00
feat(scoped): refactor compiler, add nested includes, document flatten
Refactor compiler.go for better maintainability: - Extract isScopedMode() helper function - Split getVariables() into getScopedVariables() and getLegacyVariables() - Fix directory resolution: parent chain env/vars now resolve from their own directory instead of the current task's directory Add nested includes support and tests: - Add testdata/scoped_taskfiles/inc_a/nested/Taskfile.yml (3 levels deep) - Add test case for nested include inheritance (root → a → nested) - Verify nested includes inherit vars from full parent chain Fix flaky tests: - Remove VAR from print tasks (defined in both inc_a and inc_b) - Test only unique variables (UNIQUE_A, UNIQUE_B, ROOT_VAR) Document flatten: true escape hatch: - Add migration guide step for using flatten: true - Add new section explaining flatten bypasses scoping - Include example and usage recommendations
This commit is contained in:
350
compiler.go
350
compiler.go
@@ -47,17 +47,38 @@ func (c *Compiler) FastGetVariables(t *ast.Task, call *Call) (*ast.Vars, error)
|
||||
return c.getVariables(t, call, false)
|
||||
}
|
||||
|
||||
func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) {
|
||||
// In scoped mode, OS env vars are in {{.env.XXX}} namespace, not at root
|
||||
// In legacy mode, they are at root level
|
||||
scopedMode := experiments.ScopedTaskfiles.Enabled() && t != nil && t.Location != nil && c.Graph != nil
|
||||
var result *ast.Vars
|
||||
if scopedMode {
|
||||
result = ast.NewVars()
|
||||
} else {
|
||||
result = env.GetEnviron()
|
||||
}
|
||||
// isScopedMode returns true if scoped variable resolution should be used.
|
||||
// Scoped mode requires the experiment to be enabled, a task with location info, and a graph.
|
||||
func (c *Compiler) isScopedMode(t *ast.Task) bool {
|
||||
return experiments.ScopedTaskfiles.Enabled() &&
|
||||
t != nil &&
|
||||
t.Location != nil &&
|
||||
c.Graph != nil
|
||||
}
|
||||
|
||||
func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) {
|
||||
if c.isScopedMode(t) {
|
||||
return c.getScopedVariables(t, call, evaluateShVars)
|
||||
}
|
||||
return c.getLegacyVariables(t, call, evaluateShVars)
|
||||
}
|
||||
|
||||
// getScopedVariables resolves variables in scoped mode.
|
||||
// In scoped mode:
|
||||
// - OS env vars are in {{.env.XXX}} namespace, not at root
|
||||
// - Variables from sibling includes are isolated
|
||||
//
|
||||
// Variable resolution order (lowest to highest priority):
|
||||
// 1. Root Taskfile vars
|
||||
// 2. Include Taskfile vars
|
||||
// 3. Include passthrough vars (includes: name: vars:)
|
||||
// 4. Task vars
|
||||
// 5. Call vars
|
||||
// 6. CLI vars
|
||||
func (c *Compiler) getScopedVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) {
|
||||
result := ast.NewVars()
|
||||
|
||||
// Add special variables (TASK, ROOT_DIR, etc.)
|
||||
specialVars, err := c.getSpecialVars(t, call)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -66,6 +87,8 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
|
||||
result.Set(k, ast.Var{Value: v})
|
||||
}
|
||||
|
||||
// Create range function for resolving vars in a given directory
|
||||
// NOTE: This closure captures result directly - do not refactor to method call
|
||||
getRangeFunc := func(dir string) func(k string, v ast.Var) error {
|
||||
return func(k string, v ast.Var) error {
|
||||
cache := &templater.Cache{Vars: result}
|
||||
@@ -103,10 +126,9 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
|
||||
}
|
||||
rangeFunc := getRangeFunc(c.Dir)
|
||||
|
||||
// Create task-specific range function if we have a task
|
||||
var taskRangeFunc func(k string, v ast.Var) error
|
||||
if t != nil {
|
||||
// NOTE(@andreynering): We're manually joining these paths here because
|
||||
// this is the raw task, not the compiled one.
|
||||
cache := &templater.Cache{Vars: result}
|
||||
dir := templater.Replace(t.Dir, cache)
|
||||
if err := cache.Err(); err != nil {
|
||||
@@ -116,131 +138,242 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
|
||||
taskRangeFunc = getRangeFunc(dir)
|
||||
}
|
||||
|
||||
// When scoped includes is enabled, resolve vars from DAG instead of merged vars
|
||||
if scopedMode {
|
||||
// Get root Taskfile for inheritance (parent vars are always accessible)
|
||||
rootVertex, err := c.Graph.Root()
|
||||
// Get root Taskfile for inheritance (parent vars are always accessible)
|
||||
rootVertex, err := c.Graph.Root()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// === ENV NAMESPACE ===
|
||||
// Create a separate map for environment variables
|
||||
// Accessible via {{.env.VAR}} in templates
|
||||
envMap := make(map[string]any)
|
||||
|
||||
// 1. OS environment variables
|
||||
for _, e := range os.Environ() {
|
||||
k, v, _ := strings.Cut(e, "=")
|
||||
envMap[k] = v
|
||||
}
|
||||
|
||||
// Helper to resolve env vars and add to envMap
|
||||
resolveEnvToMap := func(k string, v ast.Var, dir string) error {
|
||||
cache := &templater.Cache{Vars: result}
|
||||
newVar := templater.ReplaceVar(v, cache)
|
||||
if err := cache.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Static value
|
||||
if newVar.Value != nil || newVar.Sh == nil {
|
||||
if newVar.Value != nil {
|
||||
envMap[k] = newVar.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Dynamic value (sh:)
|
||||
if evaluateShVars {
|
||||
// Build env slice for sh execution (includes envMap values)
|
||||
envSlice := os.Environ()
|
||||
for ek, ev := range envMap {
|
||||
if s, ok := ev.(string); ok {
|
||||
envSlice = append(envSlice, fmt.Sprintf("%s=%s", ek, s))
|
||||
}
|
||||
}
|
||||
static, err := c.HandleDynamicVar(newVar, dir, envSlice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
envMap[k] = static
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2. Root taskfile env
|
||||
for k, v := range rootVertex.Taskfile.Env.All() {
|
||||
if err := resolveEnvToMap(k, v, c.Dir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// === VARS (at root level) ===
|
||||
// Apply root vars
|
||||
for k, v := range rootVertex.Taskfile.Vars.All() {
|
||||
if err := rangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// If task is from an included Taskfile, traverse the parent chain to collect vars
|
||||
if t.Location.Taskfile != rootVertex.URI {
|
||||
predecessorMap, err := c.Graph.PredecessorMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// === ENV NAMESPACE ===
|
||||
// Create a separate map for environment variables
|
||||
// Accessible via {{.env.VAR}} in templates
|
||||
envMap := make(map[string]any)
|
||||
|
||||
// 1. OS environment variables
|
||||
for _, e := range os.Environ() {
|
||||
k, v, _ := strings.Cut(e, "=")
|
||||
envMap[k] = v
|
||||
}
|
||||
|
||||
// Helper to resolve env vars and add to envMap
|
||||
resolveEnvToMap := func(k string, v ast.Var, dir string) error {
|
||||
cache := &templater.Cache{Vars: result}
|
||||
newVar := templater.ReplaceVar(v, cache)
|
||||
if err := cache.Err(); err != nil {
|
||||
return err
|
||||
// Build parent chain (excluding root, already applied)
|
||||
var parentChain []*ast.TaskfileVertex
|
||||
currentURI := t.Location.Taskfile
|
||||
for {
|
||||
edges := predecessorMap[currentURI]
|
||||
if len(edges) == 0 {
|
||||
break
|
||||
}
|
||||
// Static value
|
||||
if newVar.Value != nil || newVar.Sh == nil {
|
||||
if newVar.Value != nil {
|
||||
envMap[k] = newVar.Value
|
||||
}
|
||||
return nil
|
||||
var parentURI string
|
||||
for _, edge := range edges {
|
||||
parentURI = edge.Source
|
||||
break
|
||||
}
|
||||
// Dynamic value (sh:)
|
||||
if evaluateShVars {
|
||||
// Build env slice for sh execution (includes envMap values)
|
||||
envSlice := os.Environ()
|
||||
for ek, ev := range envMap {
|
||||
if s, ok := ev.(string); ok {
|
||||
envSlice = append(envSlice, fmt.Sprintf("%s=%s", ek, s))
|
||||
}
|
||||
}
|
||||
static, err := c.HandleDynamicVar(newVar, dir, envSlice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
envMap[k] = static
|
||||
if parentURI == rootVertex.URI {
|
||||
break
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2. Root taskfile env
|
||||
for k, v := range rootVertex.Taskfile.Env.All() {
|
||||
if err := resolveEnvToMap(k, v, c.Dir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// === VARS (at root level) ===
|
||||
// Apply root vars
|
||||
for k, v := range rootVertex.Taskfile.Vars.All() {
|
||||
if err := rangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// If task is from an included Taskfile (not the root), get its vars from the DAG
|
||||
if t.Location.Taskfile != rootVertex.URI {
|
||||
includeVertex, err := c.Graph.Vertex(t.Location.Taskfile)
|
||||
parentVertex, err := c.Graph.Vertex(parentURI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Apply include's env to envMap (overrides root's env)
|
||||
for k, v := range includeVertex.Taskfile.Env.All() {
|
||||
if err := resolveEnvToMap(k, v, filepathext.SmartJoin(c.Dir, t.Dir)); err != nil {
|
||||
parentChain = append([]*ast.TaskfileVertex{parentVertex}, parentChain...)
|
||||
currentURI = parentURI
|
||||
}
|
||||
|
||||
// Apply parent chain env and vars
|
||||
for _, parent := range parentChain {
|
||||
// Use the parent's directory for resolving dynamic env vars
|
||||
parentDir := filepath.Dir(parent.URI)
|
||||
for k, v := range parent.Taskfile.Env.All() {
|
||||
if err := resolveEnvToMap(k, v, parentDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Apply include's vars (overrides root's vars)
|
||||
for k, v := range includeVertex.Taskfile.Vars.All() {
|
||||
if err := taskRangeFunc(k, v); err != nil {
|
||||
// Vars use the parent's directory too
|
||||
parentRangeFunc := getRangeFunc(parentDir)
|
||||
for k, v := range parent.Taskfile.Vars.All() {
|
||||
if err := parentRangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply IncludeVars (vars passed via includes: section)
|
||||
if t.IncludeVars != nil {
|
||||
for k, v := range t.IncludeVars.All() {
|
||||
if err := rangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Apply direct include's env and vars
|
||||
includeVertex, err := c.Graph.Vertex(t.Location.Taskfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Use the include's directory for resolving dynamic env/vars
|
||||
includeDir := filepath.Dir(includeVertex.URI)
|
||||
for k, v := range includeVertex.Taskfile.Env.All() {
|
||||
if err := resolveEnvToMap(k, v, includeDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Apply task-level vars
|
||||
if call != nil {
|
||||
for k, v := range t.Vars.All() {
|
||||
if err := taskRangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Apply call vars (vars passed when calling a task)
|
||||
for k, v := range call.Vars.All() {
|
||||
if err := taskRangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
includeRangeFunc := getRangeFunc(includeDir)
|
||||
for k, v := range includeVertex.Taskfile.Vars.All() {
|
||||
if err := includeRangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CLI vars have highest priority - applied last to override everything
|
||||
for k, v := range c.CLIVars.All() {
|
||||
// Apply IncludeVars (vars passed via includes: section)
|
||||
if t.IncludeVars != nil {
|
||||
for k, v := range t.IncludeVars.All() {
|
||||
if err := rangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Inject env namespace into result
|
||||
result.Set("env", ast.Var{Value: envMap})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// === LEGACY MODE ===
|
||||
// Legacy behavior: use merged vars
|
||||
// Apply task-level vars
|
||||
if call != nil {
|
||||
for k, v := range t.Vars.All() {
|
||||
if err := taskRangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Apply call vars (vars passed when calling a task)
|
||||
for k, v := range call.Vars.All() {
|
||||
if err := taskRangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CLI vars have highest priority - applied last to override everything
|
||||
for k, v := range c.CLIVars.All() {
|
||||
if err := rangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Inject env namespace into result
|
||||
result.Set("env", ast.Var{Value: envMap})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getLegacyVariables resolves variables in legacy mode.
|
||||
// In legacy mode, all variables (including OS env) are merged at root level.
|
||||
func (c *Compiler) getLegacyVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) {
|
||||
result := env.GetEnviron()
|
||||
|
||||
// Add special variables (TASK, ROOT_DIR, etc.)
|
||||
specialVars, err := c.getSpecialVars(t, call)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range specialVars {
|
||||
result.Set(k, ast.Var{Value: v})
|
||||
}
|
||||
|
||||
// Create range function for resolving vars in a given directory
|
||||
// NOTE: This closure captures result directly - do not refactor to method call
|
||||
getRangeFunc := func(dir string) func(k string, v ast.Var) error {
|
||||
return func(k string, v ast.Var) error {
|
||||
cache := &templater.Cache{Vars: result}
|
||||
// Replace values
|
||||
newVar := templater.ReplaceVar(v, cache)
|
||||
// If the variable should not be evaluated, but is nil, set it to an empty string
|
||||
// This stops empty interface errors when using the templater to replace values later
|
||||
// Preserve the Sh field so it can be displayed in summary
|
||||
if !evaluateShVars && newVar.Value == nil {
|
||||
result.Set(k, ast.Var{Value: "", Sh: newVar.Sh})
|
||||
return nil
|
||||
}
|
||||
// If the variable should not be evaluated and it is set, we can set it and return
|
||||
if !evaluateShVars {
|
||||
result.Set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh})
|
||||
return nil
|
||||
}
|
||||
// Now we can check for errors since we've handled all the cases when we don't want to evaluate
|
||||
if err := cache.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
// If the variable is already set, we can set it and return
|
||||
if newVar.Value != nil || newVar.Sh == nil {
|
||||
result.Set(k, ast.Var{Value: newVar.Value})
|
||||
return nil
|
||||
}
|
||||
// If the variable is dynamic, we need to resolve it first
|
||||
static, err := c.HandleDynamicVar(newVar, dir, env.GetFromVars(result))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result.Set(k, ast.Var{Value: static})
|
||||
return nil
|
||||
}
|
||||
}
|
||||
rangeFunc := getRangeFunc(c.Dir)
|
||||
|
||||
// Create task-specific range function if we have a task
|
||||
var taskRangeFunc func(k string, v ast.Var) error
|
||||
if t != nil {
|
||||
cache := &templater.Cache{Vars: result}
|
||||
dir := templater.Replace(t.Dir, cache)
|
||||
if err := cache.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dir = filepathext.SmartJoin(c.Dir, dir)
|
||||
taskRangeFunc = getRangeFunc(dir)
|
||||
}
|
||||
|
||||
// Apply merged env and vars from all taskfiles
|
||||
for k, v := range c.TaskfileEnv.All() {
|
||||
if err := rangeFunc(k, v); err != nil {
|
||||
return nil, err
|
||||
@@ -251,6 +384,7 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if t != nil {
|
||||
for k, v := range t.IncludeVars.All() {
|
||||
if err := rangeFunc(k, v); err != nil {
|
||||
|
||||
@@ -1281,5 +1281,15 @@ func TestScopedTaskfiles(t *testing.T) {
|
||||
),
|
||||
WithTask("call-with-vars"),
|
||||
)
|
||||
// Test nested includes (3 levels: root → a → nested)
|
||||
// Verifies that nested includes inherit vars from their parent chain
|
||||
NewExecutorTest(t,
|
||||
WithName("nested"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/scoped_taskfiles"),
|
||||
task.WithSilent(true),
|
||||
),
|
||||
WithTask("a:nested:print"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
4
testdata/scoped_taskfiles/inc_a/Taskfile.yml
vendored
4
testdata/scoped_taskfiles/inc_a/Taskfile.yml
vendored
@@ -8,11 +8,13 @@ vars:
|
||||
VAR: value_from_a
|
||||
UNIQUE_A: only_in_a
|
||||
|
||||
includes:
|
||||
nested: ./nested
|
||||
|
||||
tasks:
|
||||
print:
|
||||
desc: Print vars from include A
|
||||
cmds:
|
||||
- echo "A:VAR={{.VAR}}"
|
||||
- echo "A:UNIQUE_A={{.UNIQUE_A}}"
|
||||
- echo "A:ROOT_VAR={{.ROOT_VAR}}"
|
||||
|
||||
|
||||
22
testdata/scoped_taskfiles/inc_a/nested/Taskfile.yml
vendored
Normal file
22
testdata/scoped_taskfiles/inc_a/nested/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
version: "3"
|
||||
|
||||
env:
|
||||
NESTED_ENV: env_from_nested
|
||||
|
||||
vars:
|
||||
NESTED_VAR: from_nested
|
||||
|
||||
tasks:
|
||||
print:
|
||||
desc: Print vars from nested include (3 levels deep)
|
||||
cmds:
|
||||
- echo "NESTED:ROOT_VAR={{.ROOT_VAR}}"
|
||||
- echo "NESTED:UNIQUE_A={{.UNIQUE_A}}"
|
||||
- echo "NESTED:NESTED_VAR={{.NESTED_VAR}}"
|
||||
- echo "NESTED:NESTED_ENV={{.env.NESTED_ENV}}"
|
||||
- echo "NESTED:ROOT_ENV={{.env.ROOT_ENV}}"
|
||||
|
||||
try-access-b:
|
||||
desc: Try to access B's unique var (should fail - sibling isolation)
|
||||
cmds:
|
||||
- echo "NESTED:UNIQUE_B={{.UNIQUE_B}}"
|
||||
1
testdata/scoped_taskfiles/inc_b/Taskfile.yml
vendored
1
testdata/scoped_taskfiles/inc_b/Taskfile.yml
vendored
@@ -12,7 +12,6 @@ tasks:
|
||||
print:
|
||||
desc: Print vars from include B
|
||||
cmds:
|
||||
- echo "B:VAR={{.VAR}}"
|
||||
- echo "B:UNIQUE_B={{.UNIQUE_B}}"
|
||||
- echo "B:ROOT_VAR={{.ROOT_VAR}}"
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
A:VAR=value_from_b
|
||||
A:UNIQUE_A=only_in_a
|
||||
A:ROOT_VAR=from_root
|
||||
B:VAR=value_from_b
|
||||
B:UNIQUE_B=only_in_b
|
||||
B:ROOT_VAR=from_root
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
A:VAR=value_from_a
|
||||
A:UNIQUE_A=only_in_a
|
||||
A:ROOT_VAR=from_root
|
||||
B:VAR=value_from_b
|
||||
B:UNIQUE_B=only_in_b
|
||||
B:ROOT_VAR=from_root
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
A:VAR=value_from_a
|
||||
A:UNIQUE_A=only_in_a
|
||||
A:ROOT_VAR=from_root
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
B:VAR=value_from_b
|
||||
B:UNIQUE_B=only_in_b
|
||||
B:ROOT_VAR=from_root
|
||||
|
||||
5
testdata/scoped_taskfiles/testdata/TestScopedTaskfiles-scoped-nested.golden
vendored
Normal file
5
testdata/scoped_taskfiles/testdata/TestScopedTaskfiles-scoped-nested.golden
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
NESTED:ROOT_VAR=from_root
|
||||
NESTED:UNIQUE_A=only_in_a
|
||||
NESTED:NESTED_VAR=from_nested
|
||||
NESTED:NESTED_ENV=env_from_nested
|
||||
NESTED:ROOT_ENV=env_from_root
|
||||
@@ -225,3 +225,57 @@ To migrate your Taskfiles to use this experiment:
|
||||
- If your included Taskfiles rely on variables from sibling includes, you'll
|
||||
need to either move those variables to the root Taskfile or pass them
|
||||
explicitly via the `vars:` attribute in the `includes:` section.
|
||||
|
||||
5. **Use `flatten: true` for gradual migration**:
|
||||
- If an include needs the legacy behavior (access to sibling variables), you
|
||||
can use `flatten: true` on that include as an escape hatch.
|
||||
|
||||
## Using `flatten: true`
|
||||
|
||||
The `flatten: true` option on includes bypasses scoping for that specific
|
||||
include. When an include has `flatten: true`:
|
||||
|
||||
- Its variables are merged globally (legacy behavior)
|
||||
- It can access variables from sibling includes
|
||||
- Sibling includes can access its variables
|
||||
|
||||
This is useful for gradual migration or when you have includes that genuinely
|
||||
need to share variables.
|
||||
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
ROOT_VAR: from_root
|
||||
|
||||
includes:
|
||||
# Scoped include - isolated from siblings
|
||||
api:
|
||||
taskfile: ./api
|
||||
|
||||
# Flattened include - uses legacy merge behavior
|
||||
shared:
|
||||
taskfile: ./shared
|
||||
flatten: true
|
||||
|
||||
# Another scoped include
|
||||
web:
|
||||
taskfile: ./web
|
||||
```
|
||||
|
||||
In this example:
|
||||
|
||||
- `api` and `web` are isolated from each other (cannot see each other's vars)
|
||||
- `shared` uses legacy behavior: its vars are merged globally
|
||||
- Both `api` and `web` can access variables from `shared`
|
||||
- `shared` can access variables from `api` and `web`
|
||||
|
||||
::: tip
|
||||
|
||||
Use `flatten: true` sparingly. The goal of scoped taskfiles is to improve
|
||||
isolation and predictability. Flattening should be a temporary measure during
|
||||
migration or for utility includes that genuinely need global scope.
|
||||
|
||||
:::
|
||||
|
||||
Reference in New Issue
Block a user