Skip to content

Conversation

@vmaerten
Copy link
Member

@vmaerten vmaerten commented Jan 25, 2026

Summary

This PR introduces the TASK_X_SCOPED_TASKFILES experiment which fundamentally changes how variables work in included Taskfiles. The goal is to eliminate variable conflicts and unexpected leakage between includes.

related to #2035

What this experiment does

1. Environment namespace

Environment variables (OS + env: section) move to a dedicated namespace:

# Before (legacy)
- echo "{{.HOME}}"
- echo "{{.MY_ENV}}"

# After (scoped)
- echo "{{.env.HOME}}"
- echo "{{.env.MY_ENV}}"

Special variables like {{.TASK}}, {{.ROOT_DIR}} stay at root level.

2. Variable isolation

Included Taskfiles can only see:

  • Variables from the root Taskfile (inherited)
  • Their own variables
  • Variables explicitly passed via includes: name: vars:

They can NOT see variables from sibling includes anymore.

# api/Taskfile.yml
tasks:
  show:
    cmds:
      - echo "{{.ROOT_VAR}}"  # works (inherited from root)
      - echo "{{.API_VAR}}"   # works (own variable)
      - echo "{{.WEB_VAR}}"   # empty (can't see sibling's vars)

3. Clear priority order

From lowest to highest:

  1. Root Taskfile vars
  2. Included Taskfile vars
  3. Passthrough vars (includes: foo: vars:)
  4. Task vars
  5. Call vars (when calling a task with vars:)
  6. CLI vars (task foo VAR=value)

4. Escape hatch: flatten: true

For gradual migration, you can use flatten: true on specific includes to restore legacy behavior:

includes:
  api: ./api           # scoped (default)
  shared:
    taskfile: ./shared
    flatten: true      # legacy merge behavior

Breaking changes

  • {{.HOME}}{{.env.HOME}} for all env vars
  • Sibling includes can't see each other's variables anymore

@vmaerten vmaerten changed the title feat(experiments): add SCOPED_TASKFILES experiment feat: add SCOPED_TASKFILES experiment Jan 25, 2026
@vmaerten vmaerten self-assigned this Jan 25, 2026
Add new experiment flag TASK_X_SCOPED_INCLUDES for scoped variable
resolution in included Taskfiles. When enabled, variables from included
Taskfiles will be isolated rather than merged globally.

This is the first step towards implementing lazy DAG-based variable
resolution with strict isolation between includes.
Store the TaskfileGraph in the Executor so it can be used for lazy
variable resolution when SCOPED_INCLUDES experiment is enabled.

The graph is now preserved after reading, before merging into the
final Taskfile. This allows traversing the DAG at runtime to resolve
variables from the correct scope.
Add Root() method to TaskfileGraph to get the root vertex (entrypoint
Taskfile). This will be used for lazy variable resolution.

Note: Tasks already have Location.Taskfile which can be used to find
their source Taskfile in the graph, so GetVertexByNamespace is not
needed.
When the SCOPED_INCLUDES experiment is enabled, variables from included
Taskfiles are no longer merged globally. They remain in their original
Taskfile within the DAG.

Exception: flatten includes still merge variables globally to allow
sharing common variables across multiple Taskfiles.
When SCOPED_INCLUDES experiment is enabled:
- Resolve vars from DAG instead of merged vars
- Apply root Taskfile vars first (inheritance)
- Then apply task's source Taskfile vars from DAG
- Apply IncludeVars passed via includes: section
- Skip IncludedTaskfileVars (contains parent's vars, not source's)

This ensures tasks in included Taskfiles see:
1. Root vars (inheritance from parent)
2. Their own Taskfile's vars
3. Vars passed through includes: section
4. Call vars and task-level vars
Tests verify:
- Legacy mode: vars merged globally (A sees B's VAR, can access UNIQUE_B)
- Scoped mode: vars isolated (A sees own VAR, cannot access UNIQUE_B)
- Inheritance: includes can still access root vars (ROOT_VAR)

Test structure:
- testdata/scoped_includes/ with main Taskfile and two includes
- inc_a and inc_b both define VAR with different values
- Cross-include test shows A trying to access B's UNIQUE_B
… env namespace

Rename the experiment from SCOPED_INCLUDES to SCOPED_TASKFILES to better
reflect its expanded scope. This experiment now provides:

1. Variable scoping (existing): includes see only their own vars + parent vars
2. Environment namespace (new): env vars accessible via {{.env.XXX}}

With TASK_X_SCOPED_TASKFILES=1:
- {{.VAR}} accesses vars only (scoped per include)
- {{.env.VAR}} accesses env (OS + Taskfile env:, inherited)
- {{.TASK}} and other special vars remain at root level

This is a breaking change for the experimental feature:
- {{.PATH}} no longer works, use {{.env.PATH}} instead
- Env vars are no longer at root level in templates
In scoped mode, CLI vars (e.g., `task foo VAR=value`) now correctly
override task-level vars. This is achieved by:

1. Adding a `CLIVars` field to the Compiler struct
2. Storing CLI globals in this field after parsing
3. Applying CLI vars last in scoped mode to ensure they override everything

The order of variable resolution in scoped mode is now:
1. OS env → {{.env.XXX}}
2. Root taskfile env → {{.env.XXX}}
3. Root taskfile vars → {{.VAR}}
4. Include taskfile env/vars (if applicable)
5. IncludeVars (vars passed via includes: section)
6. Task-level vars
7. CLI vars (highest priority)

Legacy mode behavior is unchanged.
Document the new experiment with:
- Environment namespace ({{.env.XXX}}) explanation
- Variable scoping between includes
- CLI variables priority
- Migration guide from legacy mode
- Comparison table between legacy and scoped modes
When calling a task with vars (e.g., `task: name` with `vars:`),
those vars were not being applied in scoped mode. This fix adds
call.Vars to the variable resolution chain.

Variable priority (lowest to highest):
1. Root Taskfile vars
2. Include Taskfile vars
3. Include passthrough vars
4. Task vars
5. Call vars (NEW)
6. CLI vars
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
@task-bot
Copy link
Collaborator

task-bot commented Jan 25, 2026

📦 Build artifacts ready!

Download binaries from this workflow run.

Available platforms: Linux, macOS, Windows (amd64, arm64)

- Fix import order in setup.go (gci)
- Fix variable alignment in experiments.go (gofmt)
- Add nolint:paralleltest directive for TestScopedTaskfiles
@vmaerten vmaerten marked this pull request as ready for review January 25, 2026 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants