Repository: gruntwork-io/terragrunt

Issue:
Automatically generate in `run --all`
## Describe the enhancement

When users use the `run` command with `run --all` or `run --graph`, automatically perform the equivalent of `stack generate`. This will allow users to transparently perform a `run --all`, and automatically pick up any configurations defined using Terragrunt Stacks.

To ensure that users have a way out of this, include a `--no-stack-generate` to explicitly opt out of generating stacks when performing runs.

## Additional context

This might result in a breaking change for users that don't expect `terragrunt.stack.hcl` files to be picked up when performing `run --all` commands, and it might be undesirable for users that want to perform runs at the root of their repositories, but have colocated `stacks` directories that will get picked up inadvertently.

This is already an issue for users that have a `units` directory, and the workaround here would be the same: Namely, run the `run --all` command within a `live` directory or the like to avoid ambiguity between source configurations and live configurations.

It shouldn't be that difficult, as it'll largely follow the same logic we use for `stack run`, etc.

## RFC Not Needed

- [x] I have evaluated the complexity of this enhancement, and I believe it does not require an RFC.



Discussion:

Solution patch:
diff --git a/.gitignore b/.gitignore
index 9ff263bd..07303cee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,4 @@ mocks/
 .terragrunt-stack
 .devcontainer.json
 .cursor/
+.env
\ No newline at end of file
diff --git a/cli/commands/run/cli.go b/cli/commands/run/cli.go
index c4437438..e006e2a7 100644
--- a/cli/commands/run/cli.go
+++ b/cli/commands/run/cli.go
@@ -2,13 +2,18 @@
 package run
 
 import (
+	"context"
+	"path/filepath"
 	"strings"
 
 	"github.com/gruntwork-io/go-commons/collections"
 	"github.com/gruntwork-io/terragrunt/cli/commands/common/graph"
 	"github.com/gruntwork-io/terragrunt/cli/commands/common/runall"
+	"github.com/gruntwork-io/terragrunt/config"
 	"github.com/gruntwork-io/terragrunt/internal/cli"
+	"github.com/gruntwork-io/terragrunt/internal/errors"
 	"github.com/gruntwork-io/terragrunt/options"
+	"github.com/gruntwork-io/terragrunt/telemetry"
 	"github.com/gruntwork-io/terragrunt/tf"
 )
 
@@ -42,6 +47,7 @@ func NewCommand(opts *options.TerragruntOptions) *cli.Command {
 
 	cmd = runall.WrapCommand(opts, cmd, Run)
 	cmd = graph.WrapCommand(opts, cmd, Run)
+	cmd = wrapWithStackGenerate(opts, cmd)
 
 	return cmd
 }
