Repository: gruntwork-io/terragrunt

Issue:
bug: Run Summary fails with `run --all --queue-exclude-external` in a unit with dependencies.
> If I run `terragrunt plan --all --queue-exclude-external` with the Report experiment enabled, and some dependencies are external to the current folder (but the resources exist and are created already), I get errors for those resources:
> 
> ```
> 12:03:47.147 ERROR  [../configuration/s3-bucket-configurations] Error ending run for module ../configuration/s3-bucket-configurations: run not found in report: ../configuration/s3-bucket-configurations
> 12:03:47.147 ERROR  [../networking/vpc] Error ending run for module ../networking/vpc: run not found in report: ../networking/vpc
> 12:03:51.762 STDOUT [runner-registration-api-large] tofu: gitlab_user_runner.main[0]: Refreshing state... [id=47873586]
> 12:03:51.805 STDOUT [runner-registration-api-medium] tofu: gitlab_user_runner.main[0]: Refreshing state... [id=47873587]
> ```
> 
> I do not believe that should be marked as an error per se. Running Terragrunt this way is a legitimate use of this tool. The resources exist already. Their outputs are correct. I only care about resources under the current folder.
> 
> Perhaps Report should ignore the external resources in this case. Or, at least, do not flag them as errors.
> 
> ```
> % terragrunt -version
> terragrunt version v0.81.8
> ``` 

 _Originally posted by @FlorinAndrei in [#3628](https://github.com/gruntwork-io/terragrunt/issues/3628#issuecomment-2988950029)_

Discussion:

Solution patch:
diff --git a/configstack/running_module.go b/configstack/running_module.go
index 767ae089..95409325 100644
--- a/configstack/running_module.go
+++ b/configstack/running_module.go
@@ -221,7 +221,34 @@ func (module *RunningModule) moduleFinished(moduleErr error, r *report.Report, r
 
 		if reportExperiment {
 			if err := r.EndRun(module.Module.Path); err != nil {
-				module.Logger.Errorf("Error ending run for module %s: %v", module.Module.Path, err)
+				// If the run is not found in the report, it likely means this module was an external dependency
+				// that was excluded from the queue (e.g., with --queue-exclude-external).
+				if !errors.Is(err, report.ErrRunNotFound) {
+					module.Logger.Errorf("Error ending run for unit %s: %v", module.Module.Path, err)
+
+					return
+				}
+
+				if module.Module.AssumeAlreadyApplied {
+					run, err := report.NewRun(module.Module.Path)
+					if err != nil {
+						module.Logger.Errorf("Error creating run for unit %s: %v", module.Module.Path, err)
+						return
+					}
+
+					if err := r.AddRun(run); err != nil {
+						module.Logger.Errorf("Error adding run for unit %s: %v", module.Module.Path, err)
+						return
+					}
+
+					if err := r.EndRun(
+						run.Path,
+						report.WithResult(report.ResultExcluded),
+						report.WithReason(report.ReasonExcludeExternal),
+					); err != nil {
+						module.Logger.Errorf("Error ending run for unit %s: %v", module.Module.Path, err)
+					}
+				}
 			}
 		}
 	} else {
@@ -234,10 +261,6 @@ func (module *RunningModule) moduleFinished(moduleErr error, r *report.Report, r
 				report.WithReason(report.ReasonRunError),
 				report.WithCauseRunError(moduleErr.Error()),
 			); err != nil {
-				// If we can't find the run, then it never started,
-				// So we should start it and then end it as a failed run.
-				//
-				// Early exit runs should already be ended at this point.
 				if errors.Is(err, report.ErrRunNotFound) {
 					run, err := report.NewRun(module.Module.Path)
 					if err != nil {
diff --git a/internal/report/colors.go b/internal/report/colors.go
index 3725e25a..2e2202ca 100644
--- a/internal/report/colors.go
+++ b/internal/report/colors.go
@@ -10,21 +10,23 @@ import (
 
 // Colorizer is a colorizer for the run summary output.
 type Colorizer struct {
-	headingColorizer     func(string) string
-	successColorizer     func(string) string
-	failureColorizer     func(string) string
-	exitColorizer        func(string) string
-	excludeColorizer     func(string) string
-	successUnitColorizer func(string) string
-	failureUnitColorizer func(string) string
-	exitUnitColorizer    func(string) string
-	excludeUnitColorizer func(string) string
-	nanosecondColorizer  func(string) string
-	microsecondColorizer func(string) string
-	millisecondColorizer func(string) string
-	secondColorizer      func(string) string
-	minuteColorizer      func(string) string
-	defaultColorizer     func(string) string
+	headingTitleColorizer func(string) string
+	headingUnitColorizer  func(string) string
+	successColorizer      func(string) string
+	failureColorizer      func(string) string
+	exitColorizer         func(string) string
+	excludeColorizer      func(string) string
+	successUnitColorizer  func(string) string
+	failureUnitColorizer  func(string) string
+	exitUnitColorizer     func(string) string
+	excludeUnitColorizer  func(string) string
+	nanosecondColorizer   func(string) string
+	microsecondColorizer  func(string) string
+	millisecondColorizer  func(string) string
+	secondColorizer       func(string) string
+	minuteColorizer       func(string) string
+	defaultColorizer      func(string) string
+	paddingColorizer      func(string) string
 }
 
 // NewColorizer creates a new Colorizer.
@@ -34,21 +36,23 @@ func NewColorizer(shouldColor bool) *Colorizer {
 
 	if !shouldColor {
 		return &Colorizer{
-			headingColorizer:     func(s string) string { return s },
-			successColorizer:     func(s string) string { return s },
-			failureColorizer:     func(s string) string { return s },
-			exitColorizer:        func(s string) string { return s },
-			excludeColorizer:     func(s string) string { return s },
-			successUnitColorizer: func(s string) string { return s },
-			failureUnitColorizer: func(s string) string { return s },
-			exitUnitColorizer:    func(s string) string { return s },
-			excludeUnitColorizer: func(s string) string { return s },
-			nanosecondColorizer:  func(s string) string { return s },
-			microsecondColorizer: func(s string) string { return s },
-			millisecondColorizer: func(s string) string { return s },
-			secondColorizer:      func(s string) string { return s },
-			minuteColorizer:      func(s string) string { return s },
-			defaultColorizer:     func(s string) string { return s },
+			headingTitleColorizer: func(s string) string { return s },
+			headingUnitColorizer:  func(s string) string { return s },
+			successColorizer:      func(s string) string { return s },
+			failureColorizer:      func(s string) string { return s },
+			exitColorizer:         func(s string) string { return s },
+			excludeColorizer:      func(s string) string { return s },
+			successUnitColorizer:  func(s string) string { return s },
+			failureUnitColorizer:  func(s string) string { return s },
+			exitUnitColorizer:     func(s string) string { return s },
+			excludeUnitColorizer:  func(s string) string { return s },
+			nanosecondColorizer:   func(s string) string { return s },
+			microsecondColorizer:  func(s string) string { return s },
+			millisecondColorizer:  func(s string) string { return s },
+			secondColorizer:       func(s string) string { return s },
+			minuteColorizer:       func(s string) string { return s },
+			defaultColorizer:      func(s string) string { return s },
+			paddingColorizer:      func(s string) string { return s },
 		}
 	}
 
@@ -67,21 +71,23 @@ func NewColorizer(shouldColor bool) *Colorizer {
 	}
 
 	return &Colorizer{
-		headingColorizer:     ansi.ColorFunc("yellow+bh"),
-		successColorizer:     ansi.ColorFunc("green+bh"),
-		failureColorizer:     ansi.ColorFunc("red+bh"),
-		exitColorizer:        ansi.ColorFunc("yellow+bh"),
-		excludeColorizer:     ansi.ColorFunc("blue+bh"),
-		successUnitColorizer: successUnitColorizer,
-		failureUnitColorizer: failureUnitColorizer,
-		exitUnitColorizer:    exitUnitColorizer,
-		excludeUnitColorizer: excludeUnitColorizer,
-		nanosecondColorizer:  ansi.ColorFunc("cyan+bh"),
-		microsecondColorizer: ansi.ColorFunc("cyan+bh"),
-		millisecondColorizer: ansi.ColorFunc("cyan+bh"),
-		secondColorizer:      ansi.ColorFunc("green+bh"),
-		minuteColorizer:      ansi.ColorFunc("yellow+bh"),
-		defaultColorizer:     ansi.ColorFunc("white+bh"),
+		headingTitleColorizer: ansi.ColorFunc("yellow+bh"),
+		headingUnitColorizer:  ansi.ColorFunc("white+bh"),
+		successColorizer:      ansi.ColorFunc("green+bh"),
+		failureColorizer:      ansi.ColorFunc("red+bh"),
+		exitColorizer:         ansi.ColorFunc("yellow+bh"),
+		excludeColorizer:      ansi.ColorFunc("blue+bh"),
+		successUnitColorizer:  successUnitColorizer,
+		failureUnitColorizer:  failureUnitColorizer,
+		exitUnitColorizer:     exitUnitColorizer,
+		excludeUnitColorizer:  excludeUnitColorizer,
+		nanosecondColorizer:   ansi.ColorFunc("cyan+bh"),
+		microsecondColorizer:  ansi.ColorFunc("cyan+bh"),
+		millisecondColorizer:  ansi.ColorFunc("cyan+bh"),
+		secondColorizer:       ansi.ColorFunc("green+bh"),
+		minuteColorizer:       ansi.ColorFunc("yellow+bh"),
+		defaultColorizer:      ansi.ColorFunc("white+bh"),
+		paddingColorizer:      ansi.ColorFunc("gray"),
 	}
 }
 
diff --git a/internal/report/report.go b/internal/report/report.go
index 20d71d5d..f668724c 100644
--- a/internal/report/report.go
+++ b/internal/report/report.go
@@ -56,12 +56,13 @@ const (
 )
 
 const (
-	ReasonRetrySucceeded Reason = "retry succeeded"
-	ReasonErrorIgnored   Reason = "error ignored"
-	ReasonRunError       Reason = "run error"
-	ReasonExcludeDir     Reason = "--queue-exclude-dir"
-	ReasonExcludeBlock   Reason = "exclude block"
-	ReasonAncestorError  Reason = "ancestor error"
+	ReasonRetrySucceeded  Reason = "retry succeeded"
+	ReasonErrorIgnored    Reason = "error ignored"
+	ReasonRunError        Reason = "run error"
+	ReasonExcludeDir      Reason = "--queue-exclude-dir"
+	ReasonExcludeBlock    Reason = "exclude block"
+	ReasonExcludeExternal Reason = "--queue-exclude-external"
+	ReasonAncestorError   Reason = "ancestor error"
 )
 
 // NewReport creates a new report.
diff --git a/internal/report/summary.go b/internal/report/summary.go
index 818eb8d2..e8965ab9 100644
--- a/internal/report/summary.go
+++ b/internal/report/summary.go
@@ -29,8 +29,9 @@ type Summary struct {
 // These are undocumented temporary environment variables that are used
 // to play with the summary, so that we can experiment with it.
 const (
-	envTmpUndocumentedReportPadder       = "TMP_UNDOCUMENTED_REPORT_PADDER"
-	envTmpUndocumentedReportUnitColorize = "TMP_UNDOCUMENTED_REPORT_UNIT_COLORIZE"
+	envTmpUndocumentedReportPadder                  = "TMP_UNDOCUMENTED_REPORT_PADDER"
+	envTmpUndocumentedReportUnitColorize            = "TMP_UNDOCUMENTED_REPORT_UNIT_COLORIZE"
+	envTmpUndocumentedColorizeDefaultSummaryPadding = "TMP_UNDOCUMENTED_COLORIZE_DEFAULT_SUMMARY_PADDING"
 )
 
 // Summarize returns a summary of the report.
@@ -39,7 +40,7 @@ func (r *Report) Summarize() *Summary {
 		workingDir:           r.workingDir,
 		shouldColor:          r.shouldColor,
 		showUnitLevelSummary: r.showUnitLevelSummary,
-		padder:               " ",
+		padder:               ".",
 		runs:                 r.Runs,
 	}
 
@@ -131,8 +132,12 @@ func (s *Summary) Write(w io.Writer) error {
 		return s.writeUnitLevelSummary(w, colorizer)
 	}
 
-	header := fmt.Sprintf("%s  %d units  %s", runSummaryHeader, s.TotalUnits(), s.TotalDurationString(colorizer))
-	if err := s.writeSummaryHeader(w, colorizer.headingColorizer(header)); err != nil {
+	header := fmt.Sprintf("%s  %s  %s",
+		colorizer.headingTitleColorizer(runSummaryHeader),
+		colorizer.headingUnitColorizer(fmt.Sprintf("%d units", s.TotalUnits())),
+		s.TotalDurationString(colorizer),
+	)
+	if err := s.writeSummaryHeader(w, header); err != nil {
 		return err
 	}
 
@@ -142,25 +147,45 @@ func (s *Summary) Write(w io.Writer) error {
 	}
 
 	if s.UnitsSucceeded > 0 {
-		if err := s.writeSummaryEntry(w, colorizer.successColorizer(successLabel), colorizer.successUnitColorizer(strconv.Itoa(s.UnitsSucceeded))); err != nil {
+		if err := s.writeSummaryEntry(
+			w,
+			colorizer.successColorizer(successLabel),
+			colorizer.successUnitColorizer(strconv.Itoa(s.UnitsSucceeded)),
+			colorizer,
+		); err != nil {
 			return err
 		}
 	}
 
 	if s.UnitsFailed > 0 {
-		if err := s.writeSummaryEntry(w, colorizer.failureColorizer(failureLabel), colorizer.failureUnitColorizer(strconv.Itoa(s.UnitsFailed))); err != nil {
+		if err := s.writeSummaryEntry(
+			w,
+			colorizer.failureColorizer(failureLabel),
+			colorizer.failureUnitColorizer(strconv.Itoa(s.UnitsFailed)),
+			colorizer,
+		); err != nil {
 			return err
 		}
 	}
 
 	if s.EarlyExits > 0 {
-		if err := s.writeSummaryEntry(w, colorizer.exitColorizer(earlyExitLabel), colorizer.exitUnitColorizer(strconv.Itoa(s.EarlyExits))); err != nil {
+		if err := s.writeSummaryEntry(
+			w,
+			colorizer.exitColorizer(earlyExitLabel),
+			colorizer.exitUnitColorizer(strconv.Itoa(s.EarlyExits)),
+			colorizer,
+		); err != nil {
 			return err
 		}
 	}
 
 	if s.Excluded > 0 {
-		if err := s.writeSummaryEntry(w, colorizer.excludeColorizer(excludeLabel), colorizer.excludeUnitColorizer(strconv.Itoa(s.Excluded))); err != nil {
+		if err := s.writeSummaryEntry(
+			w,
+			colorizer.excludeColorizer(excludeLabel),
+			colorizer.excludeUnitColorizer(strconv.Itoa(s.Excluded)),
+			colorizer,
+		); err != nil {
 			return err
 		}
 	}
@@ -178,7 +203,6 @@ const (
 	failureLabel               = "Failed"
 	earlyExitLabel             = "Early Exits"
 	excludeLabel               = "Excluded"
-	separator                  = "  "
 	separatorLineLength        = 28
 	durationAlignmentOffset    = 4
 	headerUnitCountSpacing     = 2
@@ -196,8 +220,8 @@ func (s *Summary) writeSummaryHeader(w io.Writer, value string) error {
 	return nil
 }
 
-func (s *Summary) writeSummaryEntry(w io.Writer, label string, value string) error {
-	_, err := fmt.Fprintf(w, "%s%s%s%s%s\n", prefix, label, separator, s.padding(label), value)
+func (s *Summary) writeSummaryEntry(w io.Writer, label string, value string, colorizer *Colorizer) error {
+	_, err := fmt.Fprintf(w, "%s%s%s%s\n", prefix, label, s.padding(label, colorizer), value)
 	if err != nil {
 		return err
 	}
@@ -225,8 +249,14 @@ func (s *Summary) writeUnitLevelSummary(w io.Writer, colorizer *Colorizer) error
 		headerPadding = maxUnitNameLength - defaultUnitNameLength + headerPaddingAdjustment
 	}
 
-	header := fmt.Sprintf("%s  %d units%s  %s", runSummaryHeader, s.TotalUnits(), strings.Repeat(" ", headerPadding), s.TotalDurationString(colorizer))
-	if err := s.writeSummaryHeader(w, colorizer.headingColorizer(header)); err != nil {
+	header := fmt.Sprintf(
+		"%s  %s%s  %s",
+		runSummaryHeader,
+		colorizer.headingUnitColorizer(fmt.Sprintf("%d units", s.TotalUnits())),
+		strings.Repeat(" ", headerPadding),
+		s.TotalDurationString(colorizer),
+	)
+	if err := s.writeSummaryHeader(w, colorizer.headingTitleColorizer(header)); err != nil {
 		return err
 	}
 
@@ -325,7 +355,7 @@ func (s *Summary) writeUnitDuration(w io.Writer, run *Run, colorizer *Colorizer,
 		name = strings.TrimPrefix(name, s.workingDir+string(os.PathSeparator))
 	}
 
-	padding := s.unitDurationPadding(name)
+	padding := s.unitDurationPadding(name, colorizer)
 
 	_, err := fmt.Fprintf(
 		w, "%s%s%s%s\n",
@@ -341,11 +371,11 @@ func (s *Summary) writeUnitDuration(w io.Writer, run *Run, colorizer *Colorizer,
 	return nil
 }
 
-func (s *Summary) padding(label string) string {
+func (s *Summary) padding(label string, colorizer *Colorizer) string {
 	headerUnitCountVisualPosition := s.visualLength(runSummaryHeader) + headerUnitCountSpacing
 
 	currentLabelLength := s.visualLength(label)
-	currentPosition := len(prefix) + currentLabelLength + len(separator)
+	currentPosition := len(prefix) + currentLabelLength
 
 	paddingNeeded := headerUnitCountVisualPosition - currentPosition
 
@@ -355,7 +385,21 @@ func (s *Summary) padding(label string) string {
 		paddingNeeded = 0
 	}
 
-	return strings.Repeat(s.padder, paddingNeeded)
+	padding := strings.Repeat(s.padder, paddingNeeded)
+
+	whitespaceLen := 2
+
+	if len(padding) < whitespaceLen {
+		return "  "
+	}
+
+	padding = " " + padding[1:len(padding)-1] + " "
+
+	if os.Getenv(envTmpUndocumentedColorizeDefaultSummaryPadding) == "true" {
+		return colorizer.paddingColorizer(padding)
+	}
+
+	return strings.ReplaceAll(padding, s.padder, " ")
 }
 
 // ansiRegex is used to remove ANSI escape codes from strings.
@@ -370,7 +414,7 @@ func (s *Summary) visualLength(text string) int {
 }
 
 // unitDurationPadding calculates padding for unit names to align durations with header
-func (s *Summary) unitDurationPadding(name string) string {
+func (s *Summary) unitDurationPadding(name string, colorizer *Colorizer) string {
 	maxUnitNameLength := 0
 
 	for _, run := range s.runs {
@@ -395,11 +439,17 @@ func (s *Summary) unitDurationPadding(name string) string {
 	unitPrefix := strings.Repeat(prefix, unitPrefixMultiplier)
 	currentPosition := len(unitPrefix) + len(name)
 
-	paddingNeeded := headerDurationColumn - currentPosition - durationAlignmentOffset
+	paddingNeeded := max(1, headerDurationColumn-currentPosition-durationAlignmentOffset)
 
-	if paddingNeeded < 1 {
-		paddingNeeded = 1
+	padding := strings.Repeat(s.padder, paddingNeeded)
+
+	whitespaceLen := 2
+
+	if len(padding) < whitespaceLen {
+		return "  "
 	}
 
-	return strings.Repeat(s.padder, paddingNeeded)
+	padding = " " + padding[1:len(padding)-1] + " "
+
+	return colorizer.paddingColorizer(padding)
 }
diff --git a/internal/report/writer.go b/internal/report/writer.go
index b384059e..c634bd80 100644
--- a/internal/report/writer.go
+++ b/internal/report/writer.go
@@ -88,11 +88,7 @@ func (r *Report) WriteCSV(w io.Writer) error {
 		run.mu.RLock()
 		defer run.mu.RUnlock()
 
-		name := run.Path
-
-		if r.workingDir != "" {
-			name = strings.TrimPrefix(name, r.workingDir+string(os.PathSeparator))
-		}
+		name := nameOfPath(run.Path, r.workingDir)
 
 		started := run.Started.Format(time.RFC3339)
 		ended := run.Ended.Format(time.RFC3339)
@@ -139,10 +135,7 @@ func (r *Report) WriteJSON(w io.Writer) error {
 		run.mu.RLock()
 		defer run.mu.RUnlock()
 
-		name := run.Path
-		if r.workingDir != "" {
-			name = strings.TrimPrefix(name, r.workingDir+string(os.PathSeparator))
-		}
+		name := nameOfPath(run.Path, r.workingDir)
 
 		jsonRun := JSONRun{
 			Name:    name,
@@ -232,3 +225,32 @@ func (r *Report) WriteSchema(w io.Writer) error {
 
 	return err
 }
+
+// nameOfPath returns a name for a path given a working directory.
+//
+// The logic for determining the name of a given path is:
+//
+//   - If the path is the same as the working directory, return the base name of the path.
+//     This is usually only relevant when performing a `run --all` in a unit directory.
+//
+//   - If the path is not a subdirectory of the working directory, return the path as is.
+//
+//   - Otherwise, return the path relative to the working directory, with any leading slashes removed.
+func nameOfPath(path string, workingDir string) string {
+	// If the path is the same as the working directory,
+	// return the base name of the path.
+	if path == workingDir {
+		return filepath.Base(path)
+	}
+
+	// If the path is not a subdirectory of the working directory,
+	// return the path as is.
+	if !strings.HasPrefix(path, workingDir) {
+		return path
+	}
+
+	path = strings.TrimPrefix(path, workingDir)
+	path = strings.TrimPrefix(path, string(os.PathSeparator))
+
+	return path
+}



Test patch:
diff --git a/internal/report/report_test.go b/internal/report/report_test.go
index ad9a97f6..180b8330 100644
--- a/internal/report/report_test.go
+++ b/internal/report/report_test.go
@@ -1072,7 +1072,7 @@ func TestWriteUnitLevelSummary(t *testing.T) {
 ❯❯ Run Summary  1 units  x
    ────────────────────────────
    Succeeded (1)
-      single-run         x
+      single-run ....... x
 `,
 		},
 		{
@@ -1096,9 +1096,9 @@ func TestWriteUnitLevelSummary(t *testing.T) {
 ❯❯ Run Summary  3 units  x
    ────────────────────────────
    Succeeded (3)
-      long-run           x
-      medium-run         x
-      short-run          x
+      long-run ......... x
+      medium-run ....... x
+      short-run ........ x
 `,
 		},
 		{
@@ -1124,12 +1124,12 @@ func TestWriteUnitLevelSummary(t *testing.T) {
 ❯❯ Run Summary  4 units  x
    ────────────────────────────
    Succeeded (2)
-      success-1          x
-      success-2          x
+      success-1 ........ x
+      success-2 ........ x
    Failed (1)
-      fail-run           x
+      fail-run ......... x
    Excluded (1)
-      excluded-run       x
+      excluded-run ..... x
 `,
 		},
 		{
@@ -1152,9 +1152,9 @@ func TestWriteUnitLevelSummary(t *testing.T) {
 ❯❯ Run Summary  3 units  x
    ────────────────────────────
    Succeeded (3)
-      a                  x
-      b                  x
-      c                  x
+      a ................ x
+      b ................ x
+      c ................ x
 `,
 		},
 		{
@@ -1201,17 +1201,14 @@ func TestWriteUnitLevelSummary(t *testing.T) {
 
 			// Replace the duration with x since we can't control the actual duration in tests
 			output := buf.String()
-			re := regexp.MustCompile(`Duration:(\s+).*`)
-			output = re.ReplaceAllString(output, "Duration:${1}x")
 
-			// Replace the duration in the header line (e.g., "3 units  8µs" -> "3 units  x")
-			// Updated to handle variable spacing for long unit names
-			re = regexp.MustCompile(`(\d+ units\s+)(\s*)(\d+[µnms]+|\d+[smh]|[^\s]+)`)
-			output = re.ReplaceAllString(output, "${1}${2}x")
+			// Replace the header duration with x
+			re := regexp.MustCompile(`❯❯ Run Summary  (\d+) units(\s+)(\d+.+)`)
+			output = re.ReplaceAllString(output, "❯❯ Run Summary  ${1} units${2}x")
 
-			// Replace the actual durations with x (for the new clean format without colons)
-			re = regexp.MustCompile(`([ ]{6})([^\s]+)(\s+)(\d+[µnms]+|\d+[smh])`)
-			output = re.ReplaceAllString(output, "${1}${2}${3}x")
+			// Replace all the unit level summaries
+			re = regexp.MustCompile(`([ ]{6})([^ ]+)( )([^ ]*)( )(\d+.+)`)
+			output = re.ReplaceAllString(output, "${1}${2}${3}${4}${5}x")
 
 			expected := strings.TrimSpace(tt.expected)
 			assert.Equal(t, expected, strings.TrimSpace(output))
diff --git a/test/fixtures/external-dependency/terragrunt.hcl b/test/fixtures/external-dependency/terragrunt.hcl
index edaffddb..107d5dcd 100644
--- a/test/fixtures/external-dependency/terragrunt.hcl
+++ b/test/fixtures/external-dependency/terragrunt.hcl
@@ -1,3 +1,7 @@
+feature "dep" {
+  default = "/tmp/external"
+}
+
 dependencies {
-  paths = ["/tmp/external-46521694"]
+  paths = [feature.dep.value]
 }
diff --git a/test/integration_destroy_test.go b/test/integration_destroy_test.go
index c1d7755e..302ae887 100644
--- a/test/integration_destroy_test.go
+++ b/test/integration_destroy_test.go
@@ -264,11 +264,6 @@ func TestTerragruntSkipConfirmExternalDependencies(t *testing.T) {
 	helpers.CleanupTerraformFolder(t, tmpEnvPath)
 	testPath := util.JoinPath(tmpEnvPath, testFixtureExternalDependency)
 
-	t.Cleanup(func() {
-		os.RemoveAll(filepath.ToSlash("/tmp/external-46521694"))
-	})
-	require.NoError(t, os.Mkdir(filepath.ToSlash("/tmp/external-46521694"), 0755))
-
 	helpers.CreateGitRepo(t, tmpEnvPath)
 
 	stdout := bytes.Buffer{}
@@ -278,7 +273,18 @@ func TestTerragruntSkipConfirmExternalDependencies(t *testing.T) {
 	oldStdout := os.Stderr
 	os.Stderr = w
 
-	err := helpers.RunTerragruntCommand(t, "terragrunt destroy --working-dir "+testPath, &stdout, &stderr)
+	tmp := t.TempDir()
+
+	err := helpers.RunTerragruntCommand(
+		t,
+		fmt.Sprintf(
+			"terragrunt destroy --feature dep=%s --working-dir %s",
+			tmp,
+			testPath,
+		),
+		&stdout,
+		&stderr,
+	)
 	os.Stderr = oldStdout
 	require.NoError(t, w.Close())
 
@@ -294,7 +300,7 @@ func TestTerragruntSkipConfirmExternalDependencies(t *testing.T) {
 
 	require.NoError(t, err)
 	assert.NotContains(t, captured, "Should Terragrunt apply the external dependency?")
-	assert.NotContains(t, captured, "/tmp/external1")
+	assert.NotContains(t, captured, tmp)
 }
 
 func TestStorePlanFilesRunAllDestroy(t *testing.T) {
diff --git a/test/integration_report_test.go b/test/integration_report_test.go
index eb841944..49943e59 100644
--- a/test/integration_report_test.go
+++ b/test/integration_report_test.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"os"
+	"path/filepath"
 	"regexp"
 	"sort"
 	"strings"
@@ -438,29 +439,102 @@ func TestTerragruntReportExperimentWithUnitTiming(t *testing.T) {
 	stdoutStr = sortLinesWithinCategories(stdoutStr)
 
 	// The expected format has units grouped by status with timing (sorted alphabetically within categories)
-	expectedOutput := `❯❯ Run Summary  13 units  x
+	expectedOutput := `
+❯❯ Run Summary  13 units  x
    ────────────────────────────
    Succeeded (4)
-      error-ignore        x
-      first-success       x
-      retry-success       x
-      second-success      x
+      error-ignore ...... x
+      first-success ..... x
+      retry-success ..... x
+      second-success .... x
    Failed (3)
-      chain-a             x
-      first-failure       x
-      second-failure      x
+      chain-a ........... x
+      first-failure ..... x
+      second-failure .... x
    Early Exits (4)
-      chain-b             x
-      chain-c             x
-      first-early-exit    x
-      second-early-exit   x
+      chain-b ........... x
+      chain-c ........... x
+      first-early-exit .. x
+      second-early-exit . x
    Excluded (2)
-      first-exclude       x
-      second-exclude      x`
+      first-exclude ..... x
+      second-exclude .... x`
 
 	assert.Equal(t, strings.TrimSpace(expectedOutput), strings.TrimSpace(stdoutStr))
 }
 
+func TestReportWithExternalDependenciesExcluded(t *testing.T) {
+	t.Parallel()
+
+	cleanupTerraformFolder(t, testFixtureExternalDependency)
+	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureExternalDependency)
+	rootPath := filepath.Join(tmpEnvPath, testFixtureExternalDependency)
+
+	dep := t.TempDir()
+
+	f, err := os.Create(filepath.Join(dep, "terragrunt.hcl"))
+	require.NoError(t, err)
+	f.Close()
+
+	f, err = os.Create(filepath.Join(dep, "main.tf"))
+	require.NoError(t, err)
+	f.Close()
+
+	reportDir := t.TempDir()
+	reportFile := filepath.Join(reportDir, "report.json")
+
+	stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(
+		t,
+		fmt.Sprintf(
+			"terragrunt run --all plan --queue-exclude-external --experiment report --feature dep=%s --working-dir %s --report-file %s",
+			dep,
+			rootPath,
+			reportFile,
+		),
+	)
+
+	// The command should succeed without "run not found in report" errors
+	require.NoError(t, err)
+
+	// Verify that no "run not found in report" errors appear in stderr
+	assert.NotContains(t, stderr, "run not found in report")
+	assert.Contains(t, stdout, "Run Summary")
+
+	// Verify that the report file exists
+	assert.FileExists(t, reportFile)
+
+	// Read the report file
+	reportContent, err := os.ReadFile(reportFile)
+	require.NoError(t, err)
+
+	// Verify that the report file contains the expected content
+	var report []map[string]interface{}
+	err = json.Unmarshal(reportContent, &report)
+	require.NoError(t, err)
+
+	// Verify that the report file contains the expected content
+	assert.Len(t, report, 2)
+
+	expected := []struct {
+		name   string
+		result string
+		reason string
+	}{
+		// The first run is always going to be the external dependency,
+		// as it has an instant runtime.
+		{name: dep, result: "excluded", reason: "--queue-exclude-external"},
+		{name: "external-dependency", result: "succeeded"},
+	}
+
+	for i, r := range report {
+		assert.Equal(t, expected[i].name, r["Name"])
+		assert.Equal(t, expected[i].result, r["Result"])
+		if expected[i].reason != "" {
+			assert.Equal(t, expected[i].reason, r["Reason"])
+		}
+	}
+}
+
 // lineType represents the type of line we're processing
 type lineType int