@@ -9,8 +9,12 @@ import (
99
1010 "github.com/gruntwork-io/terragrunt/internal/component"
1111 "github.com/gruntwork-io/terragrunt/internal/errors"
12+ intHclparse "github.com/gruntwork-io/terragrunt/internal/hclparse"
1213 "github.com/gruntwork-io/terragrunt/internal/util"
1314 "github.com/gruntwork-io/terragrunt/pkg/config"
15+ "github.com/hashicorp/hcl/v2"
16+ "github.com/hashicorp/hcl/v2/hclsyntax"
17+ "github.com/zclconf/go-cty/cty"
1418)
1519
1620const (
@@ -220,6 +224,8 @@ func sanitizeReadFiles(files []string) []string {
220224}
221225
222226// extractDependencyPaths extracts all dependency paths from a Terragrunt configuration.
227+ // It also checks for terragrunt.autoinclude.hcl in the same directory and extracts
228+ // dependency config_path values from it, so the DAG correctly orders units.
223229func extractDependencyPaths (cfg * config.TerragruntConfig , c component.Component ) ([]string , error ) {
224230 if cfg == nil {
225231 return nil , nil
@@ -232,6 +238,12 @@ func extractDependencyPaths(cfg *config.TerragruntConfig, c component.Component)
232238
233239 deduped := make (map [string ]struct {}, maxDedupLen )
234240
241+ // Check for autoinclude file and extract its dependency paths for the DAG.
242+ autoIncludeDeps := ExtractAutoIncludeDependencyPaths (c .Path ())
243+ for _ , dep := range autoIncludeDeps {
244+ deduped [dep ] = struct {}{}
245+ }
246+
235247 errs := make ([]error , 0 , maxDedupLen )
236248
237249 for _ , dependency := range cfg .TerragruntDependencies {
@@ -265,8 +277,16 @@ func extractDependencyPaths(cfg *config.TerragruntConfig, c component.Component)
265277 }
266278
267279 depPaths := make ([]string , 0 , len (deduped ))
280+
268281 for depPath := range deduped {
269- depPaths = append (depPaths , depPath )
282+ // When the stack-dependencies experiment is active and the dependency
283+ // path points to a stack (directory with terragrunt.stack.hcl), expand
284+ // it to all constituent unit paths so the DAG correctly blocks on each unit.
285+ if expandedPaths := ExpandStackDependency (depPath ); len (expandedPaths ) > 0 {
286+ depPaths = append (depPaths , expandedPaths ... )
287+ } else {
288+ depPaths = append (depPaths , depPath )
289+ }
270290 }
271291
272292 if len (errs ) > 0 {
@@ -275,3 +295,101 @@ func extractDependencyPaths(cfg *config.TerragruntConfig, c component.Component)
275295
276296 return depPaths , nil
277297}
298+
299+ // ExpandStackDependency checks if a dependency path points to a stack directory.
300+ // If so, it reads the stack config and returns paths to all generated units
301+ // within .terragrunt-stack/. Returns nil if not a stack.
302+ func ExpandStackDependency (depPath string ) []string {
303+ stackFile := filepath .Join (depPath , config .DefaultStackFile )
304+
305+ if ! util .FileExists (stackFile ) {
306+ return nil
307+ }
308+
309+ // Read the stack file to discover unit paths.
310+ // We only need the raw HCL to extract unit path attributes — no eval context needed.
311+ data , err := os .ReadFile (stackFile )
312+ if err != nil {
313+ return nil
314+ }
315+
316+ unitPaths := ExtractUnitPathsFromStackFile (data , depPath )
317+
318+ return unitPaths
319+ }
320+
321+ // ExtractUnitPathsFromStackFile parses a stack file and returns absolute paths
322+ // to each unit's generated directory under .terragrunt-stack/.
323+ func ExtractUnitPathsFromStackFile (data []byte , stackDir string ) []string {
324+ // Use a minimal HCL parse to extract unit blocks and their path attributes.
325+ // We import the internal hclparse package which can handle this.
326+ result , err := intHclparse .ParseStackFile (data , filepath .Join (stackDir , config .DefaultStackFile ), stackDir , nil )
327+ if err != nil {
328+ return nil
329+ }
330+
331+ paths := make ([]string , 0 , len (result .Units ))
332+
333+ for _ , unit := range result .Units {
334+ unitPath := filepath .Join (stackDir , config .StackDir , unit .Path )
335+ paths = append (paths , unitPath )
336+ }
337+
338+ return paths
339+ }
340+
341+ // ExtractAutoIncludeDependencyPaths checks for a terragrunt.autoinclude.hcl file
342+ // in the given unit directory and extracts dependency config_path values.
343+ // This ensures the DAG sees dependencies defined in autoinclude files during
344+ // graph construction, even though they're not in the main terragrunt.hcl.
345+ func ExtractAutoIncludeDependencyPaths (unitDir string ) []string {
346+ autoIncludePath := filepath .Join (unitDir , config .DefaultAutoIncludeFile )
347+
348+ if ! util .FileExists (autoIncludePath ) {
349+ return nil
350+ }
351+
352+ data , err := os .ReadFile (autoIncludePath )
353+ if err != nil {
354+ return nil
355+ }
356+
357+ // Minimal HCL parse — just extract dependency blocks and their config_path.
358+ file , diags := hclsyntax .ParseConfig (data , autoIncludePath , hcl.Pos {Line : 1 , Column : 1 })
359+ if diags .HasErrors () {
360+ return nil
361+ }
362+
363+ body , ok := file .Body .(* hclsyntax.Body )
364+ if ! ok {
365+ return nil
366+ }
367+
368+ var paths []string
369+
370+ for _ , block := range body .Blocks {
371+ if block .Type != "dependency" || len (block .Labels ) == 0 {
372+ continue
373+ }
374+
375+ configPathAttr , exists := block .Body .Attributes ["config_path" ]
376+ if ! exists {
377+ continue
378+ }
379+
380+ // Evaluate config_path — it's already a resolved string literal in the generated file.
381+ val , valDiags := configPathAttr .Expr .Value (nil )
382+ if valDiags .HasErrors () || val .Type () != cty .String {
383+ continue
384+ }
385+
386+ depPath := val .AsString ()
387+ if ! filepath .IsAbs (depPath ) {
388+ depPath = filepath .Clean (filepath .Join (unitDir , depPath ))
389+ }
390+
391+ paths = append (paths , util .ResolvePath (depPath ))
392+ }
393+
394+ return paths
395+ }
0 commit comments