@@ -96,3 +102,42 @@ func validateCommand(opts *options.TerragruntOptions) error {
 func isTerraformPath(opts *options.TerragruntOptions) bool {
 	return strings.HasSuffix(opts.TerraformPath, options.TerraformDefaultPath)
 }
+
+// wrapWithStackGenerate wraps a CLI command to handle automatic stack generation.
+// It generates a stack configuration file when running terragrunt with --all or --graph flags,
+// unless explicitly disabled with --no-stack-generate.
+func wrapWithStackGenerate(opts *options.TerragruntOptions, cmd *cli.Command) *cli.Command {
+	// Wrap the command's action to inject stack generation logic
+	cmd = cmd.WrapAction(func(ctx *cli.Context, action cli.ActionFunc) error {
+		// Determine if stack generation should occur based on command flags
+		// Stack generation is triggered by --all or --graph flags, unless --no-stack-generate is set
+		shouldGenerateStack := (opts.RunAll || opts.Graph) && !opts.NoStackGenerate
+
+		// Skip stack generation if not needed
+		if !shouldGenerateStack {
+			opts.Logger.Debugf("Skipping stack generation in %s", opts.WorkingDir)
+			return action(ctx)
+		}
+
+		// Set the stack config path to the default location in the working directory
+		opts.TerragruntStackConfigPath = filepath.Join(opts.WorkingDir, config.DefaultStackFile)
+
+		// Generate the stack configuration with telemetry tracking
+		err := telemetry.TelemeterFromContext(ctx).Collect(ctx, "stack_generate", map[string]any{
+			"stack_config_path": opts.TerragruntStackConfigPath,
+			"working_dir":       opts.WorkingDir,
+		}, func(ctx context.Context) error {
+			return config.GenerateStacks(ctx, opts)
+		})
+
+		// Handle any errors during stack generation
+		if err != nil {
+			return errors.Errorf("failed to generate stack file: %w", err)
+		}
+
+		// Execute the original command action after successful stack generation
+		return action(ctx)
+	})
+
+	return cmd
+}
diff --git a/cli/commands/run/flags.go b/cli/commands/run/flags.go
index 25a9cf1c..620f66c5 100644
--- a/cli/commands/run/flags.go
+++ b/cli/commands/run/flags.go
@@ -38,6 +38,8 @@ const (
 	SourceMapFlagName    = "source-map"
 	SourceUpdateFlagName = "source-update"
 
+	NoStackGenerate = "no-stack-generate"
+
 	// Assume IAM Role flags.
 
 	IAMAssumeRoleFlagName                 = "iam-assume-role"
@@ -119,6 +121,15 @@ func NewFlags(opts *options.TerragruntOptions, prefix flags.Prefix) cli.Flags {
 		},
 			flags.WithDeprecatedName(terragruntPrefix.FlagName("graph-root"), terragruntPrefixControl)),
 
+		// `--all` and `--graph` related flags.
+
+		flags.NewFlag(&cli.BoolFlag{
+			Name:        NoStackGenerate,
+			EnvVars:     tgPrefix.EnvVars(NoStackGenerate),
+			Destination: &opts.NoStackGenerate,
+			Usage:       "Disable automatic stack regeneration before running the command.",
+		}),
+
 		//  Backward compatibility with `terragrunt-` prefix flags.
 
 		flags.NewFlag(&cli.GenericFlag[string]{
diff --git a/cli/commands/stack/cli.go b/cli/commands/stack/cli.go
index 135d66cb..e56f0e0c 100644
--- a/cli/commands/stack/cli.go
+++ b/cli/commands/stack/cli.go
@@ -76,12 +76,6 @@ func defaultFlags(opts *options.TerragruntOptions, prefix flags.Prefix) cli.Flag
 	tgPrefix := prefix.Prepend(flags.TgPrefix)
 
 	flags := cli.Flags{
-		flags.NewFlag(&cli.BoolFlag{
-			Name:        NoStackGenerate,
-			EnvVars:     tgPrefix.EnvVars(NoStackGenerate),
-			Destination: &opts.NoStackGenerate,
-			Usage:       "Disable automatic stack regeneration before running the command.",
-		}),
 		flags.NewFlag(&cli.BoolFlag{
 			Name:        NoStackValidate,
 			EnvVars:     tgPrefix.EnvVars(NoStackValidate),



Test patch:
diff --git a/test/integration_run_test.go b/test/integration_run_test.go
new file mode 100644
index 00000000..c74bce6f
--- /dev/null
+++ b/test/integration_run_test.go
@@ -0,0 +1,121 @@
+package test_test
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/gruntwork-io/terragrunt/test/helpers"
+	"github.com/gruntwork-io/terragrunt/util"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// TestRunStacksGenerate verifies that stack generation works correctly when running terragrunt with --all flag.
+// It ensures that:
+// 1. The stack directory is created
+// 2. The stack is properly applied
+// 3. The expected number of test.txt files are generated
+func TestRunStacksGenerate(t *testing.T) {
+	t.Parallel()
+
+	// Set up test environment
+	helpers.CleanupTerraformFolder(t, testFixtureStacksBasic)
+	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic)
+	rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksBasic, "live")
+
+	// Run terragrunt with --all flag to trigger stack generation
+	helpers.RunTerragrunt(t, "terragrunt run apply --all --non-interactive --working-dir "+rootPath)
+
+	// Verify stack directory exists and validate its contents
+	path := util.JoinPath(rootPath, ".terragrunt-stack")
+	validateStackDir(t, path)
+
+	// Collect all test.txt files in the stack directory to verify correct generation
+	var txtFiles []string
+	err := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		if !info.IsDir() && info.Name() == "test.txt" {
+			txtFiles = append(txtFiles, filePath)
+		}
+		return nil
+	})
+
+	require.NoError(t, err)
+	// Verify that exactly 4 test.txt files were generated
+	assert.Len(t, txtFiles, 4)
+}
+
+// TestRunNoStacksGenerate verifies that stack generation is skipped in appropriate scenarios:
+// 1. When running without --all flag on directory which contains only terragrunt.stack.hcl
+// 2. When running with --all but --no-stack-generate flag is set on directory which contains only terragrunt.stack.hcl
+// 3. When running without --all flag on standard terragrunt directory
+// 4. When running with --all but --no-stack-generate on directory without terragrunt.stack.hcl
+func TestRunNoStacksGenerate(t *testing.T) {
+	t.Parallel()
+
+	// Define test cases for different scenarios where stack generation should be skipped
+	testdata := []struct {
+		name       string
+		cmd        string
+		subfolder  string
+		shouldFail bool
+	}{
+		{
+			name:       "NoAll",
+			cmd:        "terragrunt run apply --non-interactive",
+			subfolder:  "live",
+			shouldFail: true,
+		},
+		{
+			name:       "AllNoGenerate",
+			cmd:        "terragrunt run apply --all --no-stack-generate --non-interactive",
+			subfolder:  "live",
+			shouldFail: true,
+		},
+		{
+			name:       "Standard",
+			cmd:        "terragrunt run apply --non-interactive",
+			subfolder:  "units/chicken",
+			shouldFail: false,
+		},
+		{
+			name:       "AllNoStackToGenerate",
+			cmd:        "terragrunt run apply --all --no-stack-generate --non-interactive",
+			subfolder:  "units",
+			shouldFail: false,
+		},
+	}
+
+	helpers.CleanupTerraformFolder(t, testFixtureStacksBasic)
+
+	// Run each test case and verify stack generation is skipped
+	for _, tt := range testdata {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			// Set up test environment
+			tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic)
+			path := util.JoinPath(tmpEnvPath, testFixtureStacksBasic, tt.subfolder)
+			cmd := tt.cmd + " --working-dir " + path + " -- -auto-approve"
+
+			// Execute terragrunt command and verify no output
+			stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)
+			if tt.shouldFail {
+				require.Error(t, err)
+				assert.Empty(t, stdout)
+				assert.Empty(t, stderr)
+			} else {
+				require.NoError(t, err)
+				assert.NotEmpty(t, stdout)
+				assert.NotEmpty(t, stderr)
+			}
+
+			// Verify that stack directory was not created
+			genPath := util.JoinPath(path, ".terragrunt-stack")
+			assert.NoDirExists(t, genPath)
+		})
+	}
+}