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

Issue:
Merge the "skip" attribute from included terragrunt files is not supported
## Describe the bug

The current Terragrunt documentation, specifically the section on the skip attribute [here](https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#skip), stipulates that this flag must be explicitly set within Terragrunt modules that are intended to be skipped. This implies that the configuration will not be inherited from include blocks. Despite this being clearly documented, it appears to contradict the merging concept supported by the include block.

From a user's standpoint, it would be beneficial to have the ability to inherit this flag/attribute, similar to how many other attributes are inherited. Is there a specific rationale behind the distinct treatment of this attribute that I may be overlooking?

## Steps To Reproduce

Create a partial terragrunt hcl file

partial.hcl
```hcl
locals {
  module_path_parts = split("/", path_relative_to_include())
  module_base_path  = local.module_path_parts[0]
  module_name       = join("_", slice(local.module_path_parts, 1, length(local.module_path_parts)))
}

skip = !endswith(local.module_name, "_account_layer")
```

Include the partial file in a live module

partial.hcl
```hcl
include {
  path   = find_in_parent_folders("partial.hcl")
  expose = true
}
```

## Expected behavior

As you can see, the `skip` in the partial terragrunt file does not contain a simple `true` or `false`, it contains a more advanced logic that I would like to inherit in included modules, without having to duplicate the logic there, currently that is not possible because the `skip` attribute is not included.

## Nice to haves

- [ ] Terminal output
- [ ] Screenshots

## Versions

- Terragrunt version: [v0.59.5](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.59.5)
- OpenTofu/Terraform version: 1.5.5
- Environment details (Ubuntu 20.04, Windows 10, etc.): macOS 14.x

## Additional context

Add any other context about the problem here.



Discussion / hints:

Fix patch:
diff --git a/cli/commands/terraform/action.go b/cli/commands/terraform/action.go
index cc645fde73..178106cedf 100644
--- a/cli/commands/terraform/action.go
+++ b/cli/commands/terraform/action.go
@@ -126,7 +126,7 @@ func runTerraform(ctx context.Context, terragruntOptions *options.TerragruntOpti
 		return target.runErrorCallback(terragruntOptions, terragruntConfig, err)
 	}
 
-	if terragruntConfig.Skip {
+	if terragruntConfig.Skip != nil && *terragruntConfig.Skip {
 		terragruntOptions.Logger.Infof(
 			"Skipping terragrunt module %s due to skip = true.",
 			terragruntOptions.TerragruntConfigPath,
diff --git a/config/config.go b/config/config.go
index fb8923fcc2..5b6efce25d 100644
--- a/config/config.go
+++ b/config/config.go
@@ -102,7 +102,7 @@ type TerragruntConfig struct {
 	Dependencies                *ModuleDependencies
 	DownloadDir                 string
 	PreventDestroy              *bool
-	Skip                        bool
+	Skip                        *bool
 	IamRole                     string
 	IamAssumeRoleDuration       *int64
 	IamAssumeRoleSessionName    string
@@ -1114,7 +1114,7 @@ func convertToTerragruntConfig(ctx *ParsingContext, configPath string, terragrun
 	}
 
 	if terragruntConfigFromFile.Skip != nil {
-		terragruntConfig.Skip = *terragruntConfigFromFile.Skip
+		terragruntConfig.Skip = terragruntConfigFromFile.Skip
 		terragruntConfig.SetFieldMetadata(MetadataSkip, defaultMetadata)
 	}
 
diff --git a/config/config_as_cty.go b/config/config_as_cty.go
index d926265d9a..417c4507e3 100644
--- a/config/config_as_cty.go
+++ b/config/config_as_cty.go
@@ -25,10 +25,13 @@ func TerragruntConfigAsCty(config *TerragruntConfig) (cty.Value, error) {
 	output[MetadataTerragruntVersionConstraint] = gostringToCty(config.TerragruntVersionConstraint)
 	output[MetadataDownloadDir] = gostringToCty(config.DownloadDir)
 	output[MetadataIamRole] = gostringToCty(config.IamRole)
-	output[MetadataSkip] = goboolToCty(config.Skip)
 	output[MetadataIamAssumeRoleSessionName] = gostringToCty(config.IamAssumeRoleSessionName)
 	output[MetadataIamWebIdentityToken] = gostringToCty(config.IamWebIdentityToken)
 
+	if config.Skip != nil {
+		output[MetadataSkip] = goboolToCty(*config.Skip)
+	}
+
 	catalogConfigCty, err := catalogConfigAsCty(config.Catalog)
 	if err != nil {
 		return cty.NilVal, err
diff --git a/config/config_partial.go b/config/config_partial.go
index 16aa67e3a1..b7a6397820 100644
--- a/config/config_partial.go
+++ b/config/config_partial.go
@@ -323,7 +323,7 @@ func PartialParseConfig(ctx *ParsingContext, file *hclparse.File, includeFromChi
 			}
 
 			if decoded.Skip != nil {
-				output.Skip = *decoded.Skip
+				output.Skip = decoded.Skip
 			}
 
 			if decoded.IamRole != nil {
diff --git a/config/dependency.go b/config/dependency.go
index 0dee61f83c..444d54963e 100644
--- a/config/dependency.go
+++ b/config/dependency.go
@@ -244,7 +244,7 @@ func decodeDependencies(ctx *ParsingContext, decodedDependency TerragruntDepende
 				depCtx := ctx.WithDecodeList(TerragruntFlags, TerragruntInputs).WithTerragruntOptions(depOpts)
 
 				if depConfig, err := PartialParseConfigFile(depCtx, depPath, nil); err == nil {
-					if depConfig.Skip {
+					if depConfig.Skip != nil && *depConfig.Skip {
 						ctx.TerragruntOptions.Logger.Debugf("Skipping outputs reading for disabled dependency %s", dep.Name)
 						dep.Enabled = new(bool)
 					}
diff --git a/config/include.go b/config/include.go
index 84cdd17705..7e2841d1ca 100644
--- a/config/include.go
+++ b/config/include.go
@@ -263,8 +263,9 @@ func (cfg *TerragruntConfig) Merge(sourceConfig *TerragruntConfig, terragruntOpt
 		cfg.Engine = sourceConfig.Engine.Clone()
 	}
 
-	// Skip has to be set specifically in each file that should be skipped
-	cfg.Skip = sourceConfig.Skip
+	if sourceConfig.Skip != nil {
+		cfg.Skip = sourceConfig.Skip
+	}
 
 	if sourceConfig.RemoteState != nil {
 		cfg.RemoteState = sourceConfig.RemoteState
@@ -388,8 +389,9 @@ func (cfg *TerragruntConfig) DeepMerge(sourceConfig *TerragruntConfig, terragrun
 		cfg.Engine.Merge(sourceConfig.Engine)
 	}
 
-	// Skip has to be set specifically in each file that should be skipped
-	cfg.Skip = sourceConfig.Skip
+	if sourceConfig.Skip != nil {
+		cfg.Skip = sourceConfig.Skip
+	}
 
 	// Copy only dependencies which doesn't exist in source
 	if sourceConfig.Dependencies != nil {
diff --git a/docs/_docs/04_reference/config-blocks-and-attributes.md b/docs/_docs/04_reference/config-blocks-and-attributes.md
index 533a702414..e6dbcbbdfa 100644
--- a/docs/_docs/04_reference/config-blocks-and-attributes.md
+++ b/docs/_docs/04_reference/config-blocks-and-attributes.md
@@ -1301,9 +1301,8 @@ root level `terragrunt.hcl` file, you can set `skip = true`:
 skip = true
 ```
 
-The `skip` flag must be set explicitly in terragrunt modules that should be skipped. If you set `skip = true` in a
-`terragrunt.hcl` file that is included by another `terragrunt.hcl` file, only the `terragrunt.hcl` file that explicitly
-set `skip = true` will be skipped.
+The `skip` flag can be inherited from an included `terragrunt.hcl` file if `skip` is defined there, unless it is
+explicitly redefined in the current's module `terragrunt.hcl` file.
 
 ### iam_role
 


Test patch:
diff --git a/config/config_as_cty_test.go b/config/config_as_cty_test.go
index acf011270a..02fcf4e96c 100644
--- a/config/config_as_cty_test.go
+++ b/config/config_as_cty_test.go
@@ -82,7 +82,7 @@ func TestTerragruntConfigAsCtyDrift(t *testing.T) {
 		},
 		DownloadDir:    ".terragrunt-cache",
 		PreventDestroy: &testTrue,
-		Skip:           true,
+		Skip:           &testTrue,
 		IamRole:        "terragruntRole",
 		Inputs: map[string]interface{}{
 			"aws_region": "us-east-1",
diff --git a/config/config_partial_test.go b/config/config_partial_test.go
index 47e5e71df4..1f1e08df47 100644
--- a/config/config_partial_test.go
+++ b/config/config_partial_test.go
@@ -33,7 +33,7 @@ dependencies {
 	assert.Equal(t, "../app1", terragruntConfig.Dependencies.Paths[0])
 	assert.Equal(t, map[string]interface{}{"app1": "../app1"}, terragruntConfig.Locals)
 
-	assert.False(t, terragruntConfig.Skip)
+	assert.Nil(t, terragruntConfig.Skip)
 	assert.Nil(t, terragruntConfig.PreventDestroy)
 	assert.Nil(t, terragruntConfig.Terraform)
 	assert.Nil(t, terragruntConfig.RemoteState)
@@ -81,7 +81,8 @@ skip = true
 	assert.Len(t, terragruntConfig.Dependencies.Paths, 1)
 	assert.Equal(t, "../app1", terragruntConfig.Dependencies.Paths[0])
 
-	assert.True(t, terragruntConfig.Skip)
+	assert.NotNil(t, terragruntConfig.Skip)
+	assert.True(t, *terragruntConfig.Skip)
 	assert.True(t, *terragruntConfig.PreventDestroy)
 
 	assert.Nil(t, terragruntConfig.Terraform)
@@ -99,7 +100,7 @@ func TestPartialParseOmittedItems(t *testing.T) {
 	require.NoError(t, err)
 	assert.True(t, terragruntConfig.IsPartial)
 	assert.Nil(t, terragruntConfig.Dependencies)
-	assert.False(t, terragruntConfig.Skip)
+	assert.Nil(t, terragruntConfig.Skip)
 	assert.Nil(t, terragruntConfig.PreventDestroy)
 	assert.Nil(t, terragruntConfig.Terraform)
 	assert.Nil(t, terragruntConfig.RemoteState)
@@ -131,7 +132,7 @@ func TestPartialParseOnlyInheritsSelectedBlocksFlags(t *testing.T) {
 
 	assert.True(t, terragruntConfig.IsPartial)
 	assert.Nil(t, terragruntConfig.Dependencies)
-	assert.False(t, terragruntConfig.Skip)
+	assert.Nil(t, terragruntConfig.Skip)
 	assert.True(t, *terragruntConfig.PreventDestroy)
 	assert.Nil(t, terragruntConfig.Terraform)
 	assert.Nil(t, terragruntConfig.RemoteState)
@@ -154,7 +155,7 @@ func TestPartialParseOnlyInheritsSelectedBlocksDependencies(t *testing.T) {
 	assert.Len(t, terragruntConfig.Dependencies.Paths, 1)
 	assert.Equal(t, "../app1", terragruntConfig.Dependencies.Paths[0])
 
-	assert.False(t, terragruntConfig.Skip)
+	assert.Nil(t, terragruntConfig.Skip)
 	assert.Nil(t, terragruntConfig.PreventDestroy)
 	assert.Nil(t, terragruntConfig.Terraform)
 	assert.Nil(t, terragruntConfig.RemoteState)
diff --git a/config/config_test.go b/config/config_test.go
index 125647cef9..3123f35a4f 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -720,7 +720,7 @@ func TestParseTerragruntConfigEmptyConfig(t *testing.T) {
 	assert.Nil(t, terragruntConfig.RemoteState)
 	assert.Nil(t, terragruntConfig.Dependencies)
 	assert.Nil(t, terragruntConfig.PreventDestroy)
-	assert.False(t, terragruntConfig.Skip)
+	assert.Nil(t, terragruntConfig.Skip)
 	assert.Empty(t, terragruntConfig.IamRole)
 	assert.Empty(t, terragruntConfig.IamWebIdentityToken)
 	assert.Nil(t, terragruntConfig.RetryMaxAttempts)
@@ -1213,7 +1213,8 @@ skip = true
 	assert.Nil(t, terragruntConfig.Terraform)
 	assert.Nil(t, terragruntConfig.RemoteState)
 	assert.Nil(t, terragruntConfig.Dependencies)
-	assert.True(t, terragruntConfig.Skip)
+	assert.NotNil(t, terragruntConfig.Skip)
+	assert.True(t, *terragruntConfig.Skip)
 }
 
 func TestParseTerragruntConfigSkipFalse(t *testing.T) {
@@ -1232,7 +1233,8 @@ skip = false
 	assert.Nil(t, terragruntConfig.Terraform)
 	assert.Nil(t, terragruntConfig.RemoteState)
 	assert.Nil(t, terragruntConfig.Dependencies)
-	assert.False(t, terragruntConfig.Skip)
+	assert.NotNil(t, terragruntConfig.Skip)
+	assert.False(t, *terragruntConfig.Skip)
 }
 
 func TestIncludeFunctionsWorkInChildConfig(t *testing.T) {
diff --git a/config/include_test.go b/config/include_test.go
index 2ee641355e..7b31921a17 100644
--- a/config/include_test.go
+++ b/config/include_test.go
@@ -14,6 +14,9 @@ import (
 func TestMergeConfigIntoIncludedConfig(t *testing.T) {
 	t.Parallel()
 
+	testTrue := true
+	testFalse := false
+
 	testCases := []struct {
 		config         *config.TerragruntConfig
 		includedConfig *config.TerragruntConfig
@@ -111,18 +114,18 @@ func TestMergeConfigIntoIncludedConfig(t *testing.T) {
 		},
 		{
 			&config.TerragruntConfig{},
-			&config.TerragruntConfig{Skip: true},
-			&config.TerragruntConfig{Skip: false},
+			&config.TerragruntConfig{Skip: &testTrue},
+			&config.TerragruntConfig{Skip: &testTrue},
 		},
 		{
-			&config.TerragruntConfig{Skip: false},
-			&config.TerragruntConfig{Skip: true},
-			&config.TerragruntConfig{Skip: false},
+			&config.TerragruntConfig{Skip: &testFalse},
+			&config.TerragruntConfig{Skip: &testTrue},
+			&config.TerragruntConfig{Skip: &testFalse},
 		},
 		{
-			&config.TerragruntConfig{Skip: true},
-			&config.TerragruntConfig{Skip: true},
-			&config.TerragruntConfig{Skip: true},
+			&config.TerragruntConfig{Skip: &testTrue},
+			&config.TerragruntConfig{Skip: &testTrue},
+			&config.TerragruntConfig{Skip: &testTrue},
 		},
 		{
 			&config.TerragruntConfig{IamRole: "role2"},
@@ -166,6 +169,9 @@ func TestMergeConfigIntoIncludedConfig(t *testing.T) {
 func TestDeepMergeConfigIntoIncludedConfig(t *testing.T) {
 	t.Parallel()
 
+	testTrue := true
+	testFalse := false
+
 	// The following maps are convenience vars for setting up deep merge map tests
 	overrideMap := map[string]interface{}{
 		"simple_string_override": "hello, mock",
@@ -250,6 +256,25 @@ func TestDeepMergeConfigIntoIncludedConfig(t *testing.T) {
 			&config.TerragruntConfig{IamRole: "bar"},
 			&config.TerragruntConfig{IamRole: "foo"},
 		},
+		// skip related tests
+		{
+			"skip - preserve target",
+			&config.TerragruntConfig{},
+			&config.TerragruntConfig{Skip: &testTrue},
+			&config.TerragruntConfig{Skip: &testTrue},
+		},
+		{
+			"skip - copy source",
+			&config.TerragruntConfig{Skip: &testFalse},
+			&config.TerragruntConfig{Skip: &testTrue},
+			&config.TerragruntConfig{Skip: &testFalse},
+		},
+		{
+			"skip - still copy source",
+			&config.TerragruntConfig{Skip: &testTrue},
+			&config.TerragruntConfig{Skip: &testTrue},
+			&config.TerragruntConfig{Skip: &testTrue},
+		},
 		// Deep merge dependencies
 		{
 			"dependencies",
diff --git a/configstack/stack_test.go b/configstack/stack_test.go
index f726578bdf..9a64d063c5 100644
--- a/configstack/stack_test.go
+++ b/configstack/stack_test.go
@@ -318,7 +318,6 @@ func TestResolveTerraformModulesReadConfigFromParentConfig(t *testing.T) {
 			"retry_max_attempts":            interface{}(nil),
 			"retry_sleep_interval_sec":      interface{}(nil),
 			"retryable_errors":              interface{}(nil),
-			"skip":                          false,
 			"terraform_binary":              "",
 			"terraform_version_constraint":  "",
 			"terragrunt_version_constraint": "",
diff --git a/test/fixtures/skip/skip-true/resource1/terragrunt.hcl b/test/fixtures/skip/skip-true/resource1/terragrunt.hcl
index aa3fee3dc4..08ff3ecda8 100644
--- a/test/fixtures/skip/skip-true/resource1/terragrunt.hcl
+++ b/test/fixtures/skip/skip-true/resource1/terragrunt.hcl
@@ -2,6 +2,8 @@ include {
   path = find_in_parent_folders()
 }
 
+skip = false
+
 inputs = {
   person = "Ernie"
 }
diff --git a/test/integration_test.go b/test/integration_test.go
index 13e2d6c4dd..a87974bcbd 100644
--- a/test/integration_test.go
+++ b/test/integration_test.go
@@ -929,10 +929,13 @@ func TestApplyAllSkipTrue(t *testing.T) {
 	stdout := showStdout.String()
 	stderr := showStderr.String()
 
+	// this test is now prepared to handle the case where skip is inherited from the included terragrunt file
+	// meaning the skip-true/resource2 module will be skipped as well and only the skip-true/resource1 module will be applied
+
 	require.NoError(t, err)
 	assert.Contains(t, stderr, "Skipping terragrunt module ./terragrunt.hcl due to skip = true.")
 	assert.Contains(t, stdout, "hello, Ernie")
-	assert.Contains(t, stdout, "hello, Bert")
+	assert.NotContains(t, stdout, "hello, Bert")
 }
 
 func TestApplyAllSkipFalse(t *testing.T) {