Repository: gruntwork-io/terragrunt

Issue:
`find_in_parent_folders` fails in Explicit Stacks when run from outside stack directory
## Describe the bug

When using **Explicit Stacks** in Terragrunt, `find_in_parent_folders` behaves differently depending on the directory the `terragrunt stack run plan` command is run from. Running the command in the same directory as the `terragrunt.stack.hcl` file works, but running it from a different directory (e.g., project root) fails with a `ParentFileNotFoundError` for `region.hcl`.

This issue makes it difficult to work with Explicit Stacks in a flexible way compared to Implicit Stacks.

## Steps To Reproduce

1. Create a `terragrunt.stack.hcl` file containing:

```hcl
locals {
  regions_vars = read_terragrunt_config(find_in_parent_folders("region.hcl"))
  region       = local.regions_vars.locals.aws_region
}
```

2. Ensure `region.hcl` exists in a parent folder of `terragrunt.stack.hcl`.

3. Run:

```bash
cd ./XXX
terragrunt run plan --all
```

✅ This works.

4. Run:

```bash
cd /path/to/project/root
terragrunt run plan --all
```

❌ This fails with:

```
Call to function "find_in_parent_folders" failed: ParentFileNotFoundError: Could not find a region.hcl in any of the parent folders of ./terragrunt.hcl. Cause: Traversed all the way to the root..
```

## Expected behavior

`find_in_parent_folders` should be evaluated relative to the `terragrunt.stack.hcl` file’s location, regardless of the working directory from which `terragrunt stack run` is executed.

## Nice to haves

* [x] Terminal output

<details>
<summary>Click to expand output</summary>

```
18:28:40.042 INFO   Generating stack from ./XXX/terragrunt.stack.hcl
18:28:40.044 ERROR  Error: Error in function call
18:28:40.044 ERROR    on ./XXX/terragrunt.stack.hcl line 2, in locals:
18:28:40.044 ERROR     2:   regions_vars           = read_terragrunt_config(find_in_parent_folders("region.hcl"))
18:28:40.044 ERROR  Call to function "find_in_parent_folders" failed: ParentFileNotFoundError: Could not find a region.hcl in any of the parent folders of ./terragrunt.hcl. Cause:
18:28:40.044 ERROR  Traversed all the way to the root..
18:28:40.044 ERROR  Failed to process stack file ./XXX/terragrunt.stack.hcl Failed to read stack file ./XXX/terragrunt.stack.hcl in ./XXX error occurred:

* ./XXX/terragrunt.stack.hcl:2,51-74: Error in function call; Call to function "find_in_parent_folders" failed: ParentFileNotFoundError: Could not find a region.hcl in any of the parent folders of ./terragrunt.hcl. Cause: Traversed all the way to the root..

18:28:40.238 ERROR  Unable to determine underlying exit code, so Terragrunt will exit with error code 1
```

</details>

## Versions

* Terragrunt version: v0.84.1
* OpenTofu/Terraform version: v1.12.2


Discussion:

