@@ -35,6 +35,7 @@ import (
3535 "github.com/cloudposse/atmos/pkg/reexec"
3636 stepPkg "github.com/cloudposse/atmos/pkg/runner/step"
3737 "github.com/cloudposse/atmos/pkg/schema"
38+ "github.com/cloudposse/atmos/pkg/telemetry"
3839 "github.com/cloudposse/atmos/pkg/ui"
3940 u "github.com/cloudposse/atmos/pkg/utils"
4041 "github.com/cloudposse/atmos/pkg/version"
@@ -52,6 +53,11 @@ var missingConfigFoundMarkdown string
5253// Define a constant for the dot string that appears multiple times.
5354const currentDirPath = "."
5455
56+ const (
57+ customCommandKeyCommand = "command"
58+ customCommandKeyIdentity = "identity"
59+ )
60+
5561// FlagStack is the name of the stack flag used across commands.
5662const FlagStack = "stack"
5763
@@ -301,7 +307,7 @@ func preCustomCommand(
301307 // no "steps" means a sub command should be specified
302308 if len (commandConfig .Steps ) == 0 {
303309 if err := cmd .Help (); err != nil {
304- log .Trace ("Failed to display command help" , "error" , err , "command" , cmd .Name ())
310+ log .Trace ("Failed to display command help" , "error" , err , customCommandKeyCommand , cmd .Name ())
305311 }
306312 errUtils .Exit (0 )
307313 }
@@ -365,7 +371,7 @@ func createCustomCommand(
365371 customCommand .PersistentFlags ().Bool ("" , false , doubleDashHint )
366372
367373 // Add --identity flag to all custom commands to allow runtime override.
368- customCommand .PersistentFlags ().String ("identity" , "" , "Identity to use for authentication (overrides identity in command config)" )
374+ customCommand .PersistentFlags ().String (customCommandKeyIdentity , "" , "Identity to use for authentication (overrides identity in command config)" )
369375 AddIdentityCompletion (customCommand )
370376
371377 if err := validateCustomCommandFlags (commandConfig , parentCommand ); err != nil {
@@ -404,7 +410,7 @@ func validateFlag(cmdName string, flag *schema.CommandFlag, seen map[string]bool
404410 return errUtils .Build (errUtils .ErrDuplicateFlagRegistration ).
405411 WithExplanation (fmt .Sprintf ("Custom command '%s' defines duplicate flag '--%s'" , cmdName , flag .Name )).
406412 WithHint ("Remove or rename the duplicate flag in your atmos.yaml" ).
407- WithContext ("command" , cmdName ).
413+ WithContext (customCommandKeyCommand , cmdName ).
408414 WithContext ("flag" , flag .Name ).
409415 Err ()
410416 }
@@ -441,7 +447,7 @@ func validateFlagTypeConflict(cmdName string, flag *schema.CommandFlag, existing
441447 cmdName , flag .Name , customFlagType , existingFlagType )).
442448 WithHint ("Check the 'commands' section in atmos.yaml" ).
443449 WithHint ("Either use the existing flag type, or rename your flag to avoid conflicts" ).
444- WithContext ("command" , cmdName ).
450+ WithContext (customCommandKeyCommand , cmdName ).
445451 WithContext ("flag" , flag .Name ).
446452 WithContext ("declared_type" , customFlagType ).
447453 WithContext ("existing_type" , existingFlagType ).
@@ -461,7 +467,7 @@ func validateFlagShorthand(cmdName string, flag *schema.CommandFlag, seen map[st
461467 return errUtils .Build (errUtils .ErrDuplicateFlagRegistration ).
462468 WithExplanation (fmt .Sprintf ("Custom command '%s' defines invalid shorthand '-%s' for flag '--%s'" , cmdName , flag .Shorthand , flag .Name )).
463469 WithHint ("Use exactly one character for shorthand flags" ).
464- WithContext ("command" , cmdName ).
470+ WithContext (customCommandKeyCommand , cmdName ).
465471 WithContext ("flag" , flag .Name ).
466472 WithContext ("shorthand" , flag .Shorthand ).
467473 Err ()
@@ -471,7 +477,7 @@ func validateFlagShorthand(cmdName string, flag *schema.CommandFlag, seen map[st
471477 return errUtils .Build (errUtils .ErrDuplicateFlagRegistration ).
472478 WithExplanation (fmt .Sprintf ("Custom command '%s' defines duplicate flag shorthand '-%s'" , cmdName , flag .Shorthand )).
473479 WithHint ("Remove or change the duplicate shorthand in your atmos.yaml" ).
474- WithContext ("command" , cmdName ).
480+ WithContext (customCommandKeyCommand , cmdName ).
475481 WithContext ("shorthand" , flag .Shorthand ).
476482 Err ()
477483 }
@@ -490,7 +496,7 @@ func validateFlagShorthand(cmdName string, flag *schema.CommandFlag, seen map[st
490496 cmdName , flag .Shorthand , existingByShorthand .Name )).
491497 WithHint ("Check the 'commands' section in atmos.yaml" ).
492498 WithHint ("Change the shorthand to avoid conflicts" ).
493- WithContext ("command" , cmdName ).
499+ WithContext (customCommandKeyCommand , cmdName ).
494500 WithContext ("shorthand" , flag .Shorthand ).
495501 WithContext ("existing_flag" , existingByShorthand .Name ).
496502 WithContext ("config_path" , fmt .Sprintf ("commands.%s.flags" , cmdName )).
@@ -610,6 +616,23 @@ func executeCustomCommand(
610616 finalArgs = args
611617 }
612618
619+ conditionContext := customCommandConditionContext ()
620+ hasRunnableStep := false
621+ for i := range commandConfig .Steps {
622+ step := & commandConfig .Steps [i ]
623+ if err := schema .ValidateStepCondition (step .When ); err != nil {
624+ errUtils .CheckErrorPrintAndExit (err , "" , "" )
625+ }
626+ if step .When .Evaluate (conditionContext ) {
627+ hasRunnableStep = true
628+ break
629+ }
630+ }
631+ if ! hasRunnableStep {
632+ log .Debug ("Skipping custom command, no steps matched `when` conditions" , customCommandKeyCommand , commandConfig .Name )
633+ return
634+ }
635+
613636 // Resolve and install command dependencies.
614637 // First, load tools from .tool-versions (project-wide defaults).
615638 // Then merge with command-specific dependencies (command deps override .tool-versions).
@@ -652,7 +675,7 @@ func executeCustomCommand(
652675 }
653676
654677 if len (deps ) > 0 {
655- log .Debug ("Installing command dependencies" , "command" , commandConfig .Name , "tools" , deps )
678+ log .Debug ("Installing command dependencies" , customCommandKeyCommand , commandConfig .Name , "tools" , deps )
656679 installer := dependencies .NewInstaller (& atmosConfig )
657680 if err := installer .EnsureTools (deps ); err != nil {
658681 err = errUtils .Build (errUtils .ErrToolInstall ).
@@ -674,60 +697,25 @@ func executeCustomCommand(
674697 }
675698 }
676699
677- // Create auth manager if identity is specified for this custom command.
700+ // Create auth manager if identity is specified for this custom command and
701+ // at least one step will run.
678702 // Check for --identity flag first (it overrides the config).
679- var authManager auth.AuthManager
680- var authStackInfo * schema.ConfigAndStacksInfo
681- identityFlag , _ := cmd .Flags ().GetString ("identity" )
703+ identityFlag , _ := cmd .Flags ().GetString (customCommandKeyIdentity )
682704 commandIdentity := strings .TrimSpace (identityFlag )
683705 if commandIdentity == "" {
684706 // Fall back to identity from command config
685707 commandIdentity = strings .TrimSpace (commandConfig .Identity )
686708 }
687709
688- if commandIdentity != "" {
689- // Create a ConfigAndStacksInfo for the auth manager to populate with AuthContext.
690- // This enables YAML template functions to access authenticated credentials.
691- authStackInfo = & schema.ConfigAndStacksInfo {
692- AuthContext : & schema.AuthContext {},
693- }
694-
695- credStore := credentials .NewCredentialStoreWithConfig (& atmosConfig .Auth )
696- validator := validation .NewValidator ()
697- authManager , err = auth .NewAuthManager (& atmosConfig .Auth , credStore , validator , authStackInfo , atmosConfig .CliConfigPath )
698- if err != nil {
699- errUtils .CheckErrorPrintAndExit (fmt .Errorf ("%w: %w" , errUtils .ErrFailedToInitializeAuthManager , err ), "" , "" )
700- }
701-
702- ctx := context .Background ()
703-
704- // Try to use cached credentials first (passive check, no prompts).
705- // Only authenticate if cached credentials are not available or expired.
706- _ , err = authManager .GetCachedCredentials (ctx , commandIdentity )
707- if err != nil {
708- log .Debug ("No valid cached credentials found, authenticating" , "identity" , commandIdentity , "error" , err )
709- // No valid cached credentials - perform full authentication.
710- _ , err = authManager .Authenticate (ctx , commandIdentity )
711- if err != nil {
712- // Check for user cancellation - return clean error without wrapping.
713- if errors .Is (err , errUtils .ErrUserAborted ) {
714- errUtils .CheckErrorPrintAndExit (errUtils .ErrUserAborted , "" , "" )
715- }
716- errUtils .CheckErrorPrintAndExit (fmt .Errorf ("%w for identity %q in custom command %q: %w" ,
717- errUtils .ErrAuthenticationFailed , commandIdentity , commandConfig .Name , err ), "" , "" )
718- }
719- }
720-
721- log .Debug ("Authenticated with identity for custom command" , "identity" , commandIdentity , "command" , commandConfig .Name )
722- }
710+ authManager := prepareCustomCommandAuth (& atmosConfig , commandIdentity , commandConfig .Name , hasRunnableStep )
723711
724712 // Determine working directory for command execution.
725713 workDir , err := resolveWorkingDirectory (commandConfig .WorkingDirectory , atmosConfig .BasePath , currentDirPath )
726714 if err != nil {
727715 errUtils .CheckErrorPrintAndExit (err , "Invalid working_directory" , "https://atmos.tools/cli/configuration/commands/working-directory" )
728716 }
729717 if commandConfig .WorkingDirectory != "" {
730- log .Debug ("Using working directory for custom command" , "command" , commandConfig .Name , "working_directory" , workDir )
718+ log .Debug ("Using working directory for custom command" , customCommandKeyCommand , commandConfig .Name , "working_directory" , workDir )
731719 }
732720
733721 // Validate exec steps before executing anything: an exec step replaces
@@ -742,6 +730,11 @@ func executeCustomCommand(
742730
743731 // Execute custom command's steps
744732 for i , step := range commandConfig .Steps {
733+ if ! step .When .Evaluate (conditionContext ) {
734+ log .Debug ("Skipping custom command step, `when` condition did not match" , customCommandKeyCommand , commandConfig .Name , "step" , i )
735+ continue
736+ }
737+
745738 // Prepare template data for arguments
746739 argumentsData := map [string ]string {}
747740 for ix , arg := range commandConfig .Arguments {
@@ -882,7 +875,7 @@ func executeCustomCommand(
882875 errUtils .CheckErrorPrintAndExit (fmt .Errorf ("failed to prepare shell environment for identity %q in custom command %q step %d: %w" ,
883876 commandIdentity , commandConfig .Name , i , err ), "" , "" )
884877 }
885- log .Debug ("Prepared environment with identity for custom command step" , "identity" , commandIdentity , "command" , commandConfig .Name , "step" , i )
878+ log .Debug ("Prepared environment with identity for custom command step" , customCommandKeyIdentity , commandIdentity , customCommandKeyCommand , commandConfig .Name , "step" , i )
886879 }
887880
888881 // Process Go templates in the command's steps.
@@ -982,6 +975,47 @@ func executeCustomCommand(
982975 }
983976}
984977
978+ func customCommandConditionContext () schema.ConditionContext {
979+ return schema.ConditionContext {
980+ CI : telemetry .IsCI (),
981+ Status : schema .ConditionPredicateSuccess ,
982+ }
983+ }
984+
985+ func prepareCustomCommandAuth (atmosConfig * schema.AtmosConfiguration , commandIdentity , commandName string , hasRunnableStep bool ) auth.AuthManager {
986+ if commandIdentity == "" || ! hasRunnableStep {
987+ return nil
988+ }
989+
990+ authStackInfo := & schema.ConfigAndStacksInfo {
991+ AuthContext : & schema.AuthContext {},
992+ }
993+ credStore := credentials .NewCredentialStoreWithConfig (& atmosConfig .Auth )
994+ validator := validation .NewValidator ()
995+ authManager , err := auth .NewAuthManager (& atmosConfig .Auth , credStore , validator , authStackInfo , atmosConfig .CliConfigPath )
996+ if err != nil {
997+ errUtils .CheckErrorPrintAndExit (fmt .Errorf ("%w: %w" , errUtils .ErrFailedToInitializeAuthManager , err ), "" , "" )
998+ }
999+
1000+ ctx := context .Background ()
1001+ if _ , err = authManager .GetCachedCredentials (ctx , commandIdentity ); err == nil {
1002+ log .Debug ("Authenticated with cached identity for custom command" , customCommandKeyIdentity , commandIdentity , customCommandKeyCommand , commandName )
1003+ return authManager
1004+ }
1005+
1006+ log .Debug ("No valid cached credentials found, authenticating" , customCommandKeyIdentity , commandIdentity , "error" , err )
1007+ if _ , err = authManager .Authenticate (ctx , commandIdentity ); err == nil {
1008+ log .Debug ("Authenticated with identity for custom command" , customCommandKeyIdentity , commandIdentity , customCommandKeyCommand , commandName )
1009+ return authManager
1010+ }
1011+ if errors .Is (err , errUtils .ErrUserAborted ) {
1012+ errUtils .CheckErrorPrintAndExit (errUtils .ErrUserAborted , "" , "" )
1013+ }
1014+ errUtils .CheckErrorPrintAndExit (fmt .Errorf ("%w for identity %q in custom command %q: %w" ,
1015+ errUtils .ErrAuthenticationFailed , commandIdentity , commandName , err ), "" , "" )
1016+ return authManager
1017+ }
1018+
9851019// cloneCommand clones a custom command config into a new struct.
9861020func cloneCommand (orig * schema.Command ) (* schema.Command , error ) {
9871021 origJSON , err := json .Marshal (orig )
@@ -1661,8 +1695,8 @@ func identityFlagCompletion(cmd *cobra.Command, args []string, toComplete string
16611695
16621696// AddIdentityCompletion registers shell completion for the identity flag if present on the command.
16631697func AddIdentityCompletion (cmd * cobra.Command ) {
1664- if cmd .Flag ("identity" ) != nil {
1665- if err := cmd .RegisterFlagCompletionFunc ("identity" , identityFlagCompletion ); err != nil {
1698+ if cmd .Flag (customCommandKeyIdentity ) != nil {
1699+ if err := cmd .RegisterFlagCompletionFunc (customCommandKeyIdentity , identityFlagCompletion ); err != nil {
16661700 log .Trace ("Failed to register identity flag completion" , "error" , err )
16671701 }
16681702 }
0 commit comments