Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f42b149
chore: stack parsing improvements
denis256 Jun 1, 2026
aac743d
chore: edge partial configs
denis256 Jun 1, 2026
73fb87e
Merge remote-tracking branch 'origin/main' into stacks-edge-cases
denis256 Jun 1, 2026
743ced1
chore: adding depednecnies merging changes
denis256 Jun 1, 2026
2f17c86
chore: added passing of feature flags
denis256 Jun 1, 2026
1ee7c0e
chore: mock merge test
denis256 Jun 1, 2026
5b3992e
chore: auto include merge fixes
denis256 Jun 1, 2026
00ca27f
chore: improved autoinclude handling
denis256 Jun 1, 2026
e533c97
chore: PR review notes
denis256 Jun 1, 2026
8d1f50e
chore: PR review comments
denis256 Jun 2, 2026
163bb71
chore: autoinclude simplifications
denis256 Jun 2, 2026
c9076c8
tests improvements
denis256 Jun 2, 2026
8ca3675
chore: changelog cleanup
denis256 Jun 2, 2026
a825ed6
chore: auto include simplification
denis256 Jun 2, 2026
f90f198
chore: partial config fixes
denis256 Jun 2, 2026
3d4473b
chore: autoinclude test fix
denis256 Jun 2, 2026
41b8d8c
chore: added fuzz tests
denis256 Jun 2, 2026
a19c327
chore: updated merge include issues
denis256 Jun 2, 2026
223d8b0
chore: PR review
denis256 Jun 2, 2026
4ad9aea
chore: handling of autoincluded dependencies
denis256 Jun 2, 2026
7ffb6f5
chore: autoinclude priority fixes
denis256 Jun 2, 2026
fdefabe
chore: autoinclude fix
denis256 Jun 2, 2026
9f73e8a
chore: autoinclude tests cleanup
denis256 Jun 2, 2026
70a9db9
chore: mocks fixes
denis256 Jun 2, 2026
b41048f
Merge remote-tracking branch 'origin/main' into stacks-edge-cases
denis256 Jun 2, 2026
da4fe1b
chore: fixed auto include for stacks
denis256 Jun 3, 2026
9837f2a
chore: pr comments
denis256 Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
version: "v1.0.8"
category: "experiments-updated"
---

#### `stack-dependencies`: `autoinclude` merges like a regular include

A generated autoinclude file (`terragrunt.autoinclude.hcl` for units, `terragrunt.autoinclude.stack.hcl` for stacks) now merges into the unit or stack using the same default merge as a regular `include`, which is a shallow merge, applied uniformly across generation, full parse, and discovery. Top-level keys from the unit and the autoinclude combine, and on a conflict the autoinclude wins and replaces the unit's value rather than deep-merging nested maps. The autoinclude is the winning side on conflicts, and `locals` stay local in scope.

A `dependency` block injected through an `autoinclude` is now available before a unit's `remote_state` is evaluated, so referencing `dependency.<name>.outputs.<key>` there no longer fails. `remote_state` now behaves the same as `generate` blocks.
2 changes: 1 addition & 1 deletion internal/discovery/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ func (d *Discovery) buildComponentDependencies(
}

if opts.Experiments.Evaluate(experiment.StackDependencies) {
depPaths, err = stackDependencyPaths(ctx, l, vfs.NewOSFS(), opts, depPaths, c)
depPaths, err = stackDependencyPaths(ctx, l, vfs.NewOSFS(), opts, depPaths)
if err != nil {
return err
}
Expand Down
29 changes: 10 additions & 19 deletions internal/discovery/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/gruntwork-io/terragrunt/pkg/config"
"github.com/gruntwork-io/terragrunt/pkg/log"
"github.com/gruntwork-io/terragrunt/pkg/options"
"github.com/zclconf/go-cty/cty/function"
)

const (
Expand Down Expand Up @@ -296,29 +297,24 @@ func extractDependencyPaths(cfg *config.TerragruntConfig, c component.Component)
return depPaths, nil
}

