Repository: gruntwork-io/terragrunt (language: go)

Issue:
terragrunt always runs terraform init in advance although no backend/module changes happened
**Describe the bug**
Terragrunt always runs terraform init in advance although no [backend/module]([url]([url](url))) changes happened

**To Reproduce**
Steps to reproduce the behavior, code snippets and examples which can be used to reproduce the issue.

Structure looks like the screenshot (tg-structure)


```hcl
# tfe/_env/encryption.hcl
terraform {
  source = "${get_parent_terragrunt_dir()}/..//modules/encryption"
}

locals {
  common_vars = read_terragrunt_config("${get_terragrunt_dir()}/../../common.hcl")
}
```

```hcl
# tfe/nonprod/terragrunt.hcl
locals {
  common_vars = read_terragrunt_config("${get_terragrunt_dir()}/../../common.hcl")
}


remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
  config = {
    bucket = "${local.common_vars.inputs.resource_prefix}-states-${get_aws_account_id()}"
    key = "${path_relative_to_include()}/terraform.tfstate"
    region         = "eu-central-1"
    encrypt        = true
    dynamodb_table = "${local.common_vars.inputs.resource_prefix}-lock-table"

    s3_bucket_tags = merge(tomap({"global.env" = "Development"}), local.common_vars.inputs.default_tags)
    dynamodb_table_tags =  merge(tomap({"global.env" = "Development"}),local.common_vars.inputs.default_tags)     
  }
}

generate "provider" {
  path = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents = <<EOF
provider "aws" {
  region = "eu-central-1"
  default_tags {
      tags = merge(var.default_tags, tomap({"global.env" = "Development"}))
    }
}
EOF
}

generate "versions" {
  path      = "versions.tf"
  if_exists = "overwrite"
  contents  = <<EOF
terraform {
  required_version = ">= 1.0.0"

  required_providers {
    aws = {
      version = "~> 5.0"
      source  = "hashicorp/aws"
    }
    archive = {
      version = ">= 2.2.0"
      source  = "hashicorp/archive"
    }
    random = {
      version = ">= 3.2.0"
      source  = "hashicorp/random"
    }
  }

}

EOF

}

terraform {
    extra_arguments "common_vars" {
      commands = get_terraform_commands_that_need_vars()
      arguments = [
        "-var-file=${get_terragrunt_dir()}/../../common.tfvars",
      ]
    }
  }


```

```hcl
# tfe/nonprod/encryption/terragrunt.hcl
include "root" {
  path   = find_in_parent_folders()
  #expose = true
}


include "env" {
  path = "${get_terragrunt_dir()}/../../_env/encryption.hcl"
}

inputs = {
  env = "nonprod"
}
```


**Expected behavior**

`terragrunt plan` in `tfe/nonprod/encryption` should ONLY terraform init once in avance. Instead, it runs terraform init for each terragrunt plan.
I do not understand why as I did not change the backend nor the module content.


**Versions**
- Terragrunt version: 0.48.0
- Terraform version: 1.5.2
- Environment details (Ubuntu 20.04, Windows 10, etc.): Windows 10

