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