@@ -16,6 +16,7 @@ import (
1616 "github.com/spf13/viper"
1717
1818 errUtils "github.com/cloudposse/atmos/errors"
19+ envpkg "github.com/cloudposse/atmos/pkg/env"
1920 log "github.com/cloudposse/atmos/pkg/logger"
2021 "github.com/cloudposse/atmos/pkg/perf"
2122 "github.com/cloudposse/atmos/pkg/schema"
@@ -49,7 +50,10 @@ func ExecuteShellCommand(
4950 updatedEnv := append (env , fmt .Sprintf ("ATMOS_SHLVL=%d" , newShellLevel ))
5051
5152 cmd := exec .Command (command , args ... )
52- cmd .Env = append (os .Environ (), updatedEnv ... )
53+ // Build environment: os.Environ() + global env (atmos.yaml) + command-specific env.
54+ // Global env has lowest priority after system env, command-specific env overrides both.
55+ baseEnv := envpkg .MergeGlobalEnv (os .Environ (), atmosConfig .Env )
56+ cmd .Env = append (baseEnv , updatedEnv ... )
5357 cmd .Dir = dir
5458 cmd .Stdin = os .Stdin
5559 cmd .Stdout = os .Stdout
@@ -110,7 +114,7 @@ func ExecuteShell(
110114 command string ,
111115 name string ,
112116 dir string ,
113- env []string ,
117+ envVars []string ,
114118 dryRun bool ,
115119) error {
116120 defer perf .Track (nil , "exec.ExecuteShell" )()
@@ -126,8 +130,8 @@ func ExecuteShell(
126130 // This matches the behavior before commit 9fd7d156a where the environment
127131 // was merged rather than replaced.
128132 mergedEnv := os .Environ ()
129- for _ , envVar := range env {
130- mergedEnv = u .UpdateEnvVar (mergedEnv , parseEnvVarKey (envVar ), parseEnvVarValue (envVar ))
133+ for _ , envVar := range envVars {
134+ mergedEnv = envpkg .UpdateEnvVar (mergedEnv , parseEnvVarKey (envVar ), parseEnvVarValue (envVar ))
131135 }
132136
133137 mergedEnv = append (mergedEnv , fmt .Sprintf ("ATMOS_SHLVL=%d" , newShellLevel ))
@@ -243,8 +247,9 @@ func execTerraformShellCommand(
243247
244248 log .Debug ("Setting the ENV vars in the shell" )
245249
246- // Merge env vars, ensuring componentEnvList takes precedence
247- mergedEnv := mergeEnvVars (componentEnvList )
250+ // Merge env vars, ensuring componentEnvList takes precedence.
251+ // Include global env from atmos.yaml (lowest priority after system env).
252+ mergedEnv := mergeEnvVarsWithGlobal (componentEnvList , atmosConfig .Env )
248253
249254 // Transfer stdin, stdout, and stderr to the new process and also set the target directory for the shell to start in
250255 pa := os.ProcAttr {
@@ -341,7 +346,8 @@ func ExecAuthShellCommand(
341346 printShellEnterMessage (identityName , providerName )
342347
343348 // Merge env vars, ensuring authEnvList takes precedence.
344- mergedEnv := mergeEnvVarsSimple (authEnvList )
349+ // Include global env from atmos.yaml (lowest priority after system env).
350+ mergedEnv := mergeEnvVarsSimpleWithGlobal (authEnvList , atmosConfig .Env )
345351
346352 // Determine shell command and args.
347353 shellCommand , shellCommandArgs := determineShell (shellOverride , shellArgs )
@@ -529,6 +535,52 @@ func mergeEnvVars(componentEnvList []string) []string {
529535 return merged
530536}
531537
538+ // mergeEnvVarsWithGlobal is like mergeEnvVars but also includes global env from atmos.yaml.
539+ // Priority order: system env < global env (atmos.yaml) < component env.
540+ func mergeEnvVarsWithGlobal (componentEnvList []string , globalEnv map [string ]string ) []string {
541+ envMap := make (map [string ]string )
542+
543+ // Parse system environment variables.
544+ for _ , env := range os .Environ () {
545+ if parts := strings .SplitN (env , "=" , 2 ); len (parts ) == 2 {
546+ envMap [parts [0 ]] = parts [1 ]
547+ }
548+ }
549+
550+ // Apply global env from atmos.yaml (can override system env).
551+ for k , v := range globalEnv {
552+ envMap [k ] = v
553+ }
554+
555+ // Merge with new, Atmos defined environment variables (highest priority).
556+ for _ , env := range componentEnvList {
557+ if parts := strings .SplitN (env , "=" , 2 ); len (parts ) == 2 {
558+ // Special handling for Terraform CLI arguments environment variables.
559+ if strings .HasPrefix (parts [0 ], "TF_CLI_ARGS_" ) {
560+ // For TF_CLI_ARGS_* variables, we need to append new values to any existing values.
561+ if existing , exists := envMap [parts [0 ]]; exists {
562+ // Put the new, Atmos defined value first so it takes precedence.
563+ envMap [parts [0 ]] = parts [1 ] + " " + existing
564+ } else {
565+ // No existing value, just set the new value.
566+ envMap [parts [0 ]] = parts [1 ]
567+ }
568+ } else {
569+ // For all other environment variables, just override any existing value.
570+ envMap [parts [0 ]] = parts [1 ]
571+ }
572+ }
573+ }
574+
575+ // Convert back to slice.
576+ merged := make ([]string , 0 , len (envMap ))
577+ for k , v := range envMap {
578+ log .Trace ("Setting ENV var" , "key" , k , "value" , v )
579+ merged = append (merged , k + "=" + v )
580+ }
581+ return merged
582+ }
583+
532584// mergeEnvVarsSimple adds a list of environment variables to the system environment variables without special handling.
533585func mergeEnvVarsSimple (newEnvList []string ) []string {
534586 envMap := make (map [string ]string )
@@ -556,6 +608,39 @@ func mergeEnvVarsSimple(newEnvList []string) []string {
556608 return merged
557609}
558610
611+ // mergeEnvVarsSimpleWithGlobal is like mergeEnvVarsSimple but includes global env from atmos.yaml.
612+ // Priority order: system env < global env (atmos.yaml) < new env list.
613+ func mergeEnvVarsSimpleWithGlobal (newEnvList []string , globalEnv map [string ]string ) []string {
614+ envMap := make (map [string ]string )
615+
616+ // Parse system environment variables.
617+ for _ , env := range os .Environ () {
618+ if parts := strings .SplitN (env , envVarSeparator , 2 ); len (parts ) == 2 {
619+ envMap [parts [0 ]] = parts [1 ]
620+ }
621+ }
622+
623+ // Apply global env from atmos.yaml (can override system env).
624+ for k , v := range globalEnv {
625+ envMap [k ] = v
626+ }
627+
628+ // Merge with new environment variables (override any existing).
629+ for _ , env := range newEnvList {
630+ if parts := strings .SplitN (env , envVarSeparator , 2 ); len (parts ) == 2 {
631+ envMap [parts [0 ]] = parts [1 ]
632+ }
633+ }
634+
635+ // Convert back to slice.
636+ merged := make ([]string , 0 , len (envMap ))
637+ for k , v := range envMap {
638+ log .Trace ("Setting ENV var" , "key" , k , "value" , v )
639+ merged = append (merged , k + envVarSeparator + v )
640+ }
641+ return merged
642+ }
643+
559644// printShellEnterMessage prints a user-facing message when entering an Atmos-managed shell.
560645func printShellEnterMessage (identityName , providerName string ) {
561646 headerStyle := lipgloss .NewStyle ().
0 commit comments