**Additional context**
My project structure:
![tg-structure](https://github.com/gruntwork-io/terragrunt/assets/19877252/a430c5fa-a56f-427f-b162-f1fe44c5e1e6)



Discussion / hints:
Hi,
will be helpful to provide an example repository where this issue happens

I tried to re-use provided files in https://github.com/denis256/terragrunt-tests/tree/master/issue-2627 but Terragrunt fails with some missing files:

```
ERRO[0000] Error: Error in function call
      
ERRO[0000]   on /projects/gruntwork/terragrunt-tests/issue-2627/tfe/nonprod/encryption/../../_env/encryption.hcl line 7, in locals: 
ERRO[0000]    7:   common_vars = read_terragrunt_config("${get_terragrunt_dir()}/../../common.hcl") 
ERRO[0000]                                              
ERRO[0000] Call to function "read_terragrunt_config" failed: Terragrunt config /projects/gruntwork/terragrunt-tests/issue-2627/tfe/common.hcl not found.
 
ERRO[0000] Encountered error while evaluating locals in file /projects/gruntwork/terragrunt-tests/issue-2627/tfe/nonprod/encryption/../../_env/encryption.hcl 
ERRO[0000] /projects/gruntwork/terragrunt-tests/issue-2627/tfe/nonprod/encryption/../../_env/encryption.hcl:7,17-40: Error in function call; Call to function "read_terragrunt_config" failed: Terragrunt config /projects/gruntwork/terragrunt-tests/issue-2627/tfe/common.hcl not found. 

```

Of course:
[https://github.com/Olgoetz/terragrunt-debug](url)
With the correct link now:
https://github.com/Olgoetz/terragrunt-debug


Fix patch:
diff --git a/cli/args.go b/cli/args.go
index 85bbce9d7f..2e39e33050 100644
--- a/cli/args.go
+++ b/cli/args.go
@@ -56,7 +56,7 @@ func parseTerragruntOptionsFromArgs(terragruntVersion string, args []string, wri
 		return nil, err
 	}
 	if downloadDirRaw == "" {
-		downloadDirRaw = util.JoinPath(workingDir, options.TerragruntCacheDir)
+		downloadDirRaw = util.JoinPath(workingDir, util.TerragruntCacheDir)
 	}
 	downloadDir, err := filepath.Abs(downloadDirRaw)
 	if err != nil {
diff --git a/cli/hclfmt.go b/cli/hclfmt.go
index 3e5a430669..239d627ea0 100644
--- a/cli/hclfmt.go
+++ b/cli/hclfmt.go
@@ -46,7 +46,7 @@ func runHCLFmt(terragruntOptions *options.TerragruntOptions) error {
 	filteredTgHclFiles := []string{}
 	for _, fname := range tgHclFiles {
 		// Ignore any files that are in the .terragrunt-cache
-		if !util.ListContainsElement(strings.Split(fname, "/"), ".terragrunt-cache") {
+		if !util.ListContainsElement(strings.Split(fname, "/"), util.TerragruntCacheDir) {
 			filteredTgHclFiles = append(filteredTgHclFiles, fname)
 		} else {
 			terragruntOptions.Logger.Debugf("%s was ignored due to being in the terragrunt cache", fname)
diff --git a/cli/tfsource/types.go b/cli/tfsource/types.go
index 86b0f691b6..24d2efa69f 100644
--- a/cli/tfsource/types.go
+++ b/cli/tfsource/types.go
@@ -63,10 +63,17 @@ func (terraformSource TerraformSource) EncodeSourceVersion() (string, error) {
 				// We don't use any info from directories to calculate our hash
 				return nil
 			}
+			// avoid checking files in .terragrunt-cache directory since contents is auto-generated
+			if strings.Contains(path, util.TerragruntCacheDir) {
+				return nil
+			}
+			// avoid checking files in .terraform directory since contents is auto-generated
+			if info.Name() == util.TerraformLockFile {
+				return nil
+			}
 
 			fileModified := info.ModTime().UnixMicro()
 			hashContents := fmt.Sprintf("%s:%d", path, fileModified)
-
 			sourceHash.Write([]byte(hashContents))
 
 			return nil
diff --git a/config/config.go b/config/config.go
index 6390d4d945..6e457b8f56 100644
--- a/config/config.go
+++ b/config/config.go
@@ -565,7 +565,7 @@ func FindConfigFilesInPath(rootPath string, terragruntOptions *options.Terragrun
 		}
 
 		// Skip the Terragrunt cache dir entirely
-		if info.IsDir() && info.Name() == options.TerragruntCacheDir {
+		if info.IsDir() && info.Name() == util.TerragruntCacheDir {
 			return filepath.SkipDir
 		}
 
@@ -593,7 +593,7 @@ func containsTerragruntModule(path string, info os.FileInfo, terragruntOptions *
 	}
 
 	// Skip the Terragrunt cache dir
-	if util.ContainsPath(path, options.TerragruntCacheDir) {
+	if util.ContainsPath(path, util.TerragruntCacheDir) {
 		return false, nil
 	}
 
diff --git a/options/options.go b/options/options.go
index a1659b37fd..6386e91ea3 100644
--- a/options/options.go
+++ b/options/options.go
@@ -28,8 +28,6 @@ const DEFAULT_PARALLELISM = math.MaxInt32
 // TERRAFORM_DEFAULT_PATH just takes terraform from the path
 const TERRAFORM_DEFAULT_PATH = "terraform"
 
-const TerragruntCacheDir = ".terragrunt-cache"
-
 const DefaultTFDataDir = ".terraform"
 
 const DefaultIAMAssumeRoleDuration = 3600
@@ -299,7 +297,7 @@ func NewTerragruntOptions(terragruntConfigPath string) (*TerragruntOptions, erro
 func DefaultWorkingAndDownloadDirs(terragruntConfigPath string) (string, string, error) {
 	workingDir := filepath.Dir(terragruntConfigPath)
 
-	downloadDir, err := filepath.Abs(filepath.Join(workingDir, TerragruntCacheDir))
+	downloadDir, err := filepath.Abs(filepath.Join(workingDir, util.TerragruntCacheDir))
 	if err != nil {
 		return "", "", errors.WithStackTrace(err)
 	}
diff --git a/util/file.go b/util/file.go
index 7db42c1279..bbbf30e58c 100644
--- a/util/file.go
+++ b/util/file.go
@@ -21,6 +21,8 @@ import (
 
 const TerraformLockFile = ".terraform.lock.hcl"
 
+const TerragruntCacheDir = ".terragrunt-cache"
+
 // FileOrData will read the contents of the data of the given arg if it is a file, and otherwise return the contents by
 // itself. This will return an error if the given path is a directory.
 func FileOrData(maybePath string) (string, error) {
@@ -191,7 +193,7 @@ func expandGlobPath(source, absoluteGlobPath string) ([]string, error) {
 		return nil, errors.WithStackTrace(err)
 	}
 	for _, absoluteExpandGlobPath := range absoluteExpandGlob {
-		if strings.Contains(absoluteExpandGlobPath, ".terragrunt-cache") {
+		if strings.Contains(absoluteExpandGlobPath, TerragruntCacheDir) {
 			continue
 		}
 		relativeExpandGlobPath, err := GetPathRelativeTo(absoluteExpandGlobPath, source)


Test patch:
diff --git a/test/fixture-init-cache/app/main.tf b/test/fixture-init-cache/app/main.tf
new file mode 100644
index 0000000000..d9bf6e002e
--- /dev/null
+++ b/test/fixture-init-cache/app/main.tf
@@ -0,0 +1,5 @@
+variable "test" {}
+
+output "result" {
+  value = var.test
+}
diff --git a/test/fixture-init-cache/app/terragrunt.hcl b/test/fixture-init-cache/app/terragrunt.hcl
new file mode 100644
index 0000000000..1557aaf207
--- /dev/null
+++ b/test/fixture-init-cache/app/terragrunt.hcl
@@ -0,0 +1,8 @@
+include "root" {
+  path   = find_in_parent_folders()
+}
+
+inputs = {
+  env = "nonprod"
+}
+
diff --git a/test/fixture-init-cache/terragrunt.hcl b/test/fixture-init-cache/terragrunt.hcl
new file mode 100644
index 0000000000..24770b79a1
--- /dev/null
+++ b/test/fixture-init-cache/terragrunt.hcl
@@ -0,0 +1,20 @@
+
+generate "provider" {
+  path = "provider.tf"
+  if_exists = "overwrite_terragrunt"
+  contents = <<EOF
+provider "aws" {
+  region = "eu-central-1"
+}
+EOF
+}
+
+terraform {
+  source = "."
+  extra_arguments "common_vars" {
+    commands = get_terraform_commands_that_need_vars()
+    arguments = [
+      "-var", "test=qwe",
+    ]
+  }
+}
diff --git a/test/integration_test.go b/test/integration_test.go
index 6ce87e0248..564fcc7e82 100644
--- a/test/integration_test.go
+++ b/test/integration_test.go
@@ -151,6 +151,7 @@ const (
 	TEST_FIXTURE_REF_SOURCE                                                  = "fixture-download/remote-ref"
 	TEST_FIXTURE_SOURCE_MAP_SLASHES                                          = "fixture-source-map/slashes-in-ref"
 	TEST_FIXTURE_STRCONTAINS                                                 = "fixture-strcontains"
+	TEST_FIXTURE_INIT_CACHE                                                  = "fixture-init-cache"
 	TERRAFORM_BINARY                                                         = "terraform"
 	TERRAFORM_FOLDER                                                         = ".terraform"
 	TERRAFORM_STATE                                                          = "terraform.tfstate"
@@ -5585,6 +5586,56 @@ func TestStrContains(t *testing.T) {
 	validateOutput(t, outputs, "o2", false)
 }
 
+func TestInitSkipCache(t *testing.T) {
+	t.Parallel()
+
+	cleanupTerraformFolder(t, TEST_FIXTURE_INIT_CACHE)
+	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_INIT_CACHE)
+	rootPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_INIT_CACHE, "app")
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	require.NoError(
+		t,
+		runTerragruntCommand(t, fmt.Sprintf("terragrunt plan --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath), &stdout, &stderr),
+	)
+
+	// verify that init was invoked
+	assert.Contains(t, stdout.String(), "Terraform has been successfully initialized!")
+	assert.Contains(t, stderr.String(), "Running command: terraform init")
+
+	stdout = bytes.Buffer{}
+	stderr = bytes.Buffer{}
+
+	require.NoError(
+		t,
+		runTerragruntCommand(t, fmt.Sprintf("terragrunt plan --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath), &stdout, &stderr),
+	)
+
+	// verify that init wasn't invoked second time since cache directories are ignored
+	assert.NotContains(t, stdout.String(), "Terraform has been successfully initialized!")
+	assert.NotContains(t, stderr.String(), "Running command: terraform init")
+
+	// verify that after adding new file, init is executed
+	tfFile := util.JoinPath(tmpEnvPath, TEST_FIXTURE_INIT_CACHE, "app", "project.tf")
+	if err := ioutil.WriteFile(tfFile, []byte(""), 0644); err != nil {
+		t.Fatalf("Error writing new Terraform file to %s: %v", tfFile, err)
+	}
+
+	stdout = bytes.Buffer{}
+	stderr = bytes.Buffer{}
+
+	require.NoError(
+		t,
+		runTerragruntCommand(t, fmt.Sprintf("terragrunt plan --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath), &stdout, &stderr),
+	)
+
+	// verify that init was invoked
+	assert.Contains(t, stdout.String(), "Terraform has been successfully initialized!")
+	assert.Contains(t, stderr.String(), "Running command: terraform init")
+}
+
 func validateOutput(t *testing.T, outputs map[string]TerraformOutput, key string, value interface{}) {
 	t.Helper()
 	output, hasPlatform := outputs[key]