@@ -12,6 +12,9 @@ import (
1212 intHclparse "github.com/gruntwork-io/terragrunt/internal/hclparse"
1313 "github.com/gruntwork-io/terragrunt/internal/util"
1414 "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"
1518)
1619
1720const (
@@ -221,6 +224,8 @@ func sanitizeReadFiles(files []string) []string {
221224}
222225
223226// 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.
224229func extractDependencyPaths (cfg * config.TerragruntConfig , c component.Component ) ([]string , error ) {
225230 if cfg == nil {
226231 return nil , nil
@@ -233,6 +238,12 @@ func extractDependencyPaths(cfg *config.TerragruntConfig, c component.Component)
233238
234239 deduped := make (map [string ]struct {}, maxDedupLen )
235240
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+
236247 errs := make ([]error , 0 , maxDedupLen )
237248
238249 for _ , dependency := range cfg .TerragruntDependencies {
@@ -326,3 +337,59 @@ func ExtractUnitPathsFromStackFile(data []byte, stackDir string) []string {
326337
327338 return paths
328339}
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