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-09-23 16:07:41 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/terragrunt (Old) and /work/SRC/openSUSE:Factory/.terragrunt.new.27445 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "terragrunt" Tue Sep 23 16:07:41 2025 rev:253 rq:1306630 version:0.87.5 Changes: -------- --- /work/SRC/openSUSE:Factory/terragrunt/terragrunt.changes 2025-09-22 16:40:43.784058366 +0200 +++ /work/SRC/openSUSE:Factory/.terragrunt.new.27445/terragrunt.changes 2025-09-23 16:08:12.905924244 +0200 @@ -1,0 +2,18 @@ +Tue Sep 23 05:00:50 UTC 2025 - Johannes Kastl <[email protected]> + +- Update to version 0.87.5: + * runner-pool experiment + - Runner Pool Performance: Optimized execution flow for faster + and more efficient unit scheduling + - Benchmarks: Introduced initial benchmark tests to measure + runner-pool performance under different scenarios + * What's Changed + - chore: runner-pool benchmarks (#4855) + - chore: Offboarding AJ (#4865) + - Cleanup (#4859) + - Adding social images (#4856) + - Update subhead (#4854) + - Adding meta tags (#4849) + - docs: updated runner-pool experiment status (#4852) + +------------------------------------------------------------------- Old: ---- terragrunt-0.87.4.obscpio New: ---- terragrunt-0.87.5.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ terragrunt.spec ++++++ --- /var/tmp/diff_new_pack.KD9QeP/_old 2025-09-23 16:08:13.809962720 +0200 +++ /var/tmp/diff_new_pack.KD9QeP/_new 2025-09-23 16:08:13.813962891 +0200 @@ -17,7 +17,7 @@ Name: terragrunt -Version: 0.87.4 +Version: 0.87.5 Release: 0 Summary: Thin wrapper for Terraform for working with multiple Terraform modules License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.KD9QeP/_old 2025-09-23 16:08:13.869965274 +0200 +++ /var/tmp/diff_new_pack.KD9QeP/_new 2025-09-23 16:08:13.873965445 +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.87.4</param> + <param name="revision">v0.87.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.KD9QeP/_old 2025-09-23 16:08:13.893966296 +0200 +++ /var/tmp/diff_new_pack.KD9QeP/_new 2025-09-23 16:08:13.897966466 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/gruntwork-io/terragrunt</param> - <param name="changesrevision">35f6895bf502d599697d6cc8ef3f3551c65e3b66</param></service></servicedata> + <param name="changesrevision">9a6c90736e85eed732e87320cd34eae0fd9a16f9</param></service></servicedata> (No newline at EOF) ++++++ terragrunt-0.87.4.obscpio -> terragrunt-0.87.5.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terragrunt-0.87.4/CODEOWNERS new/terragrunt-0.87.5/CODEOWNERS --- old/terragrunt-0.87.4/CODEOWNERS 2025-09-16 21:54:29.000000000 +0200 +++ new/terragrunt-0.87.5/CODEOWNERS 2025-09-22 20:52:57.000000000 +0200 @@ -1,2 +1,2 @@ -* @denis256 @wakeful @yhakbar +* @denis256 @yhakbar diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terragrunt-0.87.4/config/config_helpers.go new/terragrunt-0.87.5/config/config_helpers.go --- old/terragrunt-0.87.4/config/config_helpers.go 2025-09-16 21:54:29.000000000 +0200 +++ new/terragrunt-0.87.5/config/config_helpers.go 2025-09-22 20:52:57.000000000 +0200 @@ -1238,7 +1238,7 @@ // using reflection extract GroupResults from getDataKeyError // may not be compatible with future versions errValue := reflect.ValueOf(err) - if errValue.Kind() == reflect.Ptr { + if errValue.Kind() == reflect.Pointer { errValue = errValue.Elem() } Binary files old/terragrunt-0.87.4/docs-starlight/public/images/terragrunt-og-image-1200x630.jpg and new/terragrunt-0.87.5/docs-starlight/public/images/terragrunt-og-image-1200x630.jpg differ Binary files old/terragrunt-0.87.4/docs-starlight/public/images/terragrunt-og-image.png and new/terragrunt-0.87.5/docs-starlight/public/images/terragrunt-og-image.png differ Binary files old/terragrunt-0.87.4/docs-starlight/public/images/terragrunt-twitter-image.jpg and new/terragrunt-0.87.5/docs-starlight/public/images/terragrunt-twitter-image.jpg differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terragrunt-0.87.4/docs-starlight/src/components/TSHero.astro new/terragrunt-0.87.5/docs-starlight/src/components/TSHero.astro --- old/terragrunt-0.87.4/docs-starlight/src/components/TSHero.astro 2025-09-16 21:54:29.000000000 +0200 +++ new/terragrunt-0.87.5/docs-starlight/src/components/TSHero.astro 2025-09-22 20:52:57.000000000 +0200 @@ -2,20 +2,17 @@ import '@styles/global.css'; import { Image } from 'astro:assets'; +import BackgroundPattern from '@assets/boxes-bg.svg'; import ColFeature from '@components/ColFeature.astro'; +import GithubIcon from '@assets/icons/github-icon.svg'; +import GitlabIcon from '@assets/icons/gitlab-icon.svg'; import Grunty2 from '@assets/grunty/grunty-2.svg'; -import BackgroundPattern from '@assets/boxes-bg.svg'; import IconDriftDetection from '@assets/icons/drift-detection.svg'; import IconLabel from '@components/dv-IconButton.astro'; import IconPatcher from '@assets/icons/terragrunt-patcher.svg'; import IconPipelines from '@assets/icons/terragrunt-pipelines.svg'; -import MobileHeroBackground from '@assets/hero-bkgnd.svg'; -import Terminal from '@components/dv-Terminal.astro'; -import ThreeColFeatures from './ThreeColFeatures.astro'; - -import GithubIcon from '@assets/icons/github-icon.svg'; -import GitlabIcon from '@assets/icons/gitlab-icon.svg'; import TerragruntIcon from '@assets/icons/terragrunt-icon.svg'; +import ThreeColFeatures from '@components/ThreeColFeatures.astro'; --- <section class="relative md:pt-22 bg-[var(--color-bg-dark)] gap-10 overflow-hidden"> @@ -33,12 +30,12 @@ <div class="max-w-[989px] flex flex-col gap-6"> <div class="flex flex-col justify-start gap-5 w-full"> - <h1 class="text-4xl md:text-6xl text-white leading-12 m-0"> - Terragrunt Scale - </h1> - <p class="max-w-[650px] text-lg text-[var(--color-gray-1)]"> - Get best-in-class tooling by the creators of Terragrunt to unlock its full potential, helping you efficiently deploy, and maintain your infrastructure without the grunt work. - </p> + <h1 class="text-4xl md:text-6xl text-white leading-12 m-0"> + Terragrunt Scale + </h1> + <p class="max-w-[650px] text-lg text-[var(--color-gray-1)]"> + Standardize infra operations without leaving your pipeline: plan on PR, apply on merge, dependency-aware across workspaces, with automated drift detection and version-bump PRs to keep resources current. + </p> </div> <div class="flex flex-col sm:flex-row sm:items-center w-full gap-4"> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terragrunt-0.87.4/docs-starlight/src/content/docs/04-reference/04-experiments.md new/terragrunt-0.87.5/docs-starlight/src/content/docs/04-reference/04-experiments.md --- old/terragrunt-0.87.4/docs-starlight/src/content/docs/04-reference/04-experiments.md 2025-09-16 21:54:29.000000000 +0200 +++ new/terragrunt-0.87.5/docs-starlight/src/content/docs/04-reference/04-experiments.md 2025-09-22 20:52:57.000000000 +0200 @@ -161,13 +161,13 @@ To transition the `runner-pool` feature to a stable release, the following must be addressed: - [x] Use new discovery and queue packages to discover units. -- [ ] Add support for including/excluding external units in the discovery process. -- [ ] Add runner pool implementation to execute discovered units. -- [ ] Add integration tests to track that the runner pool works in the same way as the current implementation. +- [x] Add support for including/excluding external units in the discovery process. +- [x] Add runner pool implementation to execute discovered units. +- [x] Add integration tests to track that the runner pool works in the same way as the current implementation. - [ ] Add performance tests to track that the runner pool implementation is faster than the current implementation. -- [ ] Add support for fail fast behavior in the runner pool. -- [ ] Improve the UI to queue to apply. -- [ ] Add OpenTelemetry support to the runner pool. +- [x] Add support for fail fast behavior in the runner pool. +- [x] Improve the UI to queue to apply. +- [x] Add OpenTelemetry support to the runner pool. ### `auto-provider-cache-dir` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terragrunt-0.87.4/docs-starlight/src/layouts/BaseLayout.astro new/terragrunt-0.87.5/docs-starlight/src/layouts/BaseLayout.astro --- old/terragrunt-0.87.4/docs-starlight/src/layouts/BaseLayout.astro 2025-09-16 21:54:29.000000000 +0200 +++ new/terragrunt-0.87.5/docs-starlight/src/layouts/BaseLayout.astro 2025-09-22 20:52:57.000000000 +0200 @@ -1,21 +1,49 @@ --- interface Props { - title: string; + title?: string; + description?: string; + url?: string; } -const { title } = Astro.props; +const { + title = 'Terragrunt | Orchestrate Terraform & OpenTofu at Scale', + description = 'Standardize IaC and manage growing infra complexity: define units & stacks, cut repetition with includes/hooks, execute modules in dependency order across environments.', + url = 'https://terragrunt.gruntwork.io/', +} = Astro.props; --- <!DOCTYPE html> <html lang="en"> - <head> - <meta charset="UTF-8" /> - <meta name="description" content="Terragrunt Homepage" /> - <meta name="generator" content={Astro.generator} /> - <meta name="viewport" content="width=device-width" /> - <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> - <title>{title}</title> - </head> - <body class="bg-white text-gray-900 antialiased"> - <slot /> - </body> + <head> + <!-- Primary Meta Tags --> + <meta charset="UTF-8" /> + <title>{title}</title> + <meta name="description" content={description} /> + <meta name="generator" content={Astro.generator} /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + + <!-- Open Graph / Facebook --> + <meta property="og:description" content={description} /> + <meta property="og:image" content="https://terragrunt.gruntwork.io/images/terragrunt-og-image-1200x630.jpg" /> + <meta property="og:image:secure_url" content="https://terragrunt.gruntwork.io/images/terragrunt-og-image-1200x630.jpg" /> + <meta property="og:image:type" content="image/jpeg" /> + <meta property="og:image:width" content="1200" /> + <meta property="og:image:height" content="630" /> + <meta property="og:image:alt" content="An image featuring the Gruntwork Mascot and the words Orchestrate Terraform & OpenTofu at Scale" /> + <meta property="og:title" content={title} /> + <meta property="og:type" content="website" /> + <meta property="og:url" content={url} /> + + <!-- X (Twitter) --> + <meta name="twitter:card" content="summary_large_image" /> + <meta name="twitter:description" content={description} /> + <meta name="twitter:image" content="https://terragrunt.gruntwork.io/images/terragrunt-twitter-image.jpg" /> + <meta name="twitter:site" content="@gruntwork_io" /> + <meta name="twitter:title" content={title} /> + + <link rel="canonical" href={url} /> + <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> + </head> + <body class="tg bg-[#FAFAFA]"> + <slot /> + </body> </html> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terragrunt-0.87.4/docs-starlight/src/pages/index.astro new/terragrunt-0.87.5/docs-starlight/src/pages/index.astro --- old/terragrunt-0.87.4/docs-starlight/src/pages/index.astro 2025-09-16 21:54:29.000000000 +0200 +++ new/terragrunt-0.87.5/docs-starlight/src/pages/index.astro 2025-09-22 20:52:57.000000000 +0200 @@ -1,4 +1,5 @@ --- +import BaseLayout from '@layouts/BaseLayout.astro'; import ConsistencySection from '@components/dv-ConsistencySection.astro'; import DrySection from '@components/dv-DrySection.astro'; import FeaturedBrands from '@components/dv-FeaturedBrands.astro'; @@ -12,39 +13,24 @@ import '@styles/global.css'; --- -<!DOCTYPE html> -<html lang="en"> - <head> - <title>Terragrunt</title> - <meta charset="UTF-8" /> - <meta name="description" content="Astro description" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <meta name="generator" content={Astro.generator} /> - <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> - </head> - - <body class="tg bg-[#FAFAFA]"> - - <Header showThemeToggle={false} /> - <Hero /> - - <PageContainer> - <FeaturedBrands /> - - <div class="flex flex-col gap-12 md:gap-38 z-10"> - <OrchestrateSection /> - <ConsistencySection /> - <DrySection /> - <Testimonials /> - </div> - - <div class="flex flex-col gap-44 z-10"> - <PetAdvertise /> - <Footer /> - </div> - </PageContainer> - - - </body> -</html> +<BaseLayout> + <Header showThemeToggle={false} /> + <Hero /> + + <PageContainer> + <FeaturedBrands /> + + <div class="flex flex-col gap-12 md:gap-38 z-10"> + <OrchestrateSection /> + <ConsistencySection /> + <DrySection /> + <Testimonials /> + </div> + + <div class="flex flex-col gap-44 z-10"> + <PetAdvertise /> + <Footer /> + </div> + </PageContainer> +</BaseLayout> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terragrunt-0.87.4/docs-starlight/src/pages/terragrunt-scale.astro new/terragrunt-0.87.5/docs-starlight/src/pages/terragrunt-scale.astro --- old/terragrunt-0.87.4/docs-starlight/src/pages/terragrunt-scale.astro 2025-09-16 21:54:29.000000000 +0200 +++ new/terragrunt-0.87.5/docs-starlight/src/pages/terragrunt-scale.astro 2025-09-22 20:52:57.000000000 +0200 @@ -1,9 +1,9 @@ --- +import BaseLayout from '@layouts/BaseLayout.astro'; import FeaturedBrands from '@components/dv-FeaturedBrands.astro'; import Footer from '@components/dv-Footer.astro'; import Header from '@components/Header.astro'; import PageContainer from '@components/PageContainer.astro'; -import PetAdvertise from '@components/dv-PetAdvertise.astro'; import SectionSpacer from '@components/SectionSpacer.astro'; import Testimonial from '@components/Testimonial.astro'; import TSAccelerator from '@components/TSAccelerator.astro'; @@ -16,42 +16,31 @@ import '@styles/global.css'; --- -<!DOCTYPE html> -<html lang="en"> - - <head> - <title>Terragrunt Scale</title> - <meta charset="UTF-8" /> - <meta name="description" content="Astro description" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <meta name="generator" content={Astro.generator} /> - <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> - </head> - - <body class="tg bg-[#FAFAFA]"> - - <Header showThemeToggle={false} /> - <TSHero /> - <PageContainer> - <FeaturedBrands /> - <SectionSpacer /> - <Vimeo url="https://player.vimeo.com/video/1118246701" /> - <SectionSpacer /> - <TSTerragruntPipelines /> - <SectionSpacer /> - <TSTerragruntDrift /> - <SectionSpacer /> - <TSTerragruntPatcher /> - <SectionSpacer /> - <TSPricing /> - <TSAccelerator /> - <SectionSpacer /> - <Testimonial /> - <SectionSpacer /> - <div class="flex flex-col gap-44 z-10"> - <Footer /> - </div> - </PageContainer> - - </body> -</html> +<BaseLayout + title="Terragrunt Scale | Secure GitOps Pipelines & Drift Detection" + description="Secure GitOps for Terragrunt: GitHub/GitLab pipelines, drift detection with auto-PRs, and patching to keep IaC current; built by the creators of Terragrunt." + url="https://terragrunt.gruntwork.io/terragrunt-scale/" +> + <Header showThemeToggle={false} /> + <TSHero /> + <PageContainer> + <FeaturedBrands /> + <SectionSpacer /> + <Vimeo url="https://player.vimeo.com/video/1118246701" /> + <SectionSpacer /> + <TSTerragruntPipelines /> + <SectionSpacer /> + <TSTerragruntDrift /> + <SectionSpacer /> + <TSTerragruntPatcher /> + <SectionSpacer /> + <TSPricing /> + <TSAccelerator /> + <SectionSpacer /> + <Testimonial /> + <SectionSpacer /> + <div class="flex flex-col gap-44 z-10"> + <Footer /> + </div> + </PageContainer> +</BaseLayout> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terragrunt-0.87.4/docs-starlight/tsconfig.json new/terragrunt-0.87.5/docs-starlight/tsconfig.json --- old/terragrunt-0.87.4/docs-starlight/tsconfig.json 2025-09-16 21:54:29.000000000 +0200 +++ new/terragrunt-0.87.5/docs-starlight/tsconfig.json 2025-09-22 20:52:57.000000000 +0200 @@ -16,6 +16,9 @@ "@components/*": [ "src/components/*" ], + "@layouts/*": [ + "src/layouts/*" + ], "@lib/*": [ "src/lib/*" ], diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terragrunt-0.87.4/internal/cloner/cloner.go new/terragrunt-0.87.5/internal/cloner/cloner.go --- old/terragrunt-0.87.4/internal/cloner/cloner.go 2025-09-16 21:54:29.000000000 +0200 +++ new/terragrunt-0.87.5/internal/cloner/cloner.go 2025-09-22 20:52:57.000000000 +0200 @@ -114,7 +114,7 @@ return cloner.cloneArray(src) case reflect.Map: return cloner.cloneMap(src) - case reflect.Ptr, reflect.UnsafePointer: + case reflect.Pointer, reflect.UnsafePointer: return cloner.clonePointer(src) case reflect.Struct: return cloner.cloneStruct(src) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terragrunt-0.87.4/internal/discovery/discovery.go new/terragrunt-0.87.5/internal/discovery/discovery.go --- old/terragrunt-0.87.4/internal/discovery/discovery.go 2025-09-16 21:54:29.000000000 +0200 +++ new/terragrunt-0.87.5/internal/discovery/discovery.go 2025-09-22 20:52:57.000000000 +0200 @@ -7,13 +7,14 @@ "io/fs" "os" "path/filepath" + "runtime" "sort" "strings" "github.com/gruntwork-io/terragrunt/config/hclparse" + "github.com/gruntwork-io/terragrunt/internal/runner/common" "github.com/gruntwork-io/terragrunt/internal/experiment" - "github.com/gruntwork-io/terragrunt/internal/runner/common" "github.com/gruntwork-io/terragrunt/util" "github.com/gruntwork-io/terragrunt/telemetry" @@ -25,6 +26,7 @@ "github.com/hashicorp/hcl/v2" "github.com/mattn/go-zglob" "github.com/zclconf/go-cty/cty" + "golang.org/x/sync/errgroup" ) const ( @@ -35,8 +37,33 @@ // skipOutputDiagnostics is a string used to identify diagnostics that reference outputs. skipOutputDiagnostics = "output" + + // Default number of concurrent workers for discovery operations + defaultDiscoveryWorkers = 4 + + // Maximum number of workers (2x default to prevent excessive concurrency) + maxDiscoveryWorkers = defaultDiscoveryWorkers * 2 + + // Channel buffer multiplier for worker pools (larger buffers reduce blocking) + channelBufferMultiplier = 4 + + // Maximum hidden directory memoization entries (prevents unbounded memory growth) + maxHiddenDirMemoSize = 1000 + + // Default maximum dependency depth for discovery + defaultMaxDependencyDepth = 1000 + + // Maximum number of cycle removal attempts (prevents infinite loops) + maxCycleRemovalAttempts = 100 ) +// defaultExcludeDirs is the default directories where units should never be discovered. +var defaultExcludeDirs = []string{ + ".git/**", + ".terraform/**", + ".terragrunt-cache/**", +} + // ConfigType is the type of Terragrunt configuration. type ConfigType string @@ -114,6 +141,9 @@ // maxDependencyDepth is the maximum depth of the dependency tree to discover. maxDependencyDepth int + // numWorkers determines the number of concurrent workers for discovery operations. + numWorkers int + // hidden determines whether to detect configurations in hidden directories. hidden bool @@ -134,6 +164,9 @@ // suppressParseErrors determines whether to suppress errors when parsing Terragrunt configurations. suppressParseErrors bool + + // useDefaultExcludes determines whether to use default exclude patterns. + useDefaultExcludes bool } // DiscoveryOption is a function that modifies a Discovery. @@ -150,6 +183,8 @@ // NewDiscovery creates a new Discovery. func NewDiscovery(dir string, opts ...DiscoveryOption) *Discovery { + numWorkers := max(min(runtime.NumCPU(), maxDiscoveryWorkers), defaultDiscoveryWorkers) + discovery := &Discovery{ workingDir: dir, hidden: false, @@ -157,6 +192,8 @@ config.StackDir, filepath.Join(config.StackDir, "**"), }, + numWorkers: numWorkers, + useDefaultExcludes: true, } for _, opt := range opts { @@ -180,20 +217,6 @@ return d } -// WithOptions applies any provided options that expose parser options. -// Accepts common.Option values; only those implementing common.ParseOptionsProvider are used. -func (d *Discovery) WithOptions(opts ...common.Option) *Discovery { //nolint: revive - for _, opt := range opts { - if provider, ok := any(opt).(common.ParseOptionsProvider); ok { - if parseOpts := provider.GetParseOptions(); len(parseOpts) > 0 { - d = d.WithParserOptions(parseOpts) - } - } - } - - return d -} - // WithDiscoverDependencies sets the DiscoverDependencies flag to true. func (d *Discovery) WithDiscoverDependencies() *Discovery { d.discoverDependencies = true @@ -201,7 +224,7 @@ d.requiresParse = true if d.maxDependencyDepth == 0 { - d.maxDependencyDepth = 1000 + d.maxDependencyDepth = defaultMaxDependencyDepth } return d @@ -277,6 +300,33 @@ return d } +// SetParseOptions implements common.ParseOptionsSetter allowing discovery to receive +// HCL parser options via generic option plumbing. +func (d *Discovery) SetParseOptions(options []hclparse.Option) { + d.parserOptions = options +} + +// WithOptions ingests runner options and applies any discovery-relevant settings. +// Currently, it extracts HCL parser options provided via common.ParseOptionsProvider +// and forwards them to discovery's parser configuration. +func (d *Discovery) WithOptions(opts ...common.Option) *Discovery { + var parserOptions []hclparse.Option + + for _, opt := range opts { + if p, ok := opt.(common.ParseOptionsProvider); ok { + if po := p.GetParseOptions(); len(po) > 0 { + parserOptions = append(parserOptions, po...) + } + } + } + + if len(parserOptions) > 0 { + d = d.WithParserOptions(parserOptions) + } + + return d +} + // WithStrictInclude enables strict include mode. func (d *Discovery) WithStrictInclude() *Discovery { d.strictInclude = true @@ -295,6 +345,18 @@ return d } +// WithNumWorkers sets the number of concurrent workers for discovery operations. +func (d *Discovery) WithNumWorkers(numWorkers int) *Discovery { + d.numWorkers = numWorkers + return d +} + +// WithoutDefaultExcludes disables the use of default exclude patterns (e.g. .git, .terraform, .terragrunt-cache). +func (d *Discovery) WithoutDefaultExcludes() *Discovery { + d.useDefaultExcludes = false + return d +} + // compileIncludePatterns compiles the include directory patterns for faster matching. func (d *Discovery) compileIncludePatterns(l log.Logger) { d.compiledIncludePatterns = make([]CompiledPattern, 0, len(d.includeDirs)) @@ -423,20 +485,32 @@ // isInHiddenDirectory returns true if the path is in a hidden directory. func (d *Discovery) isInHiddenDirectory(path string) bool { + // Check memoized hidden directories first for _, hiddenDir := range d.hiddenDirMemo { if strings.HasPrefix(path, hiddenDir) { return true } } - hiddenPath := "" + // Quick check: if path doesn't contain "." after first character, it's not hidden + if !strings.Contains(path[1:], string(os.PathSeparator)+".") { + return false + } + hiddenPath := "" parts := strings.SplitSeq(path, string(os.PathSeparator)) + for part := range parts { - hiddenPath = filepath.Join(hiddenPath, part) + if hiddenPath != "" { + hiddenPath = filepath.Join(hiddenPath, part) + } else { + hiddenPath = part + } - if strings.HasPrefix(part, ".") { - d.hiddenDirMemo = append(d.hiddenDirMemo, hiddenPath) + if strings.HasPrefix(part, ".") && part != "." && part != ".." { + if len(d.hiddenDirMemo) < maxHiddenDirMemoSize { + d.hiddenDirMemo = append(d.hiddenDirMemo, hiddenPath) + } return true } @@ -445,148 +519,363 @@ return false } -// Discover discovers Terragrunt configurations in the WorkingDir. -func (d *Discovery) Discover(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) (DiscoveredConfigs, error) { - var cfgs DiscoveredConfigs +// discoverConcurrently performs concurrent file discovery with worker pools using errgroup. +func (d *Discovery) discoverConcurrently( + ctx context.Context, + l log.Logger, + opts *options.TerragruntOptions, + filenames []string, +) (DiscoveredConfigs, error) { + g, ctx := errgroup.WithContext(ctx) + g.SetLimit(d.numWorkers + 1) // +1 for the file walker + + filePaths := make(chan string, d.numWorkers*channelBufferMultiplier) + results := make(chan *DiscoveredConfig, d.numWorkers*channelBufferMultiplier) + + g.Go(func() error { + defer close(filePaths) + return d.walkDirectoryConcurrently(ctx, l, opts, filePaths) + }) - // Set default config filenames if not set - filenames := d.configFilenames - if len(filenames) == 0 { - filenames = DefaultConfigFilenames + for range d.numWorkers { + g.Go(func() error { + return d.configWorker(ctx, l, filePaths, results, filenames) + }) } - // Prepare include/exclude glob patterns (canonicalized) for matching - var includePatterns, excludePatterns []string + // Close results channel when all workers are done + go func() { + defer close(results) - if len(d.includeDirs) > 0 { - for _, p := range d.includeDirs { - if !filepath.IsAbs(p) { - p = filepath.Join(d.workingDir, p) - } + _ = g.Wait() // We handle errors in the main thread below + }() - includePatterns = append(includePatterns, util.CleanPath(p)) - } - } + cfgs := make(DiscoveredConfigs, 0, len(results)) - if len(d.excludeDirs) > 0 { - for _, p := range d.excludeDirs { - if !filepath.IsAbs(p) { - p = filepath.Join(d.workingDir, p) - } - - excludePatterns = append(excludePatterns, util.CleanPath(p)) - } + for config := range results { + cfgs = append(cfgs, config) } - // Compile patterns if not already compiled - if len(d.compiledIncludePatterns) == 0 && len(includePatterns) > 0 { - d.includeDirs = includePatterns - d.compileIncludePatterns(l) + if err := g.Wait(); err != nil { + return cfgs, err } - if len(d.compiledExcludePatterns) == 0 && len(excludePatterns) > 0 { - d.excludeDirs = excludePatterns - d.compileExcludePatterns(l) + return cfgs, nil +} + +// walkDirectoryConcurrently walks the directory tree and sends file paths to workers. +func (d *Discovery) walkDirectoryConcurrently(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, filePaths chan<- string) error { + walkFn := filepath.WalkDir + if opts.Experiments.Evaluate(experiment.Symlinks) { + walkFn = util.WalkDirWithSymlinks } processFn := func(path string, info fs.DirEntry, err error) error { if err != nil { - return errors.New(err) + return err + } + + select { + case <-ctx.Done(): + return ctx.Err() + default: } if info.IsDir() { - return nil + return d.shouldSkipDirectory(path, l) } - // Apply include/exclude filters by directory path first - dir := filepath.Dir(path) + select { + case filePaths <- path: + case <-ctx.Done(): + return ctx.Err() + } - canonicalDir, canErr := util.CanonicalPath(dir, d.workingDir) - if canErr == nil { - for _, pattern := range d.compiledExcludePatterns { + return nil + } + + return walkFn(d.workingDir, processFn) +} + +// shouldSkipDirectory determines if a directory should be skipped during traversal. +func (d *Discovery) shouldSkipDirectory(path string, l log.Logger) error { + base := filepath.Base(path) + + switch base { + case ".git", ".terraform", ".terragrunt-cache": + return filepath.SkipDir + } + + canonicalDir, canErr := util.CanonicalPath(path, d.workingDir) + if canErr == nil { + for _, pattern := range d.compiledExcludePatterns { + if pattern.Compiled.Match(canonicalDir) { + l.Debugf("Directory %s excluded by glob %s", canonicalDir, pattern.Original) + return filepath.SkipDir + } + } + } + + return nil +} + +// configWorker processes file paths and determines if they are Terragrunt configurations. +func (d *Discovery) configWorker( + ctx context.Context, + l log.Logger, + filePaths <-chan string, + results chan<- *DiscoveredConfig, + filenames []string, +) error { + for path := range filePaths { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + config := d.processFile(path, l, filenames) + + if config != nil { + select { + case results <- config: + case <-ctx.Done(): + return ctx.Err() + } + } + } + + return nil +} + +// processFile processes a single file to determine if it's a Terragrunt configuration. +func (d *Discovery) processFile(path string, l log.Logger, filenames []string) *DiscoveredConfig { + dir := filepath.Dir(path) + + canonicalDir, canErr := util.CanonicalPath(dir, d.workingDir) + if canErr == nil { + for _, pattern := range d.compiledExcludePatterns { + if pattern.Compiled.Match(canonicalDir) { + l.Debugf("Path %s excluded by glob %s", canonicalDir, pattern.Original) + return nil + } + } + + // Enforce include patterns only when strictInclude or excludeByDefault are set + if d.strictInclude || d.excludeByDefault { + included := false + + for _, pattern := range d.compiledIncludePatterns { if pattern.Compiled.Match(canonicalDir) { - l.Debugf("Path %s excluded by glob %s", canonicalDir, pattern.Original) - return nil + included = true + break } } - // Enforce include patterns only when strictInclude or excludeByDefault are set - if d.strictInclude || d.excludeByDefault { - included := false + if !included { + return nil + } + } + } + // Now enforce hidden directory check if still applicable + if !d.hidden && d.isInHiddenDirectory(path) { + // If the directory is hidden, allow it only if it matches an include pattern + allowHidden := false + + if canErr == nil { + // Always allow .terragrunt-stack contents + cleanDir := util.CleanPath(canonicalDir) + if strings.Contains(cleanDir, "/"+config.StackDir+"/") || strings.HasSuffix(cleanDir, "/"+config.StackDir) { + allowHidden = true + } + + if !allowHidden { + // Use precompiled patterns for include matching in hidden directory check for _, pattern := range d.compiledIncludePatterns { if pattern.Compiled.Match(canonicalDir) { - included = true + allowHidden = true break } } - - if !included { - return nil - } } } - // Now enforce hidden directory check if still applicable - if !d.hidden && d.isInHiddenDirectory(path) { - // If the directory is hidden, allow it only if it matches an include pattern - allowHidden := false - - if canErr == nil { - // Always allow .terragrunt-stack contents - cleanDir := util.CleanPath(canonicalDir) - if strings.Contains(cleanDir, "/"+config.StackDir+"/") || strings.HasSuffix(cleanDir, "/"+config.StackDir) { - allowHidden = true - } + if !allowHidden { + return nil + } + } - if !allowHidden { - // Use precompiled patterns for include matching in hidden directory check - for _, pattern := range d.compiledIncludePatterns { - if pattern.Compiled.Match(canonicalDir) { - allowHidden = true - break - } - } - } + base := filepath.Base(path) + for _, fname := range filenames { + if base == fname { + cfgType := ConfigTypeUnit + if fname == config.DefaultStackFile { + cfgType = ConfigTypeStack } - if !allowHidden { - return nil + cfg := &DiscoveredConfig{ + Type: cfgType, + Path: filepath.Dir(path), + } + if d.discoveryContext != nil { + cfg.DiscoveryContext = d.discoveryContext } + + return cfg } + } - base := filepath.Base(path) - for _, fname := range filenames { - if base == fname { - cfgType := ConfigTypeUnit - if fname == config.DefaultStackFile { - cfgType = ConfigTypeStack - } + return nil +} - cfg := &DiscoveredConfig{ - Type: cfgType, - Path: filepath.Dir(path), - } - if d.discoveryContext != nil { - cfg.DiscoveryContext = d.discoveryContext - } +// parseConcurrently parses configurations concurrently to improve performance using errgroup. +func (d *Discovery) parseConcurrently(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, cfgs DiscoveredConfigs) []error { + // Filter out configs that don't need parsing + // Pre-allocate with estimated capacity to reduce reallocation + configsToParse := make([]*DiscoveredConfig, 0, len(cfgs)) + for _, cfg := range cfgs { + // Stack configurations don't need to be parsed for discovery purposes. + // They don't have exclude blocks or dependencies. + if cfg.Type == ConfigTypeStack { + continue + } - cfgs = append(cfgs, cfg) + configsToParse = append(configsToParse, cfg) + } - break + if len(configsToParse) == 0 { + return nil + } + + // Use errgroup for better error handling and synchronization + g, ctx := errgroup.WithContext(ctx) + g.SetLimit(d.numWorkers) + + // Use channels to coordinate parsing work + configChan := make(chan *DiscoveredConfig, d.numWorkers*channelBufferMultiplier) + errorChan := make(chan error, len(configsToParse)) + + // Start config sender + g.Go(func() error { + defer close(configChan) + + for _, cfg := range configsToParse { + select { + case configChan <- cfg: + case <-ctx.Done(): + return ctx.Err() } } return nil + }) + + // Start parser workers + for range d.numWorkers { + g.Go(func() error { + return d.parseWorker(ctx, l, opts, configChan, errorChan) + }) } - walkFn := filepath.WalkDir - if opts.Experiments.Evaluate(experiment.Symlinks) { - walkFn = util.WalkDirWithSymlinks + // Close error channel when all workers are done + go func() { + defer close(errorChan) + + _ = g.Wait() // We handle errors in the main thread below + }() + + // Collect errors + var errs []error + + for err := range errorChan { + if err != nil { + errs = append(errs, errors.New(err)) + } + } + + // Wait for completion and get any errgroup errors + if err := g.Wait(); err != nil { + errs = append(errs, err) + } + + return errs +} + +// parseWorker is a worker that parses configurations concurrently. +func (d *Discovery) parseWorker(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, configChan <-chan *DiscoveredConfig, errorChan chan<- error) error { + for cfg := range configChan { + // Context cancellation check + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + err := cfg.Parse(ctx, l, opts, d.suppressParseErrors, d.parserOptions) + + // Send error or handle context cancellation + select { + case errorChan <- err: + case <-ctx.Done(): + return ctx.Err() + } + } + + return nil +} + +// Discover discovers Terragrunt configurations in the WorkingDir. +func (d *Discovery) Discover(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) (DiscoveredConfigs, error) { + // Set default config filenames if not set + filenames := d.configFilenames + if len(filenames) == 0 { + filenames = DefaultConfigFilenames + } + + // Prepare include/exclude glob patterns (canonicalized) for matching + var includePatterns, excludePatterns []string + + // Add default excludes if enabled + if d.useDefaultExcludes { + excludePatterns = append(excludePatterns, defaultExcludeDirs...) + } + + if len(d.includeDirs) > 0 { + for _, p := range d.includeDirs { + if !filepath.IsAbs(p) { + p = filepath.Join(d.workingDir, p) + } + + includePatterns = append(includePatterns, util.CleanPath(p)) + } } - if err := walkFn(d.workingDir, processFn); err != nil { - return cfgs, errors.New(err) + if len(d.excludeDirs) > 0 { + for _, p := range d.excludeDirs { + if !filepath.IsAbs(p) { + p = filepath.Join(d.workingDir, p) + } + + excludePatterns = append(excludePatterns, util.CleanPath(p)) + } + } + + // Compile patterns if not already compiled + if len(d.compiledIncludePatterns) == 0 && len(includePatterns) > 0 { + d.includeDirs = includePatterns + d.compileIncludePatterns(l) + } + + if len(d.compiledExcludePatterns) == 0 && len(excludePatterns) > 0 { + d.excludeDirs = excludePatterns + d.compileExcludePatterns(l) + } + + // Use concurrent discovery for better performance + cfgs, err := d.discoverConcurrently(ctx, l, opts, filenames) + if err != nil { + return cfgs, err } errs := []error{} @@ -595,20 +884,8 @@ // as we might need to parse configurations for multiple reasons. // e.g. dependencies, exclude, etc. if d.requiresParse { - for _, cfg := range cfgs { - // Stack configurations don't need to be parsed for discovery purposes. - // They don't have exclude blocks or dependencies. - // - // This might change in the future, but for now we'll just skip parsing. - if cfg.Type == ConfigTypeStack { - continue - } - - err := cfg.Parse(ctx, l, opts, d.suppressParseErrors, d.parserOptions) - if err != nil { - errs = append(errs, errors.New(err)) - } - } + parseErrs := d.parseConcurrently(ctx, l, opts, cfgs) + errs = append(errs, parseErrs...) } if d.discoverDependencies { @@ -978,14 +1255,12 @@ // RemoveCycles removes cycles from the dependency graph. func (c DiscoveredConfigs) RemoveCycles() (DiscoveredConfigs, error) { - const maxCycleChecks = 100 - var ( err error cfg *DiscoveredConfig ) - for range maxCycleChecks { + for range maxCycleRemovalAttempts { if cfg, err = c.CycleCheck(); err == nil { break } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terragrunt-0.87.4/internal/report/summary.go new/terragrunt-0.87.5/internal/report/summary.go --- old/terragrunt-0.87.4/internal/report/summary.go 2025-09-16 21:54:29.000000000 +0200 +++ new/terragrunt-0.87.5/internal/report/summary.go 2025-09-22 20:52:57.000000000 +0200 @@ -197,8 +197,6 @@ prefix = " " unitPrefixMultiplier = 2 runSummaryHeader = "❯❯ Run Summary" - durationLabel = "Duration" - unitsLabel = "Units" successLabel = "Succeeded" failureLabel = "Failed" earlyExitLabel = "Early Exits" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terragrunt-0.87.4/internal/runner/common/unit_resolver.go new/terragrunt-0.87.5/internal/runner/common/unit_resolver.go --- old/terragrunt-0.87.4/internal/runner/common/unit_resolver.go 2025-09-16 21:54:29.000000000 +0200 +++ new/terragrunt-0.87.5/internal/runner/common/unit_resolver.go 2025-09-22 20:52:57.000000000 +0200 @@ -344,7 +344,7 @@ return false } if !r.doubleStarEnabled { - excludeFn = func(l log.Logger, unitPath string) bool { + excludeFn = func(_ log.Logger, unitPath string) bool { return collections.ListContainsElement(opts.ExcludeDirs, unitPath) } } @@ -633,7 +633,7 @@ return false } if !r.doubleStarEnabled { - includeFn = func(l log.Logger, unit *Unit) bool { + includeFn = func(_ log.Logger, unit *Unit) bool { if unit.FindUnitInPath(opts.IncludeDirs) { return true } else { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terragrunt-0.87.4/test/benchmarks/helpers/helpers.go new/terragrunt-0.87.5/test/benchmarks/helpers/helpers.go --- old/terragrunt-0.87.4/test/benchmarks/helpers/helpers.go 2025-09-16 21:54:29.000000000 +0200 +++ new/terragrunt-0.87.5/test/benchmarks/helpers/helpers.go 2025-09-22 20:52:57.000000000 +0200 @@ -107,3 +107,29 @@ b.ReportMetric(float64(planDuration.Seconds()), "plan_s/op") } + +func Apply(b *testing.B, dir string) { + b.Helper() + + // Track apply time + applyStart := time.Now() + + RunTerragruntCommand(b, "terragrunt", "run", "--all", "apply", "--non-interactive", "--working-dir", dir) + + applyDuration := time.Since(applyStart) + + b.ReportMetric(float64(applyDuration.Seconds()), "apply_s/op") +} + +func ApplyWithRunnerPool(b *testing.B, dir string) { + b.Helper() + + // Track apply time + applyStart := time.Now() + + RunTerragruntCommand(b, "terragrunt", "run", "--all", "apply", "--non-interactive", "--experiment", "runner-pool", "--working-dir", dir) + + applyDuration := time.Since(applyStart) + + b.ReportMetric(float64(applyDuration.Seconds()), "apply_s/op") +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terragrunt-0.87.4/test/benchmarks/integration_bench_test.go new/terragrunt-0.87.5/test/benchmarks/integration_bench_test.go --- old/terragrunt-0.87.4/test/benchmarks/integration_bench_test.go 2025-09-16 21:54:29.000000000 +0200 +++ new/terragrunt-0.87.5/test/benchmarks/integration_bench_test.go 2025-09-22 20:52:57.000000000 +0200 @@ -1,6 +1,8 @@ package test_test import ( + "fmt" + "math/rand" "os" "path/filepath" "testing" @@ -188,3 +190,315 @@ } }) } + +func BenchmarkUnitsNoDependencies(b *testing.B) { + baseMainTf := `resource "null_resource" "test" { + triggers = { + timestamp = timestamp() + } +}` + + emptyRootConfig := `` + includeRootConfig := `include "root" { + path = find_in_parent_folders("root.hcl") +} +terraform { + source = "." +} +` + + tmpDir := b.TempDir() + rootTerragruntConfigPath := filepath.Join(tmpDir, "root.hcl") + require.NoError(b, os.WriteFile(rootTerragruntConfigPath, []byte(emptyRootConfig), helpers.DefaultFilePermissions)) + + helpers.GenerateNUnits(b, tmpDir, 10, includeRootConfig, baseMainTf) + + helpers.Init(b, tmpDir) + + b.Run("default_runner", func(b *testing.B) { + // Warmups (not measured) + warmupApplies(b, tmpDir, false, 2) + b.ResetTimer() + + for b.Loop() { + helpers.Apply(b, tmpDir) + } + }) + + b.Run("runner_pool", func(b *testing.B) { + // Warmups (not measured) + warmupApplies(b, tmpDir, true, 2) + b.ResetTimer() + + for b.Loop() { + helpers.ApplyWithRunnerPool(b, tmpDir) + } + }) +} + +func BenchmarkUnitsNoDependenciesRandomWait(b *testing.B) { + emptyRootConfig := `` + includeRootConfig := `include "root" { + path = find_in_parent_folders("root.hcl") +} +terraform { + source = "." +} +` + + tmpDir := b.TempDir() + rootTerragruntConfigPath := filepath.Join(tmpDir, "root.hcl") + require.NoError(b, os.WriteFile(rootTerragruntConfigPath, []byte(emptyRootConfig), helpers.DefaultFilePermissions)) + + // Generate independent units with random 100-300ms waits + for i := 0; i < 10; i++ { + unitDir := filepath.Join(tmpDir, fmt.Sprintf("unit-%d", i)) + require.NoError(b, os.MkdirAll(unitDir, helpers.DefaultDirPermissions)) + + tgPath := filepath.Join(unitDir, "terragrunt.hcl") + require.NoError(b, os.WriteFile(tgPath, []byte(includeRootConfig), helpers.DefaultFilePermissions)) + + ms := 100 + rand.Intn(201) // 100..300 ms + secs := float64(ms) / 1000.0 + mainTf := fmt.Sprintf(`resource "null_resource" "wait" { + provisioner "local-exec" { + command = "bash -c 'sleep %.3f'" + } + triggers = { + timestamp = timestamp() + } +} +`, secs) + tfPath := filepath.Join(unitDir, "main.tf") + require.NoError(b, os.WriteFile(tfPath, []byte(mainTf), helpers.DefaultFilePermissions)) + } + + helpers.Init(b, tmpDir) + + b.Run("default_runner", func(b *testing.B) { + // Warmups (not measured) + warmupApplies(b, tmpDir, false, 2) + b.ResetTimer() + + for b.Loop() { + helpers.Apply(b, tmpDir) + } + }) + + b.Run("runner_pool", func(b *testing.B) { + // Warmups (not measured) + warmupApplies(b, tmpDir, true, 2) + b.ResetTimer() + + for b.Loop() { + helpers.ApplyWithRunnerPool(b, tmpDir) + } + }) +} + +func BenchmarkUnitsOneDependencyWithWait(b *testing.B) { + baseMainTf := `resource "null_resource" "test" { + triggers = { + timestamp = timestamp() + } +}` + + emptyRootConfig := `` + includeRootConfig := `include "root" { + path = find_in_parent_folders("root.hcl") +} +terraform { + source = "." +} +` + + tmpDir := b.TempDir() + rootTerragruntConfigPath := filepath.Join(tmpDir, "root.hcl") + require.NoError(b, os.WriteFile(rootTerragruntConfigPath, []byte(emptyRootConfig), helpers.DefaultFilePermissions)) + + // Create units + for i := 0; i < 10; i++ { + unitDir := filepath.Join(tmpDir, fmt.Sprintf("unit-%d", i)) + require.NoError(b, os.MkdirAll(unitDir, helpers.DefaultDirPermissions)) + + // terragrunt.hcl + var tgConfig string + if i == 2 { + // unit-2 depends on unit-1 + tgConfig = `include "root" { + path = find_in_parent_folders("root.hcl") +} +terraform { + source = "." +} +dependencies { + paths = ["../unit-1"] +}` + } else { + tgConfig = includeRootConfig + } + + tgPath := filepath.Join(unitDir, "terragrunt.hcl") + require.NoError(b, os.WriteFile(tgPath, []byte(tgConfig), helpers.DefaultFilePermissions)) + + // main.tf + var tfConfig string + if i == 1 { + // unit-1 has 400ms wait + tfConfig = `resource "null_resource" "wait" { + provisioner "local-exec" { + command = "bash -c 'sleep 0.4'" + } + triggers = { + timestamp = timestamp() + } +}` + } else { + tfConfig = baseMainTf + } + + tfPath := filepath.Join(unitDir, "main.tf") + require.NoError(b, os.WriteFile(tfPath, []byte(tfConfig), helpers.DefaultFilePermissions)) + } + + helpers.Init(b, tmpDir) + + b.Run("default_runner", func(b *testing.B) { + // Warmups (not measured) + warmupApplies(b, tmpDir, false, 2) + b.ResetTimer() + + for b.Loop() { + helpers.Apply(b, tmpDir) + } + }) + + b.Run("runner_pool", func(b *testing.B) { + // Warmups (not measured) + warmupApplies(b, tmpDir, true, 2) + b.ResetTimer() + + for b.Loop() { + helpers.ApplyWithRunnerPool(b, tmpDir) + } + }) +} + +// BenchmarkDependencyPairwiseOddDependsOnPrevEvenRandomWait generates N units (50, 100) where: +// - Every odd-indexed unit depends on the previous even-indexed unit (e.g., 1->0, 3->2, ...) +// - Even-indexed units perform a random sleep via local-exec to simulate workload (50..100ms) +// - Odd-indexed units are no-ops but depend on their paired even unit +// It measures apply times for both the default runner (configstack) and the runner pool on the SAME stack. +func BenchmarkDependencyPairwiseOddDependsOnPrevEvenRandomWait(b *testing.B) { + // Sizes for parameterized benchmark (2,4,8,...,128) + sizes := []int{2, 4, 8, 16, 32, 64, 128} + + emptyRootConfig := `` + includeRootConfig := `include "root" { + path = find_in_parent_folders("root.hcl") +} +terraform { + source = "." +} +` + + for _, n := range sizes { + b.Run(fmt.Sprintf("%d_units", n), func(b *testing.B) { + // Generate a single stack used by both runners + dir := b.TempDir() + + // Write root.hcl + require.NoError(b, os.WriteFile(filepath.Join(dir, "root.hcl"), []byte(emptyRootConfig), helpers.DefaultFilePermissions)) + + // Seed random generator deterministically within this sub-benchmark + rnd := rand.New(rand.NewSource(int64(n))) + + // Generate units where every odd depends on every even + for i := 0; i < n; i++ { + unitDir := filepath.Join(dir, fmt.Sprintf("unit-%d", i)) + require.NoError(b, os.MkdirAll(unitDir, helpers.DefaultDirPermissions)) + + // terragrunt.hcl: odd units depend only on the previous even unit (i-1) + var tgConfig string + + if i%2 == 1 { + prev := i - 1 + if prev >= 0 { + depBlock := fmt.Sprintf("dependency \"unit_%d\" {\n config_path = \"../unit-%d\"\n}\n\n", prev, prev) + tgConfig = includeRootConfig + depBlock + } else { + tgConfig = includeRootConfig + } + } else { + tgConfig = includeRootConfig + } + + tgPath := filepath.Join(unitDir, "terragrunt.hcl") + require.NoError(b, os.WriteFile(tgPath, []byte(tgConfig), helpers.DefaultFilePermissions)) + + // main.tf: even units wait 50..100ms; odd units are no-ops + var mainTf string + + if i%2 == 0 { // even: random sleep + ms := 50 + rnd.Intn(51) // 50..100ms + secs := float64(ms) / 1000.0 + mainTf = fmt.Sprintf(`resource "null_resource" "wait" { + provisioner "local-exec" { + command = "bash -c 'sleep %.3f'" + } + triggers = { + timestamp = timestamp() + } +} +`, secs) + } else { // odd: noop + mainTf = `resource "null_resource" "noop" { + triggers = { + timestamp = timestamp() + } +} +` + } + + tfPath := filepath.Join(unitDir, "main.tf") + require.NoError(b, os.WriteFile(tfPath, []byte(mainTf), helpers.DefaultFilePermissions)) + } + + // Init once to prepare + helpers.Init(b, dir) + + b.Run("configstack", func(b *testing.B) { + // Warmups (not measured) + warmupApplies(b, dir, false, 2) + b.ResetTimer() + + for b.Loop() { + helpers.Apply(b, dir) + } + }) + + b.Run("runner_pool", func(b *testing.B) { + // Warmups (not measured) + warmupApplies(b, dir, true, 2) + b.ResetTimer() + + for b.Loop() { + helpers.ApplyWithRunnerPool(b, dir) + } + }) + }) + } +} + +// warmupApplies performs a number of unmeasured apply runs to warm caches and workers. +func warmupApplies(b *testing.B, tmpDir string, useRunnerPool bool, count int) { + b.Helper() + + for range make([]struct{}, count) { + if useRunnerPool { + helpers.ApplyWithRunnerPool(b, tmpDir) + } else { + helpers.Apply(b, tmpDir) + } + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terragrunt-0.87.4/tf/getproviders/constraints.go new/terragrunt-0.87.5/tf/getproviders/constraints.go --- old/terragrunt-0.87.4/tf/getproviders/constraints.go 2025-09-16 21:54:29.000000000 +0200 +++ new/terragrunt-0.87.5/tf/getproviders/constraints.go 2025-09-22 20:52:57.000000000 +0200 @@ -2,6 +2,7 @@ import ( "fmt" + "maps" "os" "path/filepath" "strings" @@ -52,9 +53,7 @@ } // Merge constraints from this file - for addr, constraint := range fileConstraints { - constraints[addr] = constraint - } + maps.Copy(constraints, fileConstraints) } return constraints, nil @@ -96,9 +95,7 @@ providerConstraints := parseProvidersFromRequiredProvidersBlock(opts, nestedBlock) // Merge constraints from this required_providers block - for addr, constraint := range providerConstraints { - constraints[addr] = constraint - } + maps.Copy(constraints, providerConstraints) } } ++++++ terragrunt.obsinfo ++++++ --- /var/tmp/diff_new_pack.KD9QeP/_old 2025-09-23 16:08:15.330027416 +0200 +++ /var/tmp/diff_new_pack.KD9QeP/_new 2025-09-23 16:08:15.362028778 +0200 @@ -1,5 +1,5 @@ name: terragrunt -version: 0.87.4 -mtime: 1758052469 -commit: 35f6895bf502d599697d6cc8ef3f3551c65e3b66 +version: 0.87.5 +mtime: 1758567177 +commit: 9a6c90736e85eed732e87320cd34eae0fd9a16f9 ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/terragrunt/vendor.tar.gz /work/SRC/openSUSE:Factory/.terragrunt.new.27445/vendor.tar.gz differ: char 13, line 1
