Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package terragrunt for openSUSE:Factory 
checked in at 2025-06-12 15:53:50
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/terragrunt (Old)
 and      /work/SRC/openSUSE:Factory/.terragrunt.new.19631 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "terragrunt"

Thu Jun 12 15:53:50 2025 rev:235 rq:1285002 version:0.81.5

Changes:
--------
--- /work/SRC/openSUSE:Factory/terragrunt/terragrunt.changes    2025-06-11 
16:26:54.617435343 +0200
+++ /work/SRC/openSUSE:Factory/.terragrunt.new.19631/terragrunt.changes 
2025-06-12 15:55:37.775077408 +0200
@@ -1,0 +2,42 @@
+Thu Jun 12 05:22:59 UTC 2025 - Johannes Kastl 
<opensuse_buildserv...@ojkastl.de>
+
+- Update to version 0.81.5:
+  * New Features
+    - Terragrunt now supports credentials stored in .terraformrc
+      files when fetching from private registries, in addition to
+      the fallback mechanism of using TG_TF_REGISTRY_TOKEN.
+  * What's Changed
+    - feat: support credential tokens for getter (#4047)
+
+-------------------------------------------------------------------
+Thu Jun 12 05:15:37 UTC 2025 - Johannes Kastl 
<opensuse_buildserv...@ojkastl.de>
+
+- Update to version 0.81.4:
+  * Experiments Updated
+    The reports experiment now supports the --summary-unit-duration
+    flag
+    As part of delivering #3628 , the reports experiment has been
+    updated to support optionally displaying unit-level duration
+    information in the Run Summary.
+    You can now optionally display the duration for each unit run
+    as part of the Run Summary by adding the
+    --summary-unit-duration flag to your run commands:
+      $ terragrunt run --all plan --summary-unit-duration
+
+      # Omitted for brevity...
+
+      ❯❯ Run Summary
+         Duration:   10m
+            long-running-unit:    10m
+            medium-running-unit:  12s
+            short-running-unit:   5ms
+         Units:      3
+         Succeeded:  3
+
+    By default, this information will be omitted.
+    For more information, see Showing unit durations in the docs.
+  * What's Changed
+    - feat: Adding `--summary-unit-duration` (#4410)
+    - feat: Improving testing & documentation (#4409)
+
+-------------------------------------------------------------------

Old:
----
  terragrunt-0.81.3.obscpio

New:
----
  terragrunt-0.81.5.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ terragrunt.spec ++++++
--- /var/tmp/diff_new_pack.3I0hr9/_old  2025-06-12 15:55:39.051130215 +0200
+++ /var/tmp/diff_new_pack.3I0hr9/_new  2025-06-12 15:55:39.051130215 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           terragrunt
-Version:        0.81.3
+Version:        0.81.5
 Release:        0
 Summary:        Thin wrapper for Terraform for working with multiple Terraform 
modules
 License:        MIT

++++++ _service ++++++
--- /var/tmp/diff_new_pack.3I0hr9/_old  2025-06-12 15:55:39.119133029 +0200
+++ /var/tmp/diff_new_pack.3I0hr9/_new  2025-06-12 15:55:39.123133194 +0200
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/gruntwork-io/terragrunt</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">v0.81.3</param>
+    <param name="revision">v0.81.5</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.3I0hr9/_old  2025-06-12 15:55:39.143134022 +0200
+++ /var/tmp/diff_new_pack.3I0hr9/_new  2025-06-12 15:55:39.147134187 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/gruntwork-io/terragrunt</param>
-              <param 
name="changesrevision">fe11a2ea3834f3cc3cf944569cb8a1ceb4f76df1</param></service></servicedata>
+              <param 
name="changesrevision">5b4250e925d54b2e82de32ba0119f9ea1883512a</param></service></servicedata>
 (No newline at EOF)
 

++++++ terragrunt-0.81.3.obscpio -> terragrunt-0.81.5.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/terragrunt-0.81.3/cli/commands/common/graph/graph.go 
new/terragrunt-0.81.5/cli/commands/common/graph/graph.go
--- old/terragrunt-0.81.3/cli/commands/common/graph/graph.go    2025-06-10 
16:58:01.000000000 +0200
+++ new/terragrunt-0.81.5/cli/commands/common/graph/graph.go    2025-06-11 
22:15:28.000000000 +0200
@@ -60,6 +60,10 @@
                        r.WithFormat(opts.ReportFormat)
                }
 
+               if opts.SummaryUnitDuration {
+                       r.WithShowUnitTiming()
+               }
+
                stackOpts = append(stackOpts, configstack.WithReport(r))
 
                if opts.ReportSchemaFile != "" {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/terragrunt-0.81.3/cli/commands/common/runall/runall.go 
new/terragrunt-0.81.5/cli/commands/common/runall/runall.go
--- old/terragrunt-0.81.3/cli/commands/common/runall/runall.go  2025-06-10 
16:58:01.000000000 +0200
+++ new/terragrunt-0.81.5/cli/commands/common/runall/runall.go  2025-06-11 
22:15:28.000000000 +0200
@@ -61,6 +61,10 @@
                        r.WithFormat(opts.ReportFormat)
                }
 
+               if opts.SummaryUnitDuration {
+                       r.WithShowUnitTiming()
+               }
+
                stackOpts = append(stackOpts, configstack.WithReport(r))
 
                if opts.ReportSchemaFile != "" {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/terragrunt-0.81.3/cli/commands/run/flags.go 
new/terragrunt-0.81.5/cli/commands/run/flags.go
--- old/terragrunt-0.81.3/cli/commands/run/flags.go     2025-06-10 
16:58:01.000000000 +0200
+++ new/terragrunt-0.81.5/cli/commands/run/flags.go     2025-06-11 
22:15:28.000000000 +0200
@@ -29,6 +29,7 @@
        UnitsThatIncludeFlagName               = "units-that-include"
        DependencyFetchOutputFromStateFlagName = 
"dependency-fetch-output-from-state"
        UsePartialParseConfigCacheFlagName     = 
"use-partial-parse-config-cache"
+       SummaryUnitDurationFlagName            = "summary-unit-duration"
 
        BackendBootstrapFlagName        = "backend-bootstrap"
        BackendRequireBootstrapFlagName = "backend-require-bootstrap"
@@ -548,6 +549,13 @@
                        Usage:       `Disable the summary output at the end of 
a run.`,
                }),
 
+               flags.NewFlag(&cli.BoolFlag{
+                       Name:        SummaryUnitDurationFlagName,
+                       EnvVars:     
tgPrefix.EnvVars(SummaryUnitDurationFlagName),
+                       Destination: &opts.SummaryUnitDuration,
+                       Usage:       `Show duration information for each unit 
in the summary output.`,
+               }),
+
                flags.NewFlag(&cli.GenericFlag[string]{
                        Name:    ReportFileFlagName,
                        EnvVars: tgPrefix.EnvVars(ReportFileFlagName),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/terragrunt-0.81.3/docs/_docs/02_features/16-run-report.md 
new/terragrunt-0.81.5/docs/_docs/02_features/16-run-report.md
--- old/terragrunt-0.81.3/docs/_docs/02_features/16-run-report.md       
2025-06-10 16:58:01.000000000 +0200
+++ new/terragrunt-0.81.5/docs/_docs/02_features/16-run-report.md       
2025-06-11 22:15:28.000000000 +0200
@@ -41,6 +41,26 @@
 - Excluded: The number of units that were excluded from the run (if any were).
 - Early Exits: The number of units that exited early, due to a failure in a 
dependency (if any did).
 
+### Showing Unit Durations
+
+You can enable showing the duration of each unit in the run summary by using 
the `--summary-unit-duration` flag.
+
+```bash
+$ terragrunt run --all plan --summary-unit-duration
+
+# Omitted for brevity...
+
+❯❯ Run Summary
+   Duration:   10m
+      long-running-unit:    10m
+      medium-running-unit:  12s
+      short-running-unit:   5ms
+   Units:      3
+   Succeeded:  3
+```
+
+The units are sorted by duration, with the longest-running units shown first.
+
 ### Disabling the summary
 
 You can disable the summary output by using the `--summary-disable` flag.
@@ -215,7 +235,8 @@
 
 Causes indicate the specific reason for a given result, and are generally not 
guessable. These provide information on the exact mechanism that caused the 
result.
 
-- `retry succeeded`: You will find the name of the `retry` block that resulted 
in a successful retry of the unit run.
 - `error ignored`: You will find the name of the `ignore` block that resulted 
in the error being ignored.
 - `run error`: You will find the actual error message of the unit that failed.
-- `ancestor error`: You will find the name of the unit that failed, and the 
error message of the failure.
+- `ancestor error`: You will find the name of the unit that failed.
+
+The `retry succeeded` reason does not have a cause. The reason for this is 
that backwards compatibility with the deprecated 
[retryable_errors](/docs/reference/config-blocks-and-attributes/#retryable_errors)
 attribute prevents consistent reporting of the cause, as the 
`retryable_errors` attribute doesn't have a label. In the future, once the 
`retryable_errors` attribute is removed, a cause can be added here.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/terragrunt-0.81.3/docs/_docs/04_reference/02-cli-options.md 
new/terragrunt-0.81.5/docs/_docs/04_reference/02-cli-options.md
--- old/terragrunt-0.81.3/docs/_docs/04_reference/02-cli-options.md     
2025-06-10 16:58:01.000000000 +0200
+++ new/terragrunt-0.81.5/docs/_docs/04_reference/02-cli-options.md     
2025-06-11 22:15:28.000000000 +0200
@@ -1710,6 +1710,15 @@
 
 For more information, see the [Run 
Report](/docs/features/run-report#disabling-the-summary) feature.
 
+### summary-unit-duration
+
+**CLI Arg**: `--summary-unit-duration`<br/>
+**Environment Variable**: `TG_SUMMARY_UNIT_DURATION`<br/>
+
+When enabled, Terragrunt will show the duration of each unit in the run 
summary. The units are sorted by duration, with the longest-running units shown 
first.
+
+For more information, see the [Run Report](/docs/features/run-report) feature.
+
 ### iam-assume-role
 
 **CLI Arg**: `--iam-assume-role`<br/>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/terragrunt-0.81.3/docs-starlight/src/content/docs/02-features/16-run-report.mdx
 
new/terragrunt-0.81.5/docs-starlight/src/content/docs/02-features/16-run-report.mdx
--- 
old/terragrunt-0.81.3/docs-starlight/src/content/docs/02-features/16-run-report.mdx
 2025-06-10 16:58:01.000000000 +0200
+++ 
new/terragrunt-0.81.5/docs-starlight/src/content/docs/02-features/16-run-report.mdx
 2025-06-11 22:15:28.000000000 +0200
@@ -40,6 +40,26 @@
 - Excluded: The number of units that were excluded from the run (if any were).
 - Early Exits: The number of units that exited early, due to a failure in a 
dependency (if any did).
 
+### Showing Unit Durations
+
+You can enable showing the duration of each unit in the run summary by using 
the `--summary-unit-duration` flag.
+
+```bash
+$ terragrunt run --all plan --summary-unit-duration
+
+# Omitted for brevity...
+
+❯❯ Run Summary
+   Duration:   10m
+      long-running-unit:    10m
+      medium-running-unit:  12s
+      short-running-unit:   5ms
+   Units:      3
+   Succeeded:  3
+```
+
+The units are sorted by duration, with the longest-running units shown first.
+
 ### Disabling the summary
 
 You can disable the summary output by using the `--summary-disable` flag.
@@ -214,7 +234,10 @@
 
 Causes indicate the specific reason for a given result, and are generally not 
guessable. These provide information on the exact mechanism that caused the 
result.
 
-- `retry succeeded`: You will find the name of the `retry` block that resulted 
in a successful retry of the unit run.
 - `error ignored`: You will find the name of the `ignore` block that resulted 
in the error being ignored.
 - `run error`: You will find the actual error message of the unit that failed.
-- `ancestor error`: You will find the name of the unit that failed, and the 
error message of the failure.
+- `ancestor error`: You will find the name of the unit that failed.
+
+<Aside type="note">
+  The `retry succeeded` reason does not have a cause. The reason for this is 
that backwards compatibility with the 
[retryable_errors](/docs/reference/hcl/attributes/#retryable_errors) attribute 
prevents consistent reporting of the cause, as the `retryable_errors` attribute 
doesn't have a label. In the future, once the `retryable_errors` attribute is 
removed, a cause can be added here.
+</Aside>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/terragrunt-0.81.3/docs-starlight/src/data/commands/run.mdx 
new/terragrunt-0.81.5/docs-starlight/src/data/commands/run.mdx
--- old/terragrunt-0.81.3/docs-starlight/src/data/commands/run.mdx      
2025-06-10 16:58:01.000000000 +0200
+++ new/terragrunt-0.81.5/docs-starlight/src/data/commands/run.mdx      
2025-06-11 22:15:28.000000000 +0200
@@ -69,6 +69,7 @@
   - source-map
   - source-update
   - summary-disable
+  - summary-unit-duration
   - tf-forward-stdout
   - tf-path
   - units-that-include
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/terragrunt-0.81.3/docs-starlight/src/data/flags/summary-unit-duration.mdx 
new/terragrunt-0.81.5/docs-starlight/src/data/flags/summary-unit-duration.mdx
--- 
old/terragrunt-0.81.3/docs-starlight/src/data/flags/summary-unit-duration.mdx   
    1970-01-01 01:00:00.000000000 +0100
+++ 
new/terragrunt-0.81.5/docs-starlight/src/data/flags/summary-unit-duration.mdx   
    2025-06-11 22:15:28.000000000 +0200
@@ -0,0 +1,11 @@
+---
+name: summary-unit-duration
+description: Shows the duration of each unit in the run summary.
+type: bool
+env:
+  - TG_SUMMARY_UNIT_DURATION
+---
+
+When enabled, Terragrunt will show the duration of each unit in the run 
summary. The units are sorted by duration, with the longest-running units shown 
first.
+
+For more information, see the [Run Report](/docs/features/run-report) feature.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/terragrunt-0.81.3/internal/report/report.go 
new/terragrunt-0.81.5/internal/report/report.go
--- old/terragrunt-0.81.3/internal/report/report.go     2025-06-10 
16:58:01.000000000 +0200
+++ new/terragrunt-0.81.5/internal/report/report.go     2025-06-11 
22:15:28.000000000 +0200
@@ -21,11 +21,12 @@
 
 // Report captures data for a report/summary.
 type Report struct {
-       workingDir  string
-       format      Format
-       Runs        []*Run
-       mu          sync.RWMutex
-       shouldColor bool
+       workingDir     string
+       format         Format
+       Runs           []*Run
+       mu             sync.RWMutex
+       shouldColor    bool
+       showUnitTiming bool
 }
 
 // Run captures data for a run.
@@ -61,12 +62,14 @@
        firstRunStart  *time.Time
        lastRunEnd     *time.Time
        padder         string
-       TotalUnits     int
+       workingDir     string
+       runs           []*Run
        UnitsSucceeded int
        UnitsFailed    int
        EarlyExits     int
        Excluded       int
        shouldColor    bool
+       showUnitTiming bool
 }
 
 // Colorizer is a colorizer for the run summary output.
@@ -76,6 +79,7 @@
        failureColorizer     func(string) string
        exitColorizer        func(string) string
        excludeColorizer     func(string) string
+       nanosecondColorizer  func(string) string
        microsecondColorizer func(string) string
        millisecondColorizer func(string) string
        secondColorizer      func(string) string
@@ -92,6 +96,7 @@
                        failureColorizer:     func(s string) string { return s 
},
                        exitColorizer:        func(s string) string { return s 
},
                        excludeColorizer:     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 
},
@@ -106,6 +111,7 @@
                failureColorizer:     ansi.ColorFunc("red+bh"),
                exitColorizer:        ansi.ColorFunc("yellow+bh"),
                excludeColorizer:     ansi.ColorFunc("blue+bh"),
+               nanosecondColorizer:  ansi.ColorFunc("cyan+bh"),
                microsecondColorizer: ansi.ColorFunc("cyan+bh"),
                millisecondColorizer: ansi.ColorFunc("cyan+bh"),
                secondColorizer:      ansi.ColorFunc("green+bh"),
@@ -114,6 +120,27 @@
        }
 }
 
+// colorDuration returns the duration as a string, colored based on the 
duration.
+func (c *Colorizer) colorDuration(duration time.Duration) string {
+       if duration < time.Microsecond {
+               return c.nanosecondColorizer(fmt.Sprintf("%dns", 
duration.Nanoseconds()))
+       }
+
+       if duration < time.Millisecond {
+               return c.microsecondColorizer(fmt.Sprintf("%dµs", 
duration.Microseconds()))
+       }
+
+       if duration < time.Second {
+               return c.millisecondColorizer(fmt.Sprintf("%dms", 
duration.Milliseconds()))
+       }
+
+       if duration < time.Minute {
+               return c.secondColorizer(fmt.Sprintf("%ds", 
int(duration.Seconds())))
+       }
+
+       return c.minuteColorizer(fmt.Sprintf("%dm", int(duration.Minutes())))
+}
+
 // NewReport creates a new report.
 func NewReport() *Report {
        report := &Report{
@@ -148,6 +175,15 @@
        return r
 }
 
+// WithShowUnitTiming sets the showUnitTiming flag for the report.
+//
+// When enabled, the summary of the report will include timings for each unit.
+func (r *Report) WithShowUnitTiming() *Report {
+       r.showUnitTiming = true
+
+       return r
+}
+
 // ErrPathMustBeAbsolute is returned when a report run path is not absolute.
 var ErrPathMustBeAbsolute = errors.New("report run path must be absolute")
 
@@ -345,9 +381,11 @@
 // Summarize returns a summary of the report.
 func (r *Report) Summarize() *Summary {
        summary := &Summary{
-               TotalUnits:  len(r.Runs),
-               shouldColor: r.shouldColor,
-               padder:      " ",
+               workingDir:     r.workingDir,
+               shouldColor:    r.shouldColor,
+               showUnitTiming: r.showUnitTiming,
+               padder:         " ",
+               runs:           r.Runs,
        }
 
        if os.Getenv(envTmpUndocumentedReportPadder) != "" {
@@ -365,6 +403,10 @@
        return summary
 }
 
+func (s *Summary) TotalUnits() int {
+       return len(s.runs)
+}
+
 func (s *Summary) Update(run *Run) {
        run.mu.RLock()
        defer run.mu.RUnlock()
@@ -403,19 +445,7 @@
 func (s *Summary) TotalDurationString(colorizer *Colorizer) string {
        duration := s.TotalDuration()
 
-       if duration < time.Millisecond {
-               return colorizer.microsecondColorizer(fmt.Sprintf("%dµs", 
duration.Microseconds()))
-       }
-
-       if duration < time.Second {
-               return colorizer.millisecondColorizer(fmt.Sprintf("%dms", 
duration.Milliseconds()))
-       }
-
-       if duration < time.Minute {
-               return colorizer.secondColorizer(fmt.Sprintf("%ds", 
int(duration.Seconds())))
-       }
-
-       return colorizer.minuteColorizer(fmt.Sprintf("%dm", 
int(duration.Minutes())))
+       return colorizer.colorDuration(duration)
 }
 
 // WriteToFile writes the report to a file.
@@ -688,7 +718,13 @@
                return err
        }
 
-       if err := s.writeSummaryEntry(w, unitsLabel, 
colorizer.defaultColorizer(strconv.Itoa(s.TotalUnits))); err != nil {
+       if s.showUnitTiming {
+               if err := s.writeUnitsTiming(w, colorizer); err != nil {
+                       return err
+               }
+       }
+
+       if err := s.writeSummaryEntry(w, unitsLabel, 
colorizer.defaultColorizer(strconv.Itoa(s.TotalUnits()))); err != nil {
                return err
        }
 
@@ -720,15 +756,16 @@
 }
 
 const (
-       prefix           = "   "
-       runSummaryHeader = "❯❯ Run Summary"
-       durationLabel    = "Duration"
-       unitsLabel       = "Units"
-       successLabel     = "Succeeded"
-       failureLabel     = "Failed"
-       earlyExitLabel   = "Early Exits"
-       excludeLabel     = "Excluded"
-       separator        = ": "
+       prefix               = "   "
+       unitPrefixMultiplier = 2
+       runSummaryHeader     = "❯❯ Run Summary"
+       durationLabel        = "Duration"
+       unitsLabel           = "Units"
+       successLabel         = "Succeeded"
+       failureLabel         = "Failed"
+       earlyExitLabel       = "Early Exits"
+       excludeLabel         = "Excluded"
+       separator            = ": "
 )
 
 func (s *Summary) writeSummaryHeader(w io.Writer, value string) error {
@@ -749,6 +786,54 @@
        return nil
 }
 
+func (s *Summary) writeUnitsTiming(w io.Writer, colorizer *Colorizer) error {
+       errs := []error{}
+
+       // Sort the runs by duration, longest first.
+       sortedRuns := slices.Clone(s.runs)
+       slices.SortFunc(sortedRuns, func(a, b *Run) int {
+               aDuration := a.Ended.Sub(a.Started)
+               bDuration := b.Ended.Sub(b.Started)
+
+               return int(bDuration - aDuration)
+       })
+
+       for _, run := range sortedRuns {
+               if err := s.writeUnitTiming(w, run, colorizer); err != nil {
+                       errs = append(errs, err)
+               }
+       }
+
+       if len(errs) > 0 {
+               return errors.Join(errs...)
+       }
+
+       return nil
+}
+
+func (s *Summary) writeUnitTiming(w io.Writer, run *Run, colorizer *Colorizer) 
error {
+       duration := run.Ended.Sub(run.Started)
+
+       name := run.Path
+       if s.workingDir != "" {
+               name = strings.TrimPrefix(name, 
s.workingDir+string(os.PathSeparator))
+       }
+
+       _, err := fmt.Fprintf(
+               w, "%s%s%s%s %s\n",
+               strings.Repeat(prefix, unitPrefixMultiplier),
+               name,
+               separator,
+               s.unitDurationPadding(name),
+               colorizer.colorDuration(duration),
+       )
+       if err != nil {
+               return err
+       }
+
+       return nil
+}
+
 func (s *Summary) longestLineLength() int {
        // Start with the length of the labels
        // That are always present
@@ -795,3 +880,26 @@
 
        return strings.Repeat(s.padder, longestLineLength-labelLength)
 }
+
+func (s *Summary) longestUnitDurationLineLength() int {
+       names := make([]int, 0, len(s.runs))
+
+       for _, run := range s.runs {
+               name := run.Path
+               if s.workingDir != "" {
+                       name = strings.TrimPrefix(name, 
s.workingDir+string(os.PathSeparator))
+               }
+
+               names = append(names, len(name))
+       }
+
+       return slices.Max(names) + (len(prefix) * unitPrefixMultiplier) + 
len(separator)
+}
+
+func (s *Summary) unitDurationPadding(name string) string {
+       longestLineLength := s.longestUnitDurationLineLength()
+
+       labelLength := (len(prefix) * unitPrefixMultiplier) + len(name) + 
len(separator)
+
+       return strings.Repeat(s.padder, longestLineLength-labelLength)
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/terragrunt-0.81.3/internal/report/report_test.go 
new/terragrunt-0.81.5/internal/report/report_test.go
--- old/terragrunt-0.81.3/internal/report/report_test.go        2025-06-10 
16:58:01.000000000 +0200
+++ new/terragrunt-0.81.5/internal/report/report_test.go        2025-06-11 
22:15:28.000000000 +0200
@@ -186,6 +186,86 @@
        }
 }
 
+func TestEndRunAlreadyEnded(t *testing.T) {
+       t.Parallel()
+
+       tmp := t.TempDir()
+
+       tests := []struct {
+               name           string
+               initialResult  report.Result
+               expectedResult report.Result
+               secondResult   report.Result
+               initialOptions []report.EndOption
+               secondOptions  []report.EndOption
+       }{
+               {
+                       name:           "already ended with early exit is not 
overwritten",
+                       initialResult:  report.ResultEarlyExit,
+                       secondResult:   report.ResultSucceeded,
+                       expectedResult: report.ResultEarlyExit,
+               },
+               {
+                       name:           "already ended with excluded is not 
overwritten",
+                       initialResult:  report.ResultExcluded,
+                       secondResult:   report.ResultSucceeded,
+                       expectedResult: report.ResultExcluded,
+               },
+               {
+                       name:           "already ended with retry succeeded is 
overwritten",
+                       initialResult:  report.ResultSucceeded,
+                       initialOptions: 
[]report.EndOption{report.WithReason(report.ReasonRetrySucceeded)},
+                       secondResult:   report.ResultSucceeded,
+                       expectedResult: report.ResultSucceeded,
+               },
+               {
+                       name:           "already ended with retry failed is 
overwritten",
+                       initialResult:  report.ResultSucceeded,
+                       initialOptions: 
[]report.EndOption{report.WithReason(report.ReasonRetrySucceeded)},
+                       secondResult:   report.ResultFailed,
+                       expectedResult: report.ResultFailed,
+               },
+               {
+                       name:           "already ended with error ignored is 
overwritten",
+                       initialResult:  report.ResultSucceeded,
+                       initialOptions: 
[]report.EndOption{report.WithReason(report.ReasonErrorIgnored)},
+                       secondResult:   report.ResultSucceeded,
+                       expectedResult: report.ResultSucceeded,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       t.Parallel()
+
+                       // Create a new report and run for each test case
+                       r := report.NewReport()
+                       runName := filepath.Join(tmp, tt.name)
+                       run := newRun(t, runName)
+                       r.AddRun(run)
+
+                       // Set up initial options with the initial result
+                       initialOptions := append(tt.initialOptions, 
report.WithResult(tt.initialResult))
+
+                       // End the run with the initial state
+                       err := r.EndRun(runName, initialOptions...)
+                       require.NoError(t, err)
+
+                       // Set up second options with the second result
+                       secondOptions := append(tt.secondOptions, 
report.WithResult(tt.secondResult))
+
+                       // Then try to end it again with a different state
+                       err = r.EndRun(runName, secondOptions...)
+                       require.NoError(t, err)
+
+                       // Verify that the result is the expected one
+                       endedRun, err := r.GetRun(runName)
+                       require.NoError(t, err)
+                       assert.Equal(t, tt.expectedResult, endedRun.Result)
+               })
+       }
+}
+
 func TestSummarize(t *testing.T) {
        t.Parallel()
 
@@ -253,7 +333,7 @@
                        }
 
                        summary := r.Summarize()
-                       assert.Equal(t, tt.wantTotalUnits, summary.TotalUnits)
+                       assert.Equal(t, tt.wantTotalUnits, summary.TotalUnits())
                        assert.Equal(t, tt.wantSucceeded, 
summary.UnitsSucceeded)
                        assert.Equal(t, tt.wantFailed, summary.UnitsFailed)
                        assert.Equal(t, tt.wantEarlyExits, summary.EarlyExits)
@@ -962,6 +1042,101 @@
        assert.Equal(t, "ignore-block", *ignored.Cause)
 }
 
+func TestWriteUnitsTiming(t *testing.T) {
+       t.Parallel()
+
+       tmp := t.TempDir()
+
+       tests := []struct {
+               name     string
+               setup    func(*report.Report)
+               expected string
+       }{
+               {
+                       name: "empty runs",
+                       setup: func(r *report.Report) {
+                               // No runs added
+                       },
+                       expected: `
+❯❯ Run Summary
+   Duration:  x
+   Units:     0
+`,
+               },
+               {
+                       name: "single run",
+                       setup: func(r *report.Report) {
+                               run := newRun(t, filepath.Join(tmp, 
"single-run"))
+                               r.AddRun(run)
+                               r.EndRun(run.Path)
+                       },
+                       expected: `
+❯❯ Run Summary
+   Duration:   x
+      single-run:  x
+   Units:      1
+   Succeeded:  1
+`,
+               },
+               {
+                       name: "multiple runs sorted by duration",
+                       setup: func(r *report.Report) {
+                               // Add runs with different durations
+                               longRun := newRun(t, filepath.Join(tmp, 
"long-run"))
+                               r.AddRun(longRun)
+
+                               mediumRun := newRun(t, filepath.Join(tmp, 
"medium-run"))
+                               r.AddRun(mediumRun)
+
+                               shortRun := newRun(t, filepath.Join(tmp, 
"short-run"))
+                               r.AddRun(shortRun)
+
+                               r.EndRun(shortRun.Path)
+                               r.EndRun(mediumRun.Path)
+                               r.EndRun(longRun.Path)
+                       },
+                       expected: `
+❯❯ Run Summary
+   Duration:   x
+      long-run:    x
+      medium-run:  x
+      short-run:   x
+   Units:      3
+   Succeeded:  3
+`,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       t.Parallel()
+
+                       r := report.NewReport().
+                               WithDisableColor().
+                               WithShowUnitTiming().
+                               WithWorkingDir(tmp)
+
+                       tt.setup(r)
+
+                       var buf bytes.Buffer
+                       err := r.WriteSummary(&buf)
+                       require.NoError(t, err)
+
+                       // 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 actual durations with x
+                       re = regexp.MustCompile(`([ ]{6})([^\s]+:)(\s+)(.*)`)
+                       output = re.ReplaceAllString(output, "${1}${2}${3}x")
+
+                       expected := strings.TrimSpace(tt.expected)
+                       assert.Equal(t, expected, strings.TrimSpace(output))
+               })
+       }
+}
+
 // newRun creates a new run, and asserts that it doesn't error.
 func newRun(t *testing.T, name string) *report.Run {
        t.Helper()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/terragrunt-0.81.3/options/options.go 
new/terragrunt-0.81.5/options/options.go
--- old/terragrunt-0.81.3/options/options.go    2025-06-10 16:58:01.000000000 
+0200
+++ new/terragrunt-0.81.5/options/options.go    2025-06-11 22:15:28.000000000 
+0200
@@ -308,6 +308,8 @@
        ForceBackendMigrate bool
        // SummaryDisable disables the summary output at the end of a run.
        SummaryDisable bool
+       // SummaryUnitDuration enables showing duration information for each 
unit in the summary.
+       SummaryUnitDuration bool
 }
 
 // TerragruntOptionsFunc is a functional option type used to pass options in 
