@@ -2455,3 +2455,160 @@ locals {
24552455 "catalog units should be excluded by the negated filter, but found: %s" , p )
24562456 }
24572457}
2458+
2459+ // TestWorktreePhase_Integration_GitFilterExclusionPreventsParsingInWorktree verifies that
2460+ // a negated filter prevents excluded units from being parsed inside worktree sub-discoveries.
2461+ // The land-mine unit uses run_cmd("exit 1") which would cause a fatal parse error if evaluated.
2462+ func TestWorktreePhase_Integration_GitFilterExclusionPreventsParsingInWorktree (t * testing.T ) {
2463+ t .Parallel ()
2464+
2465+ tmpDir , runner := setupGitRepo (t )
2466+
2467+ createUnit (t , tmpDir , "app" , `# App unit` )
2468+ createUnit (t , tmpDir , "land-mine" , `
2469+ locals {
2470+ boom = run_cmd("--terragrunt-quiet", "bash", "-c", "exit 1")
2471+ }
2472+ ` )
2473+
2474+ commitChanges (t , runner , "Initial commit" )
2475+
2476+ err := os .WriteFile (filepath .Join (tmpDir , "app" , "terragrunt.hcl" ), []byte (`# Modified app` ), 0o644 )
2477+ require .NoError (t , err )
2478+
2479+ err = os .WriteFile (filepath .Join (tmpDir , "land-mine" , "terragrunt.hcl" ), []byte (`
2480+ locals {
2481+ boom = run_cmd("--terragrunt-quiet", "bash", "-c", "exit 1")
2482+ modified = true
2483+ }
2484+ ` ), 0o644 )
2485+ require .NoError (t , err )
2486+
2487+ commitChanges (t , runner , "Modify both units" )
2488+
2489+ l := logger .CreateLogger ()
2490+ filters , parseErr := filter .ParseFilterQueries (l , []string {"[HEAD~1...HEAD]" , "!./land-mine" })
2491+ require .NoError (t , parseErr )
2492+
2493+ w , err := worktrees .NewWorktrees (t .Context (), l , worktrees.WorktreeOpts {
2494+ WorkingDir : tmpDir ,
2495+ GitExpressions : filters .UniqueGitFilters (),
2496+ })
2497+ require .NoError (t , err )
2498+
2499+ t .Cleanup (func () {
2500+ cleanupErr := w .Cleanup (context .WithoutCancel (t .Context ()), l )
2501+ require .NoError (t , cleanupErr )
2502+ })
2503+
2504+ opts := options .NewTerragruntOptions ()
2505+ opts .WorkingDir = tmpDir
2506+ opts .RootWorkingDir = tmpDir
2507+
2508+ d := discovery .NewDiscovery (tmpDir ).
2509+ WithDiscoveryContext (& component.DiscoveryContext {WorkingDir : tmpDir , Cmd : "plan" }).
2510+ WithWorktrees (w ).
2511+ WithRelationships ().
2512+ WithFilters (filters )
2513+
2514+ // If the land-mine unit is parsed, run_cmd("exit 1") causes a fatal error.
2515+ components , err := d .Discover (t .Context (), l , opts )
2516+ require .NoError (t , err )
2517+
2518+ unitPaths := components .Filter (component .UnitKind ).Paths ()
2519+
2520+ toWorktree := w .WorktreePairs ["[HEAD~1...HEAD]" ].ToWorktree .Path
2521+ assert .Contains (t , unitPaths , filepath .Join (toWorktree , "app" ), "app should be discovered" )
2522+
2523+ for _ , p := range unitPaths {
2524+ assert .NotContains (t , p , "land-mine" , "land-mine should not be discovered: %s" , p )
2525+ }
2526+ }
2527+
2528+ // TestWorktreePhase_Integration_StackDiscoveryDoesNotParseUnits verifies that
2529+ // discoverStacks (via GenerateStacks) does not parse non-stack components even when
2530+ // reading filters trigger the parse phase. The land-mine unit uses run_cmd("exit 1")
2531+ // which would cause a fatal error if parsed.
2532+ func TestWorktreePhase_Integration_StackDiscoveryDoesNotParseUnits (t * testing.T ) {
2533+ t .Parallel ()
2534+
2535+ tmpDir , runner := setupGitRepo (t )
2536+
2537+ // Create a land-mine unit at the repo root
2538+ createUnit (t , tmpDir , "land-mine" , `
2539+ locals {
2540+ boom = run_cmd("--terragrunt-quiet", "bash", "-c", "exit 1")
2541+ }
2542+ ` )
2543+
2544+ // Create a catalog unit for the stack to source
2545+ catalogDir := filepath .Join (tmpDir , "catalog" , "units" , "myapp" )
2546+ err := os .MkdirAll (catalogDir , 0o755 )
2547+ require .NoError (t , err )
2548+
2549+ err = os .WriteFile (filepath .Join (catalogDir , "terragrunt.hcl" ), []byte (`# catalog unit` ), 0o644 )
2550+ require .NoError (t , err )
2551+
2552+ err = os .WriteFile (filepath .Join (catalogDir , "main.tf" ), []byte (`output "ok" { value = "ok" }` ), 0o644 )
2553+ require .NoError (t , err )
2554+
2555+ // Create a stack that reads a config file (triggers reading filter path in worktreeStacksToGenerate)
2556+ stackDir := filepath .Join (tmpDir , "live" , "stack" )
2557+ err = os .MkdirAll (stackDir , 0o755 )
2558+ require .NoError (t , err )
2559+
2560+ err = os .WriteFile (filepath .Join (stackDir , "config.hcl" ), []byte (`inputs = { v = "v1" }` ), 0o644 )
2561+ require .NoError (t , err )
2562+
2563+ err = os .WriteFile (filepath .Join (stackDir , "terragrunt.stack.hcl" ), []byte (`
2564+ locals {
2565+ config = read_terragrunt_config("config.hcl")
2566+ }
2567+
2568+ unit "myapp" {
2569+ source = "${get_repo_root()}/catalog/units/myapp"
2570+ path = "myapp"
2571+ values = local.config.inputs
2572+ }
2573+ ` ), 0o644 )
2574+ require .NoError (t , err )
2575+
2576+ commitChanges (t , runner , "Initial commit" )
2577+
2578+ // Change only the config file (triggers reading filter, not a direct stack change)
2579+ err = os .WriteFile (filepath .Join (stackDir , "config.hcl" ), []byte (`inputs = { v = "v2" }` ), 0o644 )
2580+ require .NoError (t , err )
2581+
2582+ commitChanges (t , runner , "Update config file" )
2583+
2584+ l := logger .CreateLogger ()
2585+ gitExpressions := filter.GitExpressions {filter .NewGitExpression ("HEAD~1" , "HEAD" )}
2586+
2587+ w , err := worktrees .NewWorktrees (t .Context (), l , worktrees.WorktreeOpts {
2588+ WorkingDir : tmpDir ,
2589+ GitExpressions : gitExpressions ,
2590+ })
2591+ require .NoError (t , err )
2592+
2593+ t .Cleanup (func () {
2594+ cleanupErr := w .Cleanup (context .WithoutCancel (t .Context ()), l )
2595+ require .NoError (t , cleanupErr )
2596+ })
2597+
2598+ opts := options .NewTerragruntOptions ()
2599+ opts .WorkingDir = tmpDir
2600+ opts .RootWorkingDir = tmpDir
2601+
2602+ parsedFilters , parseErr := filter .ParseFilterQueries (l , []string {"[HEAD~1...HEAD]" })
2603+ require .NoError (t , parseErr )
2604+
2605+ opts .Filters = parsedFilters
2606+ opts .Experiments = experiment .NewExperiments ()
2607+ err = opts .Experiments .EnableExperiment (experiment .FilterFlag )
2608+ require .NoError (t , err )
2609+
2610+ // GenerateStacks internally calls discoverStacks with reading filters.
2611+ // If the land-mine unit is parsed, run_cmd("exit 1") causes a fatal error.
2612+ err = generate .GenerateStacks (t .Context (), l , opts , w )
2613+ require .NoError (t , err )
2614+ }
0 commit comments