diff --git a/cmd/account/access/access.go b/cmd/account/access/access.go index 32baab97..cc8a1ea7 100644 --- a/cmd/account/access/access.go +++ b/cmd/account/access/access.go @@ -10,6 +10,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/accessrequest" "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -25,6 +26,7 @@ func New(runtimeCtx *runtime.Context) *cobra.Command { }, } + runtimeattach.Register(cmd, runtimeattach.CredsAndTenant) return cmd } diff --git a/cmd/account/account.go b/cmd/account/account.go index bc96644c..01eba27c 100644 --- a/cmd/account/account.go +++ b/cmd/account/account.go @@ -8,6 +8,7 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/account/list_key" "github.com/smartcontractkit/cre-cli/cmd/account/unlink_key" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" ) func New(runtimeContext *runtime.Context) *cobra.Command { @@ -22,5 +23,6 @@ func New(runtimeContext *runtime.Context) *cobra.Command { accountCmd.AddCommand(unlink_key.New(runtimeContext)) accountCmd.AddCommand(list_key.New(runtimeContext)) + runtimeattach.Register(accountCmd, runtimeattach.Empty) return accountCmd } diff --git a/cmd/account/link_key/link_key.go b/cmd/account/link_key/link_key.go index 7416bfa9..460c5e53 100644 --- a/cmd/account/link_key/link_key.go +++ b/cmd/account/link_key/link_key.go @@ -26,6 +26,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/types" "github.com/smartcontractkit/cre-cli/internal/ui" @@ -89,6 +90,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { settings.AddSkipConfirmation(cmd) cmd.Flags().StringP("owner-label", "l", "", "Label for the workflow owner") + runtimeattach.Register(cmd, runtimeattach.FullWithDeploymentRPC) return cmd } diff --git a/cmd/account/list_key/list_key.go b/cmd/account/list_key/list_key.go index 0e0f3f14..84c207fb 100644 --- a/cmd/account/list_key/list_key.go +++ b/cmd/account/list_key/list_key.go @@ -13,6 +13,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -58,6 +59,7 @@ func New(runtimeCtx *runtime.Context) *cobra.Command { return h.Execute(cmd.Context()) }, } + runtimeattach.Register(cmd, runtimeattach.CredsAndTenant) return cmd } diff --git a/cmd/account/unlink_key/unlink_key.go b/cmd/account/unlink_key/unlink_key.go index 12dd10c7..036454a1 100644 --- a/cmd/account/unlink_key/unlink_key.go +++ b/cmd/account/unlink_key/unlink_key.go @@ -25,6 +25,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/types" "github.com/smartcontractkit/cre-cli/internal/ui" @@ -87,6 +88,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { } settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) + runtimeattach.Register(cmd, runtimeattach.FullWithDeploymentRPC) return cmd } diff --git a/cmd/creinit/creinit.go b/cmd/creinit/creinit.go index 35ad3502..7a012f71 100644 --- a/cmd/creinit/creinit.go +++ b/cmd/creinit/creinit.go @@ -15,6 +15,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/templateconfig" "github.com/smartcontractkit/cre-cli/internal/templaterepo" @@ -82,6 +83,7 @@ Templates are fetched dynamically from GitHub repositories.`, _ = initCmd.Flags().MarkDeprecated("template-id", "use --template instead") _ = initCmd.Flags().MarkHidden("template-id") + runtimeattach.Register(initCmd, runtimeattach.Empty) return initCmd } diff --git a/cmd/creinit/go_module_init.go b/cmd/creinit/go_module_init.go index fb1f1cee..8cd18a5c 100644 --- a/cmd/creinit/go_module_init.go +++ b/cmd/creinit/go_module_init.go @@ -35,16 +35,16 @@ func initializeGoModule(logger *zerolog.Logger, workingDirectory, moduleName str } } - if err := runCommand(logger, workingDirectory, "go", "get", "github.com/smartcontractkit/cre-sdk-go@"+constants.SdkVersion); err != nil { - return nil, err - } - if err := runCommand(logger, workingDirectory, "go", "get", "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm@"+constants.EVMCapabilitiesVersion); err != nil { - return nil, err - } - if err := runCommand(logger, workingDirectory, "go", "get", "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http@"+constants.HTTPCapabilitiesVersion); err != nil { - return nil, err + // Single go get: one module graph resolution instead of four sequential + // downloads (important for tests under tight -timeout, e.g. -timeout=120s). + getArgs := []string{ + "get", + "github.com/smartcontractkit/cre-sdk-go@" + constants.SdkVersion, + "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm@" + constants.EVMCapabilitiesVersion, + "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http@" + constants.HTTPCapabilitiesVersion, + "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron@" + constants.CronCapabilitiesVersion, } - if err := runCommand(logger, workingDirectory, "go", "get", "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron@"+constants.CronCapabilitiesVersion); err != nil { + if err := runCommand(logger, workingDirectory, "go", getArgs...); err != nil { return nil, err } @@ -67,6 +67,7 @@ func runCommand(logger *zerolog.Logger, dir, command string, args ...string) err cmd := exec.Command(command, args...) cmd.Dir = dir + cmd.Env = os.Environ() output, err := cmd.CombinedOutput() if err != nil { diff --git a/cmd/generate-bindings/generate-bindings.go b/cmd/generate-bindings/generate-bindings.go index 63691e80..f7cb0219 100644 --- a/cmd/generate-bindings/generate-bindings.go +++ b/cmd/generate-bindings/generate-bindings.go @@ -15,6 +15,7 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/generate-bindings/bindings" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/ui" "github.com/smartcontractkit/cre-cli/internal/validation" ) @@ -65,6 +66,7 @@ For JSON files the ABI is read from the top-level "abi" field.`, generateBindingsCmd.Flags().StringP("abi", "a", "", "Path to ABI directory (defaults to contracts/{chain-family}/src/abi/). Supports *.abi and *.json files") generateBindingsCmd.Flags().StringP("pkg", "k", "bindings", "Base package name (each contract gets its own subdirectory)") + runtimeattach.Register(generateBindingsCmd, runtimeattach.Empty) return generateBindingsCmd } diff --git a/cmd/login/login.go b/cmd/login/login.go index 01c271a3..592a9fa6 100644 --- a/cmd/login/login.go +++ b/cmd/login/login.go @@ -19,6 +19,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/oauth" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/tenantctx" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -42,6 +43,7 @@ func New(runtimeCtx *runtime.Context) *cobra.Command { }, } + runtimeattach.Register(cmd, runtimeattach.Empty) return cmd } diff --git a/cmd/logout/logout.go b/cmd/logout/logout.go index ee0f86f1..47002fcc 100644 --- a/cmd/logout/logout.go +++ b/cmd/logout/logout.go @@ -14,6 +14,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/tenantctx" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -33,6 +34,7 @@ func New(runtimeCtx *runtime.Context) *cobra.Command { return h.execute() }, } + runtimeattach.Register(cmd, runtimeattach.Empty) return cmd } diff --git a/cmd/registry/list/list.go b/cmd/registry/list/list.go index 1ea5247d..3338c109 100644 --- a/cmd/registry/list/list.go +++ b/cmd/registry/list/list.go @@ -6,11 +6,12 @@ import ( "github.com/spf13/cobra" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/ui" ) func New(runtimeContext *runtime.Context) *cobra.Command { - return &cobra.Command{ + cmd := &cobra.Command{ Use: "list", Short: "Lists available workflow registries for the current environment", Long: `Displays the registries configured for your organization, including type and address.`, @@ -44,4 +45,6 @@ func New(runtimeContext *runtime.Context) *cobra.Command { return nil }, } + runtimeattach.Register(cmd, runtimeattach.CredsAndTenant) + return cmd } diff --git a/cmd/registry/registry.go b/cmd/registry/registry.go index 440df4fe..34befca5 100644 --- a/cmd/registry/registry.go +++ b/cmd/registry/registry.go @@ -5,6 +5,7 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/registry/list" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" ) func New(runtimeContext *runtime.Context) *cobra.Command { @@ -16,5 +17,6 @@ func New(runtimeContext *runtime.Context) *cobra.Command { registryCmd.AddCommand(list.New(runtimeContext)) + runtimeattach.Register(registryCmd, runtimeattach.Empty) return registryCmd } diff --git a/cmd/root.go b/cmd/root.go index 05dce7bb..8bed7bb4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,10 +27,11 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/whoami" "github.com/smartcontractkit/cre-cli/cmd/workflow" "github.com/smartcontractkit/cre-cli/internal/constants" - "github.com/smartcontractkit/cre-cli/internal/context" "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/logger" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" + "github.com/smartcontractkit/cre-cli/internal/runtimespec" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/telemetry" "github.com/smartcontractkit/cre-cli/internal/ui" @@ -40,11 +41,10 @@ import ( //go:embed template/help_template.tpl var helpTemplate string -// errLoginCompleted is a sentinel error returned from PersistentPreRunE when -// the auto-login flow completes successfully. Returning an error (instead of -// calling os.Exit) lets Execute() emit telemetry and exit cleanly with code 0, -// while still preventing Cobra from running the original command's RunE. -var errLoginCompleted = errors.New("login completed successfully; please re-run your command") +// errLoginCompleted aliases runtime.ErrLoginCompleted for backwards-compatible +// comparison in this package. Pre-run (runtimespec.Apply) can return the same +// sentinel after an interactive login during credential setup. +var errLoginCompleted = runtime.ErrLoginCompleted var ( // RootCmd represents the base command when called without any subcommands @@ -161,74 +161,23 @@ func newRootCommand() *cobra.Command { return fmt.Errorf("failed to load environment details: %w", err) } - if isLoadCredentials(cmd) { + preRunSpec := runtimeattach.SpecForCommand(cmd) + if !preRunSpec.IsEmpty() { if showSpinner { - spinner.Update("Validating credentials...") + if preRunSpec.Credentials { + spinner.Update("Validating credentials...") + } else if preRunSpec.NeedsSettingsLoad() { + spinner.Update("Loading settings...") + } } - skipValidation := shouldSkipValidation(cmd) - err := runtimeContext.AttachCredentials(cmd.Context(), skipValidation) - if err != nil { + if err := runtimespec.Apply(cmd.Context(), runtimeContext, cmd, args, preRunSpec); err != nil { if showSpinner { spinner.Stop() } - - if errors.Is(err, runtime.ErrValidationFailed) { - // Credentials exist but validation failed (likely network). - // Do NOT prompt for re-login -- that causes an infinite loop. - ui.Line() - if runtimeContext.EnvironmentSet != nil && runtimeContext.EnvironmentSet.RequiresVPN() { - ui.ErrorWithSuggestions("Credential validation failed", []string{ - fmt.Sprintf("The %s environment requires Tailscale VPN.", runtimeContext.EnvironmentSet.EnvName), - "Ensure Tailscale is connected to the smartcontract.com network, then retry.", - }) - } else { - ui.Error("Credential validation failed") - } - ui.EnvContext(runtimeContext.EnvironmentSet.EnvLabel()) - ui.Line() - return fmt.Errorf("authentication required: %w", err) - } - - // No credentials on disk -- prompt user to login - ui.Line() - ui.Warning("You are not logged in") - ui.EnvContext(runtimeContext.EnvironmentSet.EnvLabel()) - ui.Line() - - runLogin, formErr := ui.Confirm("Would you like to login now?", - ui.WithLabels("Yes, login", "No, cancel"), - ) - if formErr != nil { - return fmt.Errorf("authentication required: %w", err) - } - - if !runLogin { - return fmt.Errorf("authentication required: %w", err) - } - - // Run login flow - ui.Line() - if loginErr := login.Run(runtimeContext); loginErr != nil { - return fmt.Errorf("login failed: %w", loginErr) - } - - // Signal Execute() to exit cleanly (code 0) without running - // the original command. The user needs to re-run their command - // now that credentials are available. - return errLoginCompleted - } - - // Ensure user context exists (fetches via GQL if missing, supports API key and bearer) - if showSpinner { - spinner.Update("Loading user context...") - } - if err := runtimeContext.AttachTenantContext(cmd.Context()); err != nil { - runtimeContext.Logger.Warn().Err(err).Msg("failed to load user context — context.yaml not available") + return err } - // Check if organization is ungated for commands that require it - cmdPath := cmd.CommandPath() - if cmdPath == "cre account link-key" { + if preRunSpec.Credentials && cmd.CommandPath() == "cre account link-key" { if err := runtimeContext.Credentials.CheckIsUngatedOrganization(); err != nil { if showSpinner { spinner.Stop() @@ -238,50 +187,6 @@ func newRootCommand() *cobra.Command { } } - // load settings from yaml files - if isLoadSettings(cmd) { - if showSpinner { - spinner.Update("Loading settings...") - } - // Capture the invocation directory before SetExecutionContext changes it. - if invocationDir, err := os.Getwd(); err == nil { - runtimeContext.InvocationDir = invocationDir - } - - // Set execution context (project root + workflow directory if applicable) - projectRootFlag := runtimeContext.Viper.GetString(settings.Flags.ProjectRoot.Name) - if err := context.SetExecutionContext(cmd, args, projectRootFlag, rootLogger); err != nil { - if showSpinner { - spinner.Stop() - } - return err - } - - // Stop spinner before AttachSettings — it may prompt for target selection - if showSpinner { - spinner.Stop() - } - - err := runtimeContext.AttachSettings(cmd, isLoadDeploymentRPC(cmd)) - if err != nil { - return fmt.Errorf("%w", err) - } - - if err := runtimeContext.AttachResolvedRegistry(); err != nil { - return err - } - - if err := runtimeContext.FinalizeDeferredWorkflowOwner(cmd); err != nil { - return err - } - - // Restart spinner for remaining initialization - if showSpinner { - spinner = ui.NewSpinner() - spinner.Start("Loading settings...") - } - } - // Stop the initialization spinner - commands can start their own if needed if showSpinner { spinner.Stop() @@ -445,97 +350,9 @@ func newRootCommand() *cobra.Command { templatesCmd, ) - return rootCmd -} - -func isLoadSettings(cmd *cobra.Command) bool { - // It is not expected to have the settings file when running the following commands - var excludedCommands = map[string]struct{}{ - "cre version": {}, - "cre login": {}, - "cre logout": {}, - "cre whoami": {}, - "cre account access": {}, - "cre account list-key": {}, - "cre init": {}, - "cre generate-bindings": {}, - "cre completion bash": {}, - "cre completion fish": {}, - "cre completion powershell": {}, - "cre completion zsh": {}, - "cre help": {}, - "cre update": {}, - "cre workflow": {}, - "cre workflow custom-build": {}, - "cre workflow limits": {}, - "cre workflow limits export": {}, - "cre workflow build": {}, - "cre account": {}, - "cre secrets": {}, - "cre templates": {}, - "cre templates list": {}, - "cre templates add": {}, - "cre templates remove": {}, - "cre registry": {}, - "cre registry list": {}, - "cre": {}, - } - - _, exists := excludedCommands[cmd.CommandPath()] - return !exists -} + runtimeattach.Register(rootCmd, runtimeattach.Empty) -func isLoadCredentials(cmd *cobra.Command) bool { - // It is not expected to have the credentials loaded when running the following commands - var excludedCommands = map[string]struct{}{ - "cre version": {}, - "cre login": {}, - "cre logout": {}, - "cre completion bash": {}, - "cre completion fish": {}, - "cre completion powershell": {}, - "cre completion zsh": {}, - "cre help": {}, - "cre generate-bindings": {}, - "cre update": {}, - "cre workflow": {}, - "cre workflow limits": {}, - "cre workflow limits export": {}, - "cre account": {}, - "cre secrets": {}, - "cre workflow build": {}, - "cre workflow hash": {}, - "cre templates": {}, - "cre templates list": {}, - "cre templates add": {}, - "cre templates remove": {}, - "cre": {}, - } - - _, exists := excludedCommands[cmd.CommandPath()] - return !exists -} - -func isLoadDeploymentRPC(cmd *cobra.Command) bool { - var includedCommands = map[string]struct{}{ - "cre workflow deploy": {}, - "cre workflow pause": {}, - "cre workflow activate": {}, - "cre workflow delete": {}, - "cre account link-key": {}, - "cre account unlink-key": {}, - } - _, exists := includedCommands[cmd.CommandPath()] - return exists -} - -func shouldSkipValidation(cmd *cobra.Command) bool { - var excludedCommands = map[string]struct{}{ - "cre logout": {}, - } - - _, exists := excludedCommands[cmd.CommandPath()] - return exists + return rootCmd } func shouldCheckForUpdates(cmd *cobra.Command) bool { diff --git a/cmd/secrets/create/create.go b/cmd/secrets/create/create.go index ddfd4f45..97a56d08 100644 --- a/cmd/secrets/create/create.go +++ b/cmd/secrets/create/create.go @@ -11,6 +11,7 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/secrets/common" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/settings" ) @@ -68,5 +69,6 @@ func New(ctx *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) + runtimeattach.Register(cmd, runtimeattach.Full) return cmd } diff --git a/cmd/secrets/delete/delete.go b/cmd/secrets/delete/delete.go index d7a630d9..dc6a89dd 100644 --- a/cmd/secrets/delete/delete.go +++ b/cmd/secrets/delete/delete.go @@ -26,6 +26,7 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/secrets/common" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/types" "github.com/smartcontractkit/cre-cli/internal/ui" @@ -101,6 +102,7 @@ func New(ctx *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) + runtimeattach.Register(cmd, runtimeattach.Full) return cmd } diff --git a/cmd/secrets/execute/execute.go b/cmd/secrets/execute/execute.go index 4525efb0..d0d1e9f8 100644 --- a/cmd/secrets/execute/execute.go +++ b/cmd/secrets/execute/execute.go @@ -14,6 +14,7 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/secrets/common" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/settings" ) @@ -95,5 +96,6 @@ func New(ctx *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(cmd) + runtimeattach.Register(cmd, runtimeattach.Full) return cmd } diff --git a/cmd/secrets/list/list.go b/cmd/secrets/list/list.go index 48596c3e..b804eb43 100644 --- a/cmd/secrets/list/list.go +++ b/cmd/secrets/list/list.go @@ -23,6 +23,7 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/secrets/common" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/types" "github.com/smartcontractkit/cre-cli/internal/ui" @@ -74,6 +75,7 @@ func New(ctx *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) + runtimeattach.Register(cmd, runtimeattach.Full) return cmd } diff --git a/cmd/secrets/secrets.go b/cmd/secrets/secrets.go index 836e5f84..e2b15b3f 100644 --- a/cmd/secrets/secrets.go +++ b/cmd/secrets/secrets.go @@ -12,6 +12,7 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/secrets/update" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" ) func New(runtimeContext *runtime.Context) *cobra.Command { @@ -41,5 +42,6 @@ func New(runtimeContext *runtime.Context) *cobra.Command { secretsCmd.AddCommand(list.New(runtimeContext)) secretsCmd.AddCommand(execute.New(runtimeContext)) + runtimeattach.Register(secretsCmd, runtimeattach.Empty) return secretsCmd } diff --git a/cmd/secrets/update/update.go b/cmd/secrets/update/update.go index 95019d27..a52d4a31 100644 --- a/cmd/secrets/update/update.go +++ b/cmd/secrets/update/update.go @@ -11,6 +11,7 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/secrets/common" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/settings" ) @@ -70,5 +71,6 @@ func New(ctx *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) + runtimeattach.Register(cmd, runtimeattach.Full) return cmd } diff --git a/cmd/templates/add/add.go b/cmd/templates/add/add.go index f531a6c6..5cb75caa 100644 --- a/cmd/templates/add/add.go +++ b/cmd/templates/add/add.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/templateconfig" "github.com/smartcontractkit/cre-cli/internal/templaterepo" "github.com/smartcontractkit/cre-cli/internal/ui" @@ -17,7 +18,7 @@ type handler struct { } func New(runtimeContext *runtime.Context) *cobra.Command { - return &cobra.Command{ + cmd := &cobra.Command{ Use: "add ...", Short: "Adds a template repository source", Long: `Adds one or more template repository sources to ~/.cre/template.yaml. These repositories are used by cre init to discover available templates.`, @@ -28,6 +29,8 @@ func New(runtimeContext *runtime.Context) *cobra.Command { return h.Execute(args) }, } + runtimeattach.Register(cmd, runtimeattach.Empty) + return cmd } func (h *handler) Execute(repos []string) error { diff --git a/cmd/templates/list/list.go b/cmd/templates/list/list.go index 8ddb6d7e..5faa548b 100644 --- a/cmd/templates/list/list.go +++ b/cmd/templates/list/list.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/viper" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/templateconfig" "github.com/smartcontractkit/cre-cli/internal/templaterepo" "github.com/smartcontractkit/cre-cli/internal/ui" @@ -51,6 +52,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { cmd.Flags().Bool("refresh", false, "Bypass cache and fetch fresh data") cmd.Flags().Bool("json", false, "Output template list as JSON") + runtimeattach.Register(cmd, runtimeattach.Empty) return cmd } diff --git a/cmd/templates/remove/remove.go b/cmd/templates/remove/remove.go index a8b36787..90ee34bc 100644 --- a/cmd/templates/remove/remove.go +++ b/cmd/templates/remove/remove.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/templateconfig" "github.com/smartcontractkit/cre-cli/internal/templaterepo" "github.com/smartcontractkit/cre-cli/internal/ui" @@ -17,7 +18,7 @@ type handler struct { } func New(runtimeContext *runtime.Context) *cobra.Command { - return &cobra.Command{ + cmd := &cobra.Command{ Use: "remove ...", Short: "Removes a template repository source", Long: `Removes one or more template repository sources from ~/.cre/template.yaml. The ref portion is optional and ignored during matching.`, @@ -28,6 +29,8 @@ func New(runtimeContext *runtime.Context) *cobra.Command { return h.Execute(args) }, } + runtimeattach.Register(cmd, runtimeattach.Empty) + return cmd } func (h *handler) Execute(repos []string) error { diff --git a/cmd/templates/templates.go b/cmd/templates/templates.go index e5148766..b1d5098b 100644 --- a/cmd/templates/templates.go +++ b/cmd/templates/templates.go @@ -7,6 +7,7 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/templates/list" "github.com/smartcontractkit/cre-cli/cmd/templates/remove" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" ) func New(runtimeContext *runtime.Context) *cobra.Command { @@ -25,5 +26,6 @@ To scaffold a new project from a template, use: cre init`, templatesCmd.AddCommand(add.New(runtimeContext)) templatesCmd.AddCommand(remove.New(runtimeContext)) + runtimeattach.Register(templatesCmd, runtimeattach.Empty) return templatesCmd } diff --git a/cmd/update/update.go b/cmd/update/update.go index c846a518..e2ef27f7 100644 --- a/cmd/update/update.go +++ b/cmd/update/update.go @@ -21,6 +21,7 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/version" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -401,5 +402,6 @@ func New(_ *runtime.Context) *cobra.Command { // <-- No longer uses rt }, } + runtimeattach.Register(versionCmd, runtimeattach.Empty) return versionCmd } diff --git a/cmd/version/version.go b/cmd/version/version.go index f1d0d727..3a9a0d47 100644 --- a/cmd/version/version.go +++ b/cmd/version/version.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -21,5 +22,6 @@ func New(runtimeContext *runtime.Context) *cobra.Command { }, } + runtimeattach.Register(versionCmd, runtimeattach.Empty) return versionCmd } diff --git a/cmd/whoami/whoami.go b/cmd/whoami/whoami.go index 01051ead..f01a437d 100644 --- a/cmd/whoami/whoami.go +++ b/cmd/whoami/whoami.go @@ -12,6 +12,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -26,6 +27,7 @@ func New(runtimeCtx *runtime.Context) *cobra.Command { return h.Execute(cmd.Context()) }, } + runtimeattach.Register(cmd, runtimeattach.CredsAndTenant) return cmd } diff --git a/cmd/workflow/activate/activate.go b/cmd/workflow/activate/activate.go index 039b3f67..5e962fa2 100644 --- a/cmd/workflow/activate/activate.go +++ b/cmd/workflow/activate/activate.go @@ -10,6 +10,7 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/client" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/ui" "github.com/smartcontractkit/cre-cli/internal/validation" @@ -51,6 +52,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(activateCmd) settings.AddSkipConfirmation(activateCmd) + runtimeattach.Register(activateCmd, runtimeattach.FullWithDeploymentRPC) return activateCmd } diff --git a/cmd/workflow/build/build.go b/cmd/workflow/build/build.go index f92f6973..2d8f68af 100644 --- a/cmd/workflow/build/build.go +++ b/cmd/workflow/build/build.go @@ -10,6 +10,7 @@ import ( cmdcommon "github.com/smartcontractkit/cre-cli/cmd/common" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -31,6 +32,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { } buildCmd.Flags().StringP("output", "o", "", "Output file path for the compiled WASM binary (default: /binary.wasm)") buildCmd.Flags().Bool(cmdcommon.SkipTypeChecksCLIFlag, false, "Skip TypeScript project typecheck during compilation (passes "+cmdcommon.SkipTypeChecksFlag+" to cre-compile)") + runtimeattach.Register(buildCmd, runtimeattach.Empty) return buildCmd } diff --git a/cmd/workflow/convert/convert.go b/cmd/workflow/convert/convert.go index e1dba583..3c4cd22b 100644 --- a/cmd/workflow/convert/convert.go +++ b/cmd/workflow/convert/convert.go @@ -12,6 +12,7 @@ import ( cmdcommon "github.com/smartcontractkit/cre-cli/cmd/common" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/transformation" "github.com/smartcontractkit/cre-cli/internal/ui" @@ -45,6 +46,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { }, } convertCmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation prompt and convert immediately") + runtimeattach.Register(convertCmd, runtimeattach.CredsAndTenant) return convertCmd } diff --git a/cmd/workflow/delete/delete.go b/cmd/workflow/delete/delete.go index 50ab39cc..5db356d0 100644 --- a/cmd/workflow/delete/delete.go +++ b/cmd/workflow/delete/delete.go @@ -12,6 +12,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/ui" "github.com/smartcontractkit/cre-cli/internal/validation" @@ -49,6 +50,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(deleteCmd) settings.AddSkipConfirmation(deleteCmd) + runtimeattach.Register(deleteCmd, runtimeattach.FullWithDeploymentRPC) return deleteCmd } diff --git a/cmd/workflow/deploy/deploy.go b/cmd/workflow/deploy/deploy.go index c243faae..b2070c16 100644 --- a/cmd/workflow/deploy/deploy.go +++ b/cmd/workflow/deploy/deploy.go @@ -16,6 +16,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/ui" "github.com/smartcontractkit/cre-cli/internal/validation" @@ -109,6 +110,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { deployCmd.Flags().Bool(cmdcommon.SkipTypeChecksCLIFlag, false, "Skip TypeScript project typecheck during compilation (passes "+cmdcommon.SkipTypeChecksFlag+" to cre-compile)") deployCmd.MarkFlagsMutuallyExclusive("config", "no-config", "default-config") + runtimeattach.Register(deployCmd, runtimeattach.FullWithDeploymentRPC) return deployCmd } diff --git a/cmd/workflow/hash/hash.go b/cmd/workflow/hash/hash.go index 99cc311c..c873c2f4 100644 --- a/cmd/workflow/hash/hash.go +++ b/cmd/workflow/hash/hash.go @@ -11,6 +11,7 @@ import ( cmdcommon "github.com/smartcontractkit/cre-cli/cmd/common" "github.com/smartcontractkit/cre-cli/internal/ethkeys" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -68,6 +69,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { hashCmd.MarkFlagsMutuallyExclusive("config", "no-config", "default-config") hashCmd.Flags().Bool(cmdcommon.SkipTypeChecksCLIFlag, false, "Skip TypeScript project typecheck during compilation (passes "+cmdcommon.SkipTypeChecksFlag+" to cre-compile)") + runtimeattach.Register(hashCmd, runtimeattach.ProjectSettingsNoCreds) return hashCmd } diff --git a/cmd/workflow/limits/export.go b/cmd/workflow/limits/export.go index 4a206857..d9378b06 100644 --- a/cmd/workflow/limits/export.go +++ b/cmd/workflow/limits/export.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" ) func New() *cobra.Command { @@ -15,8 +16,11 @@ func New() *cobra.Command { Long: `The limits command provides tools for managing workflow simulation limits.`, } - limitsCmd.AddCommand(newExportCmd()) + export := newExportCmd() + limitsCmd.AddCommand(export) + runtimeattach.Register(limitsCmd, runtimeattach.Empty) + runtimeattach.Register(export, runtimeattach.Empty) return limitsCmd } diff --git a/cmd/workflow/pause/pause.go b/cmd/workflow/pause/pause.go index 4507b351..e11b4824 100644 --- a/cmd/workflow/pause/pause.go +++ b/cmd/workflow/pause/pause.go @@ -10,6 +10,7 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/client" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/ui" "github.com/smartcontractkit/cre-cli/internal/validation" @@ -33,13 +34,11 @@ func New(runtimeContext *runtime.Context) *cobra.Command { Example: `cre workflow pause ./my-workflow`, RunE: func(cmd *cobra.Command, args []string) error { handler := newHandler(runtimeContext) - inputs, err := handler.ResolveInputs(runtimeContext.Viper) if err != nil { return err } handler.inputs = inputs - if err := handler.ValidateInputs(); err != nil { return err } @@ -49,6 +48,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(pauseCmd) settings.AddSkipConfirmation(pauseCmd) + runtimeattach.Register(pauseCmd, runtimeattach.FullWithDeploymentRPC) return pauseCmd } diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index cdd372b6..06230f12 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -41,6 +41,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/ui" "github.com/smartcontractkit/cre-cli/internal/validation" @@ -111,6 +112,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { simulateCmd.Flags().Int("evm-event-index", -1, "EVM trigger log index (0-based)") simulateCmd.Flags().String("limits", "default", "Production limits to enforce during simulation: 'default' for prod defaults, path to a limits JSON file (e.g. from 'cre workflow limits export'), or 'none' to disable") simulateCmd.Flags().Bool(cmdcommon.SkipTypeChecksCLIFlag, false, "Skip TypeScript project typecheck during compilation (passes "+cmdcommon.SkipTypeChecksFlag+" to cre-compile)") + runtimeattach.Register(simulateCmd, runtimeattach.Full) return simulateCmd } diff --git a/cmd/workflow/test/test.go b/cmd/workflow/test/test.go index c131e844..8dc1fa4a 100644 --- a/cmd/workflow/test/test.go +++ b/cmd/workflow/test/test.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/viper" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" "github.com/smartcontractkit/cre-cli/internal/validation" ) @@ -43,6 +44,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { testCmd.Flags().StringP("run", "r", "", "Runs only tests and examples matching provided regular expression") + runtimeattach.Register(testCmd, runtimeattach.Full) return testCmd } diff --git a/cmd/workflow/workflow.go b/cmd/workflow/workflow.go index f03a7bdf..d06cd29f 100644 --- a/cmd/workflow/workflow.go +++ b/cmd/workflow/workflow.go @@ -14,6 +14,7 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate" "github.com/smartcontractkit/cre-cli/cmd/workflow/test" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/runtimeattach" ) func New(runtimeContext *runtime.Context) *cobra.Command { @@ -34,5 +35,6 @@ func New(runtimeContext *runtime.Context) *cobra.Command { workflowCmd.AddCommand(simulate.New(runtimeContext)) workflowCmd.AddCommand(limits.New()) + runtimeattach.Register(workflowCmd, runtimeattach.Empty) return workflowCmd } diff --git a/internal/runtime/attach_config.go b/internal/runtime/attach_config.go new file mode 100644 index 00000000..5cb609bb --- /dev/null +++ b/internal/runtime/attach_config.go @@ -0,0 +1,53 @@ +package runtime + +// AttachConfig selects which runtime attach steps run inside ApplyAttachConfig. +// For steps that are not set, the corresponding work is skipped. Settings-related +// fields are OR'd: if any are true, project settings (settings.New) are loaded +// as a single step — loadWorkflowSettings/CLD are not split in this first iteration. +type AttachConfig struct { + Environment bool + Credentials bool + TenantContext bool + ExecutionContext bool + ConfigMerge bool + Target bool + WorkflowConfig bool + StorageConfig bool + CLDConfig bool + ValidateDeploymentRPC bool + ResolvedRegistry bool + // ResolveWorkflowOwner: when true with ResolvedRegistry, run settings.FinalizeWorkflowOwner + // at the end of AttachResolvedRegistry (same pre-run apply step). + ResolveWorkflowOwner bool + // SkipCredentialValidation passes through to AttachCredentials. + SkipCredentialValidation bool +} + +// NeedsSettingsLoad returns true when we must run settings.New (Viper + workflow YAML + user + storage + CLD). +func (a *AttachConfig) NeedsSettingsLoad() bool { + if a == nil { + return false + } + if a.ResolvedRegistry || a.ResolveWorkflowOwner { + return true + } + return a.ConfigMerge || a.Target || a.WorkflowConfig || a.StorageConfig || a.CLDConfig +} + +// IsEmpty reports whether Apply would perform no work (all attach flags off). +// Environment is always loaded by root; this type does not track that. +func (a *AttachConfig) IsEmpty() bool { + if a == nil { + return true + } + if a.Credentials || a.TenantContext || a.ExecutionContext { + return false + } + if a.NeedsSettingsLoad() { + return false + } + if a.ValidateDeploymentRPC { + return false + } + return true +} diff --git a/internal/runtime/runtime_context.go b/internal/runtime/runtime_context.go index 255b7674..45c21bfb 100644 --- a/internal/runtime/runtime_context.go +++ b/internal/runtime/runtime_context.go @@ -72,22 +72,6 @@ func (ctx *Context) AttachSettings(cmd *cobra.Command, validateDeployRPC bool) e return nil } -// FinalizeDeferredWorkflowOwner fills workflow owner when settings load deferred it -// (non-empty deployment-registry). Call after AttachResolvedRegistry. -func (ctx *Context) FinalizeDeferredWorkflowOwner(cmd *cobra.Command) error { - if ctx.Settings == nil { - return nil - } - return settings.FinalizeWorkflowOwner( - ctx.Viper, - cmd, - &ctx.Settings.Workflow, - ctx.Settings.User.TargetName, - ctx.ResolvedRegistry, - ctx.DerivedWorkflowOwner, - ) -} - func (ctx *Context) AttachCredentials(validationCtx context.Context, skipValidation bool) error { var err error @@ -148,7 +132,10 @@ func (ctx *Context) AttachTenantContext(validationCtx context.Context) error { // AttachResolvedRegistry resolves the deployment-registry from workflow // settings against the tenant context registries. Must be called after // AttachSettings and AttachTenantContext. -func (ctx *Context) AttachResolvedRegistry() error { +// When finalizeWorkflowOwner is true, it also fills workflow owner when settings +// load deferred it (non-empty deployment-registry); that requires resolved +// registry and (for off-chain) derived owner from credentials. +func (ctx *Context) AttachResolvedRegistry(cmd *cobra.Command, finalizeWorkflowOwner bool) error { deploymentRegistry := "" if ctx.Settings != nil { deploymentRegistry = ctx.Settings.Workflow.UserWorkflowSettings.DeploymentRegistry @@ -160,7 +147,18 @@ func (ctx *Context) AttachResolvedRegistry() error { } ctx.ResolvedRegistry = resolved - return nil + + if !finalizeWorkflowOwner || ctx.Settings == nil { + return nil + } + return settings.FinalizeWorkflowOwner( + ctx.Viper, + cmd, + &ctx.Settings.Workflow, + ctx.Settings.User.TargetName, + ctx.ResolvedRegistry, + ctx.DerivedWorkflowOwner, + ) } func (ctx *Context) AttachEnvironmentSet() error { diff --git a/internal/runtime/sentinel.go b/internal/runtime/sentinel.go new file mode 100644 index 00000000..a958e4b3 --- /dev/null +++ b/internal/runtime/sentinel.go @@ -0,0 +1,7 @@ +package runtime + +import "errors" + +// ErrLoginCompleted is returned when the user finished logging in and must re-run +// the same command. Matches the legacy root PersistentPreRunE login-completed path. +var ErrLoginCompleted = errors.New("login completed successfully; please re-run your command") diff --git a/internal/runtimeattach/presets.go b/internal/runtimeattach/presets.go new file mode 100644 index 00000000..8277e39c --- /dev/null +++ b/internal/runtimeattach/presets.go @@ -0,0 +1,57 @@ +package runtimeattach + +import creruntime "github.com/smartcontractkit/cre-cli/internal/runtime" + +// Shared, read-only attach presets. Commands register a pointer; multiple +// commands can share the same pointer when their attach behavior is identical. + +// Empty is a no-op in Apply. +var Empty = &creruntime.AttachConfig{} + +// CredsAndTenant loads only credentials and tenant context (e.g. whoami, registry list without project settings). +var CredsAndTenant = &creruntime.AttachConfig{ + Credentials: true, + TenantContext: true, +} + +// ProjectSettingsNoCreds loads project path, Viper settings, and registry/owner +// without loading cloud credentials (offline hash). +var ProjectSettingsNoCreds = &creruntime.AttachConfig{ + ExecutionContext: true, + ConfigMerge: true, + Target: true, + WorkflowConfig: true, + StorageConfig: true, + CLDConfig: true, + ResolvedRegistry: true, + ResolveWorkflowOwner: true, +} + +// Full loads credentials and full project settings (default for workflow deploy paths that do not validate deployment RPC). +var Full = &creruntime.AttachConfig{ + Credentials: true, + TenantContext: true, + ExecutionContext: true, + ConfigMerge: true, + Target: true, + WorkflowConfig: true, + StorageConfig: true, + CLDConfig: true, + ResolvedRegistry: true, + ResolveWorkflowOwner: true, +} + +// FullWithDeploymentRPC is Full plus registry-chain RPC validation for on-chain registries. +var FullWithDeploymentRPC = &creruntime.AttachConfig{ + Credentials: true, + TenantContext: true, + ExecutionContext: true, + ConfigMerge: true, + Target: true, + WorkflowConfig: true, + StorageConfig: true, + CLDConfig: true, + ResolvedRegistry: true, + ResolveWorkflowOwner: true, + ValidateDeploymentRPC: true, +} diff --git a/internal/runtimeattach/register.go b/internal/runtimeattach/register.go new file mode 100644 index 00000000..7c759e04 --- /dev/null +++ b/internal/runtimeattach/register.go @@ -0,0 +1,49 @@ +// Package runtimeattach maps each Cobra command to a runtime.Context attach +// spec (which credentials, settings, and registry/owner resolution to load). +// Register from each command’s New() so the attach plan lives next to the +// command, not in a global path table. +package runtimeattach + +import ( + "sync" + + "github.com/spf13/cobra" + + creruntime "github.com/smartcontractkit/cre-cli/internal/runtime" +) + +// Register sets the runtime attach spec for a specific Cobra command. Call +// from each command's New() (and for each subcommand) so the decision of what +// to attach is defined next to the command, not in a global path table. +// Nil spec is treated as empty (no attach work in Apply). +func Register(cmd *cobra.Command, spec *creruntime.AttachConfig) { + if cmd == nil { + return + } + if spec == nil { + spec = Empty + } + mu.Lock() + defer mu.Unlock() + registeredByCommand[cmd] = spec +} + +// SpecForCommand returns the attach spec for the Cobra command that is +// actually running (the leaf the user invoked). Commands that were not +// registered (e.g. shell completion) get an empty spec. +func SpecForCommand(cmd *cobra.Command) *creruntime.AttachConfig { + if cmd == nil { + return Empty + } + mu.RLock() + defer mu.RUnlock() + if s, ok := registeredByCommand[cmd]; ok { + return s + } + return Empty +} + +var ( + mu sync.RWMutex + registeredByCommand = make(map[*cobra.Command]*creruntime.AttachConfig) +) diff --git a/internal/runtimespec/apply.go b/internal/runtimespec/apply.go new file mode 100644 index 00000000..3f4ddf03 --- /dev/null +++ b/internal/runtimespec/apply.go @@ -0,0 +1,86 @@ +package runtimespec + +import ( + "context" + "fmt" + "os" + + "github.com/spf13/cobra" + + intcontext "github.com/smartcontractkit/cre-cli/internal/context" + "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/settings" +) + +// Apply runs the attach steps selected in spec in a fixed dependency order. +// The root command calls this from PersistentPreRunE for all paths; a nil or +// empty spec is a no-op. +func Apply(ctx context.Context, rt *runtime.Context, cmd *cobra.Command, args []string, spec *runtime.AttachConfig) error { + if spec == nil || spec.IsEmpty() { + return nil + } + + if spec.Environment && rt.EnvironmentSet == nil { + if err := rt.AttachEnvironmentSet(); err != nil { + return fmt.Errorf("load environment: %w", err) + } + } + + if spec.Credentials { + if spec.SkipCredentialValidation { + if err := rt.AttachCredentials(cmd.Context(), true); err != nil { + return err + } + } else { + if err := attachCredentialsInteractive(cmd, rt); err != nil { + return err + } + } + } + + if spec.TenantContext { + if err := rt.AttachTenantContext(ctx); err != nil { + rt.Logger.Warn().Err(err).Msg("failed to load user context — context.yaml not available") + } + } + + if spec.ExecutionContext { + if rt.InvocationDir == "" { + if invocationDir, err := os.Getwd(); err == nil { + rt.InvocationDir = invocationDir + } + } + projectRootFlag := rt.Viper.GetString(settings.Flags.ProjectRoot.Name) + if err := intcontext.SetExecutionContext(cmd, args, projectRootFlag, rt.Logger); err != nil { + return err + } + } + + if spec.NeedsSettingsLoad() { + // Defer ValidateDeploymentRPC inside load until after we know registry type; + // pass false so private/off-chain registries are not forced to define chain RPCs. + if err := rt.AttachSettings(cmd, false); err != nil { + return fmt.Errorf("load settings: %w", err) + } + } + + if spec.ResolveWorkflowOwner && !spec.ResolvedRegistry { + return fmt.Errorf("internal: ResolveWorkflowOwner requires ResolvedRegistry in attach spec") + } + + if spec.ResolvedRegistry { + if err := rt.AttachResolvedRegistry(cmd, spec.ResolveWorkflowOwner); err != nil { + return err + } + } + + if spec.ValidateDeploymentRPC && rt.ResolvedRegistry != nil && rt.Settings != nil && rt.EnvironmentSet != nil { + if rt.ResolvedRegistry.Type() == settings.RegistryTypeOnChain { + if err := settings.ValidateDeploymentRPC(&rt.Settings.Workflow, rt.EnvironmentSet.WorkflowRegistryChainName); err != nil { + return err + } + } + } + + return nil +} diff --git a/internal/runtimespec/credentials.go b/internal/runtimespec/credentials.go new file mode 100644 index 00000000..2eeef91e --- /dev/null +++ b/internal/runtimespec/credentials.go @@ -0,0 +1,67 @@ +package runtimespec + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" + + "github.com/smartcontractkit/cre-cli/cmd/login" + "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +// attachCredentialsInteractive loads credentials and, when needed, runs the +// same login prompt flow as root PersistentPreRunE. +func attachCredentialsInteractive(cmd *cobra.Command, rt *runtime.Context) error { + skipValidation := false // matches shouldSkipValidation for non-logout commands in root + err := rt.AttachCredentials(cmd.Context(), skipValidation) + if err != nil { + if errors.Is(err, runtime.ErrValidationFailed) { + ui.Line() + if rt.EnvironmentSet != nil && rt.EnvironmentSet.RequiresVPN() { + ui.ErrorWithSuggestions("Credential validation failed", []string{ + fmt.Sprintf("The %s environment requires Tailscale VPN.", rt.EnvironmentSet.EnvName), + "Ensure Tailscale is connected to the smartcontract.com network, then retry.", + }) + } else { + ui.Error("Credential validation failed") + } + if rt.EnvironmentSet != nil { + ui.EnvContext(rt.EnvironmentSet.EnvLabel()) + } + ui.Line() + return fmt.Errorf("authentication required: %w", err) + } + + if errors.Is(err, runtime.ErrNoCredentials) { + ui.Line() + ui.Warning("You are not logged in") + if rt.EnvironmentSet != nil { + ui.EnvContext(rt.EnvironmentSet.EnvLabel()) + } + ui.Line() + + runLogin, formErr := ui.Confirm("Would you like to login now?", + ui.WithLabels("Yes, login", "No, cancel"), + ) + if formErr != nil { + return fmt.Errorf("authentication required: %w", err) + } + + if !runLogin { + return fmt.Errorf("authentication required: %w", err) + } + + ui.Line() + if loginErr := login.Run(rt); loginErr != nil { + return fmt.Errorf("login failed: %w", loginErr) + } + return runtime.ErrLoginCompleted + } + + return err + } + + return nil +} diff --git a/internal/settings/workflow_settings.go b/internal/settings/workflow_settings.go index db29dbee..a8a51b3e 100644 --- a/internal/settings/workflow_settings.go +++ b/internal/settings/workflow_settings.go @@ -124,7 +124,8 @@ func loadWorkflowSettings(logger *zerolog.Logger, v *viper.Viper, cmd *cobra.Com deploymentRegistry := workflowSettings.UserWorkflowSettings.DeploymentRegistry // If deployment-registry is set, owner depends on how that id resolves; defer to - // FinalizeWorkflowOwner (after ResolveRegistry). Otherwise resolve from env/config now. + // FinalizeWorkflowOwner (called from runtime.Context.AttachResolvedRegistry after + // ResolveRegistry). Otherwise resolve from env/config now. if !ShouldSkipGetOwner(cmd) { if deploymentRegistry == "" { ownerAddress, ownerType, err := GetWorkflowOwner(v)