certain integration tests
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/terragrunt-0.81.3/test/fixtures/private-registry/env.tfrc 
new/terragrunt-0.81.5/test/fixtures/private-registry/env.tfrc
--- old/terragrunt-0.81.3/test/fixtures/private-registry/env.tfrc       
1970-01-01 01:00:00.000000000 +0100
+++ new/terragrunt-0.81.5/test/fixtures/private-registry/env.tfrc       
2025-06-11 22:15:28.000000000 +0200
@@ -0,0 +1,3 @@
+credentials "__registry_host__" {
+    token = "__registry_token__"
+}
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/terragrunt-0.81.3/test/fixtures/private-registry/terragrunt.hcl 
new/terragrunt-0.81.5/test/fixtures/private-registry/terragrunt.hcl
--- old/terragrunt-0.81.3/test/fixtures/private-registry/terragrunt.hcl 
1970-01-01 01:00:00.000000000 +0100
+++ new/terragrunt-0.81.5/test/fixtures/private-registry/terragrunt.hcl 
2025-06-11 22:15:28.000000000 +0200
@@ -0,0 +1,4 @@
+# Retrieve a module from the public terraform registry to use with terragrunt
+terraform {
+  source = "tfr://__registry_url__"
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/terragrunt-0.81.3/test/integration_private_registry_test.go 
new/terragrunt-0.81.5/test/integration_private_registry_test.go
--- old/terragrunt-0.81.3/test/integration_private_registry_test.go     
1970-01-01 01:00:00.000000000 +0100
+++ new/terragrunt-0.81.5/test/integration_private_registry_test.go     
2025-06-11 22:15:28.000000000 +0200
@@ -0,0 +1,83 @@
+//go:build private_registry
+
+package test_test
+
+import (
+       "fmt"
+       "net/url"
+       "os"
+       "strings"
+       "testing"
+
+       "github.com/gruntwork-io/terragrunt/test/helpers"
+       "github.com/gruntwork-io/terragrunt/util"
+       "github.com/stretchr/testify/require"
+)
+
+const (
+       privateRegistryFixturePath = "fixtures/private-registry"
+)
+
+func setupPrivateRegistryTest(t *testing.T) (string, string, string) {
+       t.Helper()
+
+       registryToken := os.Getenv("PRIVATE_REGISTRY_TOKEN")
+
+       // the private registry test is recommended to be a clone of 
gruntwork-io/terraform-null-terragrunt-registry-test
+       registryUrl := os.Getenv("PRIVATE_REGISTRY_URL")
+
+       if registryToken == "" || registryUrl == "" {
+               t.Skip("Skipping test because it requires a valid Terraform 
registry token and url")
+       }
+
+       helpers.CleanupTerraformFolder(t, privateRegistryFixturePath)
+       tmpEnvPath := helpers.CopyEnvironment(t, privateRegistryFixturePath)
+       rootPath := util.JoinPath(tmpEnvPath, privateRegistryFixturePath)
+
+       URL, err := url.Parse("tfr://" + registryUrl)
+       if err != nil {
+               t.Fatalf("REGISTRY_URL is invalid: %v", err)
+       }
+
+       if URL.Hostname() == "" {
+               t.Fatal("REGISTRY_URL is invalid")
+       }
+
+       helpers.CopyAndFillMapPlaceholders(t, 
util.JoinPath(privateRegistryFixturePath, "terragrunt.hcl"), 
util.JoinPath(rootPath, "terragrunt.hcl"), map[string]string{
+               "__registry_url__": registryUrl,
+       })
+
+       return rootPath, URL.Hostname(), registryToken
+}
+
+func TestPrivateRegistryWithConfgFileToken(t *testing.T) {
+       rootPath, host, token := setupPrivateRegistryTest(t)
+
+       helpers.CopyAndFillMapPlaceholders(t, 
util.JoinPath(privateRegistryFixturePath, "env.tfrc"), util.JoinPath(rootPath, 
"env.tfrc"), map[string]string{
+               "__registry_token__": token,
+               "__registry_host__":  host,
+       })
+
+       t.Setenv("TF_CLI_CONFIG_FILE", util.JoinPath(rootPath, "env.tfrc"))
+
+       _, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt init 
--non-interactive --log-level=trace --working-dir="+rootPath)
+
+       // the hashicorp/null provider errors on install, but that indicates 
that the private tfr module was downloaded
+       require.Contains(t, err.Error(), "hashicorp/null", "Error accessing the 
private registry")
+}
+
+func TestPrivateRegistryWithEnvToken(t *testing.T) {
+       rootPath, host, token := setupPrivateRegistryTest(t)
+
+       // Convert host to format suitable for Terraform env vars.
+       // This is based on the tf/cliconfig/credentials.go 
collectCredentialsFromEnv
+       host = strings.ReplaceAll(strings.ReplaceAll(host, ".", "_"), "-", "__")
+
+       t.Setenv(fmt.Sprintf("TF_TOKEN_%s", host), token)
+
+       _, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt init 
--non-interactive --log-level=trace --working-dir="+rootPath)
+
+       // The main test is for authentication against the private registry, so 
if the null provider fails then we know
+       // that terragrunt authenticated and downloaded the module.
+       require.Contains(t, err.Error(), "hashicorp/null", "Error accessing the 
private registry")
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/terragrunt-0.81.3/test/integration_report_test.go 
new/terragrunt-0.81.5/test/integration_report_test.go
--- old/terragrunt-0.81.3/test/integration_report_test.go       2025-06-10 
16:58:01.000000000 +0200
+++ new/terragrunt-0.81.5/test/integration_report_test.go       2025-06-11 
22:15:28.000000000 +0200
@@ -370,3 +370,113 @@
                })
        }
 }
+
+func TestTerragruntReportExperimentWithUnitTiming(t *testing.T) {
+       t.Parallel()
+
+       // Set up test environment
+       helpers.CleanupTerraformFolder(t, testFixtureReportPath)
+       tmpEnvPath := helpers.CopyEnvironment(t, testFixtureReportPath)
+       rootPath := util.JoinPath(tmpEnvPath, testFixtureReportPath)
+
+       // Run terragrunt with report experiment enabled and unit timing enabled
+       var stdout bytes.Buffer
+       var stderr bytes.Buffer
+       err := helpers.RunTerragruntCommand(t, "terragrunt run --all apply 
--experiment report --non-interactive --working-dir "+rootPath+" 
--summary-unit-duration", &stdout, &stderr)
+       require.NoError(t, err)
+
+       // Verify the report output contains expected information
+       stdoutStr := stdout.String()
+
+       // Replace the duration lines with fixed durations
+       re := regexp.MustCompile(`Duration:(\s+)(.*)`)
+       stdoutStr = re.ReplaceAllString(stdoutStr, "Duration:${1}x")
+
+       // Replace unit timing durations with x
+       re = regexp.MustCompile(`([ ]{6})([^\s]+:)(\s+)(.*)`)
+       stdoutStr = re.ReplaceAllString(stdoutStr, "${1}${2}${3}x")
+
+       // Find and extract the run summary section
+       lines := strings.Split(stdoutStr, "\n")
+
+       // Find the "Run Summary" line
+       summaryStartIdx := -1
+       for i, line := range lines {
+               if strings.Contains(line, "Run Summary") {
+                       summaryStartIdx = i
+                       break
+               }
+       }
+       require.NotEqual(t, -1, summaryStartIdx, "Could not find 'Run Summary' 
line")
+
+       // Find the end of the summary (last non-empty line after summary start)
+       summaryEndIdx := len(lines) - 1
+       for i := summaryEndIdx; i > summaryStartIdx; i-- {
+               if strings.TrimSpace(lines[i]) != "" {
+                       summaryEndIdx = i
+                       break
+               }
+       }
+
+       // Extract unit duration lines (lines that start with 6 spaces and 
contain a colon)
+       var unitLogLines []string
+       var otherLines []string
+
+       for i := summaryStartIdx; i <= summaryEndIdx; i++ {
+               line := lines[i]
+               // Check if this is a unit duration line (6 spaces + unit name 
+ colon)
+               if strings.HasPrefix(line, "      ") && strings.Contains(line, 
":") && !strings.Contains(line, "Duration:") {
+                       unitLogLines = append(unitLogLines, line)
+               } else {
+                       otherLines = append(otherLines, line)
+               }
+       }
+
+       // Sort the duration lines alphabetically so that they show up 
consistently
+       sort.Slice(unitLogLines, func(i, j int) bool {
+               // Extract the unit name from the line
+               unitNameI := 
strings.TrimSpace(re.ReplaceAllString(unitLogLines[i], "${2}"))
+               unitNameJ := 
strings.TrimSpace(re.ReplaceAllString(unitLogLines[j], "${2}"))
+
+               // Compare the unit names
+               return unitNameI < unitNameJ
+       })
+
+       // Reconstruct the summary with sorted unit lines
+       // Insert sorted unit lines after the "Duration:" line
+       finalLines := make([]string, 0, len(otherLines)+len(unitLogLines))
+
+       unitInserted := false
+       for _, line := range otherLines {
+               finalLines = append(finalLines, line)
+               if strings.Contains(line, "Duration:") && !unitInserted {
+                       finalLines = append(finalLines, unitLogLines...)
+                       unitInserted = true
+               }
+       }
+
+       stdoutStr = strings.Join(finalLines, "\n")
+
+       assert.Equal(t, strings.TrimSpace(`
+❯❯ Run Summary
+   Duration:     x
+      chain-a:            x
+      chain-b:            x
+      chain-c:            x
+      error-ignore:       x
+      first-early-exit:   x
+      first-exclude:      x
+      first-failure:      x
+      first-success:      x
+      retry-success:      x
+      second-early-exit:  x
+      second-exclude:     x
+      second-failure:     x
+      second-success:     x
+   Units:        13
+   Succeeded:    4
+   Failed:       3
+   Early Exits:  4
+   Excluded:     2
+`), strings.TrimSpace(stdoutStr))
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/terragrunt-0.81.3/tf/getter.go 
new/terragrunt-0.81.5/tf/getter.go
--- old/terragrunt-0.81.3/tf/getter.go  2025-06-10 16:58:01.000000000 +0200
+++ new/terragrunt-0.81.5/tf/getter.go  2025-06-11 22:15:28.000000000 +0200
@@ -17,8 +17,10 @@
        "github.com/hashicorp/go-cleanhttp"
        "github.com/hashicorp/go-getter"
        safetemp "github.com/hashicorp/go-safetemp"
+       svchost "github.com/hashicorp/terraform-svchost"
 
        "github.com/gruntwork-io/terragrunt/internal/errors"
+       "github.com/gruntwork-io/terragrunt/tf/cliconfig"
        "github.com/gruntwork-io/terragrunt/util"
 )
 
@@ -326,6 +328,25 @@
        return terraformGet, nil
 }
 