Solution patch:
diff --git a/config/config_helpers.go b/config/config_helpers.go
index 1270b5ef..86278629 100644
--- a/config/config_helpers.go
+++ b/config/config_helpers.go
@@ -272,9 +272,6 @@ func getPathToRepoRoot(ctx *ParsingContext, l log.Logger) (string, error) {
 // GetTerragruntDir returns the directory where the Terragrunt configuration file lives.
 func GetTerragruntDir(ctx *ParsingContext, l log.Logger) (string, error) {
 	path := ctx.TerragruntOptions.TerragruntConfigPath
-	if val, ok := ctx.Context.Value(stackParserContext{}).(stackParserContext); ok {
-		path = val.stackConfigFile
-	}
 
 	terragruntConfigFileAbsPath, err := filepath.Abs(path)
 	if err != nil {
diff --git a/config/stack.go b/config/stack.go
index fb0071f3..c726a206 100644
--- a/config/stack.go
+++ b/config/stack.go
@@ -74,10 +74,6 @@ type Stack struct {
 	Path         string     `hcl:"path,attr"`
 }
 
-type stackParserContext struct {
-	stackConfigFile string
-}
-
 // GenerateStacks generates the stack files.
 func GenerateStacks(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {
 	processedFiles := make(map[string]bool)
@@ -693,8 +689,10 @@ func (u *Unit) ReadOutputs(ctx context.Context, l log.Logger, opts *options.Terr
 func ReadStackConfigFile(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, filePath string, values *cty.Value) (*StackConfig, error) {
 	l.Debugf("Reading Terragrunt stack config file at %s", filePath)
 
-	ctx = context.WithValue(ctx, stackParserContext{}, stackParserContext{stackConfigFile: filePath})
-	parser := NewParsingContext(ctx, l, opts)
+	stackOpts := opts.Clone()
+	stackOpts.TerragruntConfigPath = filePath
+
+	parser := NewParsingContext(ctx, l, stackOpts)
 
 	file, err := hclparse.NewParser(parser.ParserOptions...).ParseFromFile(filePath)
 	if err != nil {



Test patch:
diff --git a/config/config_helpers_test.go b/config/config_helpers_test.go
index 6a4c9f2b..80500c3e 100644
--- a/config/config_helpers_test.go
+++ b/config/config_helpers_test.go
@@ -333,6 +333,49 @@ func TestFindInParentFolders(t *testing.T) {
 	}
 }
 
+func TestFindInParentFoldersWithStackFile(t *testing.T) {
+	t.Parallel()
+
+	tempDir := t.TempDir()
+
+	regionHclPath := filepath.Join(tempDir, "region.hcl")
+	regionHclContent := `locals {
+  aws_region = "us-east-1"
+}`
+	err := os.WriteFile(regionHclPath, []byte(regionHclContent), 0644)
+	require.NoError(t, err)
+
+	stackDir := filepath.Join(tempDir, "stack")
+	err = os.MkdirAll(stackDir, 0755)
+	require.NoError(t, err)
+
+	stackHclPath := filepath.Join(stackDir, "terragrunt.stack.hcl")
+	stackHclContent := `locals {
+  regions_vars = read_terragrunt_config(find_in_parent_folders("region.hcl"))
+  region       = local.regions_vars.locals.aws_region
+}
+
+unit "test" {
+  source = "."
+  path   = "test"
+}`
+	err = os.WriteFile(stackHclPath, []byte(stackHclContent), 0644)
+	require.NoError(t, err)
+
+	l := logger.CreateLogger()
+	opts, err := options.NewTerragruntOptionsForTest(stackHclPath)
+	require.NoError(t, err)
+	opts.WorkingDir = tempDir
+
+	stackConfig, err := config.ReadStackConfigFile(t.Context(), l, opts, stackHclPath, nil)
+	require.NoError(t, err)
+	require.NotNil(t, stackConfig)
+
+	region, exists := stackConfig.Locals["region"]
+	require.True(t, exists, "Expected 'region' local to be parsed")
+	require.Equal(t, "us-east-1", region)
+}
+
 func TestResolveTerragruntInterpolation(t *testing.T) {
 	t.Parallel()
 
diff --git a/test/fixtures/stacks/find-in-parent-folders/live/stack/terragrunt.stack.hcl b/test/fixtures/stacks/find-in-parent-folders/live/stack/terragrunt.stack.hcl
new file mode 100644
index 00000000..9daedc2d
--- /dev/null
+++ b/test/fixtures/stacks/find-in-parent-folders/live/stack/terragrunt.stack.hcl
@@ -0,0 +1,13 @@
+locals {
+  mock_vars = read_terragrunt_config(find_in_parent_folders("mock.hcl"))
+  mock       = local.mock_vars.locals.mock
+}
+
+unit "foo" {
+  source = "../../units/foo"
+  path   = "foo"
+
+  values = {
+    mock = local.mock
+  }
+}
diff --git a/test/fixtures/stacks/find-in-parent-folders/mock.hcl b/test/fixtures/stacks/find-in-parent-folders/mock.hcl
new file mode 100644
index 00000000..58a6e7c6
--- /dev/null
+++ b/test/fixtures/stacks/find-in-parent-folders/mock.hcl
@@ -0,0 +1,3 @@
+locals {
+  mock = "mock"
+}
diff --git a/test/fixtures/stacks/find-in-parent-folders/units/foo/main.tf b/test/fixtures/stacks/find-in-parent-folders/units/foo/main.tf
new file mode 100644
index 00000000..932ece2a
--- /dev/null
+++ b/test/fixtures/stacks/find-in-parent-folders/units/foo/main.tf
@@ -0,0 +1,8 @@
+variable "mock" {
+  description = "Mock value to be passed through"
+  type        = string
+}
+
+output "mock" {
+  value = var.mock
+}
diff --git a/test/fixtures/stacks/find-in-parent-folders/units/foo/terragrunt.hcl b/test/fixtures/stacks/find-in-parent-folders/units/foo/terragrunt.hcl
new file mode 100644
index 00000000..260cb622
--- /dev/null
+++ b/test/fixtures/stacks/find-in-parent-folders/units/foo/terragrunt.hcl
@@ -0,0 +1,7 @@
+terraform {
+  source = "."
+}
+
+inputs = {
+  mock = values.mock
+}
diff --git a/test/integration_stacks_test.go b/test/integration_stacks_test.go
index f9d68b42..16d93e12 100644
--- a/test/integration_stacks_test.go
+++ b/test/integration_stacks_test.go
@@ -50,6 +50,7 @@ const (
 	testFixtureStackTerragruntDir              = "fixtures/stacks/terragrunt-dir"
 	testFixtureStacksAllNoStackDir             = "fixtures/stacks/all-no-stack-dir"
 	testFixtureStackNoDotTerragruntStackOutput = "fixtures/stacks/no-dot-terragrunt-stack-output"
+	testFixtureStackFindInParentFolders        = "fixtures/stacks/find-in-parent-folders"
 )
 
 func TestStacksGenerateBasicWithQueueIncludeDirFlag(t *testing.T) {
@@ -1393,3 +1394,47 @@ func TestStackOutputWithNoDotTerragruntStack(t *testing.T) {
 
 	assert.Contains(t, stdout, "name = \"app1\"")
 }
+
+func TestStackFindInParentFolders(t *testing.T) {
+	t.Parallel()
+
+	helpers.CleanupTerraformFolder(t, testFixtureStackFindInParentFolders)
+	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackFindInParentFolders)
+	rootPath := util.JoinPath(tmpEnvPath, testFixtureStackFindInParentFolders, "live")
+
+	// Test that stack generation works from the live directory
+	helpers.RunTerragrunt(t, "terragrunt stack generate --working-dir "+rootPath)
+
+	// Verify that the stack directory was created and contains the expected unit
+	stackDir := util.JoinPath(rootPath, "stack", ".terragrunt-stack")
+	validateStackDir(t, stackDir)
+
+	// Verify that the foo unit was generated
+	fooUnitPath := util.JoinPath(stackDir, "foo")
+	assert.True(t, util.FileExists(fooUnitPath), "Expected foo unit to exist in .terragrunt-stack directory")
+	assert.True(t, util.FileExists(util.JoinPath(fooUnitPath, "terragrunt.hcl")), "Expected terragrunt.hcl to exist in foo unit")
+
+	// Test that stack generation works from the parent directory (this tests our fix)
+	parentPath := util.JoinPath(tmpEnvPath, testFixtureStackFindInParentFolders)
+	helpers.RunTerragrunt(t, "terragrunt stack generate --working-dir "+parentPath)
+
+	// Verify that the stack directory was created from the parent directory
+	parentStackDir := util.JoinPath(parentPath, "live", "stack", ".terragrunt-stack")
+	validateStackDir(t, parentStackDir)
+
+	// Verify that the foo unit was generated from the parent directory
+	parentFooUnitPath := util.JoinPath(parentStackDir, "foo")
+	assert.True(t, util.FileExists(parentFooUnitPath), "Expected foo unit to exist when generating from parent directory")
+	assert.True(t, util.FileExists(util.JoinPath(parentFooUnitPath, "terragrunt.hcl")), "Expected terragrunt.hcl to exist in foo unit when generating from parent directory")
+
+	stackPath := util.JoinPath(rootPath, "stack")
+
+	_, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt stack run apply --non-interactive --working-dir "+stackPath)
+	require.NoError(t, err, "Expected stack run apply to succeed")
+
+	outputStdout, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt stack output --raw foo.mock --non-interactive --working-dir "+stackPath)
+	require.NoError(t, err, "Expected stack output to succeed")
+
+	// Verify that the mock value from mock.hcl was correctly passed through
+	assert.Equal(t, "mock", strings.TrimSpace(outputStdout), "Expected raw output to contain just the mock value")
+}