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

Reply via email to