+func applyHostToken(req *http.Request) (*http.Request, error) {
+       cliCfg, err := cliconfig.LoadUserConfig()
+       if err != nil {
+               return nil, err
+       }
+
+       if creds := 
cliCfg.CredentialsSource().ForHost(svchost.Hostname(req.URL.Hostname())); creds 
!= nil {
+               creds.PrepareRequest(req)
+       } else {
+               // fall back to the TG_TF_REGISTRY_TOKEN
+               authToken := os.Getenv(authTokenEnvName)
+               if authToken != "" {
+                       req.Header.Add("Authorization", "Bearer "+authToken)
+               }
+       }
+
+       return req, nil
+}
+
 // httpGETAndGetResponse is a helper function to make a GET request to the 
given URL using the http client. This
 // function will then read the response and return the contents + the response 
header.
 func httpGETAndGetResponse(ctx context.Context, logger log.Logger, getURL 
url.URL) ([]byte, *http.Header, error) {
@@ -336,9 +357,9 @@
 
        // Handle authentication via env var. Authentication is done by 
providing the registry token as a bearer token in
        // the request header.
-       authToken := os.Getenv(authTokenEnvName)
-       if authToken != "" {
-               req.Header.Add("Authorization", "Bearer "+authToken)
+       req, err = applyHostToken(req)
+       if err != nil {
+               return nil, nil, errors.New(err)
        }
 
        resp, err := httpClient.Do(req)

++++++ terragrunt.obsinfo ++++++
--- /var/tmp/diff_new_pack.3I0hr9/_old  2025-06-12 15:55:40.383185340 +0200
+++ /var/tmp/diff_new_pack.3I0hr9/_new  2025-06-12 15:55:40.387185505 +0200
@@ -1,5 +1,5 @@
 name: terragrunt
-version: 0.81.3
-mtime: 1749567481
-commit: fe11a2ea3834f3cc3cf944569cb8a1ceb4f76df1
+version: 0.81.5
+mtime: 1749672928
+commit: 5b4250e925d54b2e82de32ba0119f9ea1883512a
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/terragrunt/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.terragrunt.new.19631/vendor.tar.gz differ: char 13, 
line 1

Reply via email to