// stackDependencyPaths returns additional dependency paths from autoinclude
// files and expands stack directory paths into constituent unit paths.
// stackDependencyPaths expands stack directory dependency paths into their constituent unit paths.
// Autoinclude-declared dependencies are already folded into the parsed config by the partial-parse
// merge (which honors enabled/disabled and same-name override), so they arrive via depPaths here.
// Only called when the StackDependencies experiment is enabled.
func stackDependencyPaths(
ctx context.Context,
l log.Logger,
fs vfs.FS,
opts *options.TerragruntOptions,
depPaths []string,
c component.Component,
) ([]string, error) {
// Add dependencies declared in autoinclude files.
autoIncludeDeps, err := inthclparse.AutoIncludeDependencyPaths(fs, c.Path())
if err != nil {
return nil, err
}

for _, dep := range autoIncludeDeps {
depPaths = append(depPaths, util.ResolvePath(dep))
}

_, pctx := configbridge.NewParsingContext(ctx, l, opts)

// Factory builds the dir-scoped function map for each stack dir visited during expansion.
funcsFor := inthclparse.StackFuncFactory(func(stackDir string) (map[string]function.Function, error) {
return config.EarlyStackParseFunctions(ctx, l, stackDir, pctx)
})

// Expand stack dependency paths to individual unit paths.
expanded := make([]string, 0, len(depPaths))

Expand All @@ -338,12 +334,7 @@ func stackDependencyPaths(
continue
}

discoveryFuncs, fnErr := config.EarlyStackParseFunctions(ctx, l, depPath, pctx)
if fnErr != nil {
return nil, NewStackDependencyExpansionError(depPath, fnErr)
}

unitPaths, err := inthclparse.UnitPathsFromStackDir(fs, depPath, discoveryFuncs)
unitPaths, err := inthclparse.UnitPathsFromStackDir(fs, depPath, funcsFor)
if err != nil {
return nil, NewStackDependencyExpansionError(depPath, err)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/discovery/phase_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ func (p *GraphPhase) discoverDependencies(
}

if state.opts.Experiments.Evaluate(experiment.StackDependencies) {
depPaths, err = stackDependencyPaths(ctx, l, vfs.NewOSFS(), state.opts, depPaths, c)
depPaths, err = stackDependencyPaths(ctx, l, vfs.NewOSFS(), state.opts, depPaths)
if err != nil {
return err
}
Expand Down Expand Up @@ -667,7 +667,7 @@ func (p *GraphPhase) processUpstreamCandidate(
if state.graphTraversalState.opts.Experiments.Evaluate(experiment.StackDependencies) {
var stackErr error

deps, stackErr = stackDependencyPaths(ctx, l, vfs.NewOSFS(), state.graphTraversalState.opts, deps, candidate)
deps, stackErr = stackDependencyPaths(ctx, l, vfs.NewOSFS(), state.graphTraversalState.opts, deps)
if stackErr != nil {
state.errMu.Lock()
*state.errs = append(*state.errs, stackErr)
Expand Down
2 changes: 1 addition & 1 deletion internal/discovery/phase_relationship.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func (p *RelationshipPhase) discoverRelationships(
}

if state.opts.Experiments.Evaluate(experiment.StackDependencies) {
paths, err = stackDependencyPaths(ctx, l, vfs.NewOSFS(), state.opts, paths, c)
paths, err = stackDependencyPaths(ctx, l, vfs.NewOSFS(), state.opts, paths)
if err != nil {
return err
}
Expand Down
70 changes: 70 additions & 0 deletions internal/hclparse/autoinclude.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ type AutoIncludeDependency struct {
// They contain dependency.*.outputs.* which is runtime-only.
// The RawBody is preserved so the generator can copy these from the AST.
func (a *AutoIncludeHCL) Resolve(evalCtx *hcl.EvalContext) (*AutoIncludeResolved, hcl.Diagnostics) {
return a.ResolveForKind(evalCtx, KindUnit, "")
}

// ResolveForKind is Resolve with the component kind and parent name known, so a
// stack-level autoinclude can be validated against the unsupported pattern where
// an injected unit/stack consumes a sibling dependency's outputs through values.
func (a *AutoIncludeHCL) ResolveForKind(evalCtx *hcl.EvalContext, kind AutoIncludeKind, name string) (*AutoIncludeResolved, hcl.Diagnostics) {
if a == nil || a.Remain == nil {
return nil, nil
}
Expand All @@ -105,6 +112,12 @@ func (a *AutoIncludeHCL) Resolve(evalCtx *hcl.EvalContext) (*AutoIncludeResolved
}}
}

if kind == KindStack {
if diags := validateStackAutoIncludeDepValues(body, name); diags.HasErrors() {
return nil, diags
}
}

deps := make([]AutoIncludeDependency, 0, len(body.Blocks))

var diags hcl.Diagnostics
Expand Down Expand Up @@ -317,3 +330,60 @@ func extractDepPath(block *hclsyntax.Block, autoIncludePath, unitDir string) (st

return util.ResolvePath(depPath), nil
}

// StackAutoIncludeDepValuesError scans a stack autoinclude body for the unsupported cross-level pattern: an injected unit/stack whose values reference dependency outputs, which are not available at stack generate time. Returns the populated typed error, or nil when absent. Shared by the fail-fast generation check and the pkg/config backstop so the two cannot drift.
func StackAutoIncludeDepValuesError(body *hclsyntax.Body, stackName string) *StackAutoIncludeDependencyValuesError {
for _, block := range body.Blocks {
if block.Type != VarUnit && block.Type != VarStack {
continue
}

valuesAttr, hasValues := block.Body.Attributes[varValues]
if !hasValues {
continue
}

if !valuesReferenceDependency(valuesAttr.Expr) {
continue
}

return &StackAutoIncludeDependencyValuesError{
StackName: stackName,
UnitName: blockLabelsString(block),
Subject: valuesAttr.Expr.Range().Ptr(),
}
}

return nil
}

// validateStackAutoIncludeDepValues rejects a stack-level autoinclude that declares a dependency block whose outputs are referenced by the values of an injected unit/stack block.
func validateStackAutoIncludeDepValues(body *hclsyntax.Body, stackName string) hcl.Diagnostics {
typed := StackAutoIncludeDepValuesError(body, stackName)
if typed == nil {
return nil
}

return hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: "stack autoinclude dependency outputs referenced by injected values",
Detail: typed.Error(),
Subject: typed.Subject,
Extra: *typed,
}}
}

// valuesReferenceDependency reports whether expr references the dependency namespace in any form.
// RootName matches every traversal uniformly: dependency.foo, dependency["foo"], and the dynamic
// dependency[values.x] all report "dependency" as the root, so no per-form handling is needed. Any such
// reference is unsupported because injected values are evaluated at stack generate time, when no
// dependency outputs exist, regardless of whether the autoinclude declares that dependency.
func valuesReferenceDependency(expr hclsyntax.Expression) bool {
for _, traversal := range expr.Variables() {
if traversal.RootName() == varDependency {
return true
}
}

return false
}
Loading
Loading