From 3035fac610068fd83a9ff78e561afc59205d2d32 Mon Sep 17 00:00:00 2001 From: "Optimus (AI Agent)" Date: Fri, 6 Mar 2026 07:07:39 +0000 Subject: [PATCH 1/4] fix: wait for deployment readiness before reporting success When deploying to non-local providers (e.g. kubernetes), the CLI now polls the deployment status until it reaches a terminal state (deployed, failed, or cancelled) instead of returning immediately. Add --no-wait flag to skip readiness polling and return immediately. Fixes #230 Co-Authored-By: Claude Opus 4.6 --- internal/cli/agent/deploy.go | 19 +++++++++++-- internal/cli/agent/deploy_test.go | 36 +++++++++++++++++++++++++ internal/cli/common/wait.go | 45 +++++++++++++++++++++++++++++++ internal/cli/common/wait_test.go | 13 +++++++++ internal/cli/mcp/deploy.go | 9 +++++++ 5 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 internal/cli/agent/deploy_test.go create mode 100644 internal/cli/common/wait.go create mode 100644 internal/cli/common/wait_test.go diff --git a/internal/cli/agent/deploy.go b/internal/cli/agent/deploy.go index 341b88bc..f1862168 100644 --- a/internal/cli/agent/deploy.go +++ b/internal/cli/agent/deploy.go @@ -5,6 +5,7 @@ import ( "os" "strings" + cliCommon "github.com/agentregistry-dev/agentregistry/internal/cli/common" "github.com/agentregistry-dev/agentregistry/pkg/models" "github.com/spf13/cobra" ) @@ -69,10 +70,12 @@ func runDeploy(cmd *cobra.Command, args []string) error { config["KAGENT_NAMESPACE"] = namespace } + noWait, _ := cmd.Flags().GetBool("no-wait") + if providerID == "local" { return deployLocal(name, version, config, providerID) } - return deployToProvider(name, version, config, namespace, providerID) + return deployToProvider(name, version, config, namespace, providerID, noWait) } // buildDeployConfig creates the configuration map with all necessary environment variables @@ -112,7 +115,7 @@ func deployLocal(name, version string, config map[string]string, providerID stri } // deployToProvider deploys an agent to a non-local provider. -func deployToProvider(name, version string, config map[string]string, namespace string, providerID string) error { +func deployToProvider(name, version string, config map[string]string, namespace string, providerID string, noWait bool) error { deployment, err := apiClient.DeployAgent(name, version, config, providerID) if err != nil { return fmt.Errorf("failed to deploy agent: %w", err) @@ -122,6 +125,17 @@ func deployToProvider(name, version string, config map[string]string, namespace if ns == "" { ns = "(default)" } + + if noWait { + fmt.Printf("Agent '%s' version '%s' submitted to providerId=%s in namespace '%s'\n", deployment.ServerName, deployment.Version, providerID, ns) + return nil + } + + fmt.Printf("Waiting for agent '%s' to become ready...\n", deployment.ServerName) + if err := cliCommon.WaitForDeploymentReady(apiClient, deployment.ID); err != nil { + return err + } + fmt.Printf("Agent '%s' version '%s' deployed to providerId=%s in namespace '%s'\n", deployment.ServerName, deployment.Version, providerID, ns) return nil } @@ -131,4 +145,5 @@ func init() { DeployCmd.Flags().String("provider-id", "", "Deployment target provider ID (defaults to local when omitted)") DeployCmd.Flags().Bool("prefer-remote", false, "Prefer using a remote source when available") DeployCmd.Flags().String("namespace", "", "Kubernetes namespace for agent deployment (defaults to current kubeconfig context)") + DeployCmd.Flags().Bool("no-wait", false, "Return immediately without waiting for the deployment to become ready") } diff --git a/internal/cli/agent/deploy_test.go b/internal/cli/agent/deploy_test.go new file mode 100644 index 00000000..b2b3aca3 --- /dev/null +++ b/internal/cli/agent/deploy_test.go @@ -0,0 +1,36 @@ +package agent + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDeployCmd_NoWaitFlag(t *testing.T) { + f := DeployCmd.Flags().Lookup("no-wait") + require.NotNil(t, f, "--no-wait flag should be registered") + assert.Equal(t, "false", f.DefValue) + assert.Equal(t, "bool", f.Value.Type()) +} + +func TestDeployCmd_Flags(t *testing.T) { + tests := []struct { + name string + flag string + wantType string + }{ + {"version", "version", "string"}, + {"provider-id", "provider-id", "string"}, + {"namespace", "namespace", "string"}, + {"prefer-remote", "prefer-remote", "bool"}, + {"no-wait", "no-wait", "bool"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := DeployCmd.Flags().Lookup(tt.flag) + require.NotNil(t, f, "--%s flag should be registered", tt.flag) + assert.Equal(t, tt.wantType, f.Value.Type()) + }) + } +} diff --git a/internal/cli/common/wait.go b/internal/cli/common/wait.go new file mode 100644 index 00000000..c4b4a867 --- /dev/null +++ b/internal/cli/common/wait.go @@ -0,0 +1,45 @@ +package common + +import ( + "fmt" + "time" + + "github.com/agentregistry-dev/agentregistry/internal/client" +) + +const ( + defaultWaitTimeout = 5 * time.Minute + defaultPollInterval = 2 * time.Second +) + +// WaitForDeploymentReady polls a deployment until it reaches a terminal state +// (deployed or failed). Returns an error if the deployment fails or the timeout +// is exceeded. +func WaitForDeploymentReady(c *client.Client, deploymentID string) error { + deadline := time.Now().Add(defaultWaitTimeout) + + for { + dep, err := c.GetDeploymentByID(deploymentID) + if err != nil { + return fmt.Errorf("polling deployment status: %w", err) + } + if dep == nil { + return fmt.Errorf("deployment %s not found", deploymentID) + } + + switch dep.Status { + case "deployed": + return nil + case "failed": + return fmt.Errorf("deployment failed") + case "cancelled": + return fmt.Errorf("deployment was cancelled") + } + + if time.Now().After(deadline) { + return fmt.Errorf("timed out waiting for deployment to become ready (current status: %s)", dep.Status) + } + + time.Sleep(defaultPollInterval) + } +} diff --git a/internal/cli/common/wait_test.go b/internal/cli/common/wait_test.go new file mode 100644 index 00000000..bd85993a --- /dev/null +++ b/internal/cli/common/wait_test.go @@ -0,0 +1,13 @@ +package common + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestWaitConstants(t *testing.T) { + assert.Equal(t, 5*time.Minute, defaultWaitTimeout) + assert.Equal(t, 2*time.Second, defaultPollInterval) +} diff --git a/internal/cli/mcp/deploy.go b/internal/cli/mcp/deploy.go index 3b460635..51688e06 100644 --- a/internal/cli/mcp/deploy.go +++ b/internal/cli/mcp/deploy.go @@ -17,6 +17,7 @@ var ( deployYes bool deployProviderID string deployNamespace string + deployNoWait bool ) var DeployCmd = &cobra.Command{ @@ -38,6 +39,7 @@ func init() { DeployCmd.Flags().BoolVarP(&deployYes, "yes", "y", false, "Automatically accept all prompts (use default/latest version)") DeployCmd.Flags().StringVar(&deployProviderID, "provider-id", "", "Deployment target provider ID (defaults to local when omitted)") DeployCmd.Flags().StringVar(&deployNamespace, "namespace", "", "Kubernetes namespace for deployment (if provider targets Kubernetes)") + DeployCmd.Flags().BoolVar(&deployNoWait, "no-wait", false, "Return immediately without waiting for the deployment to become ready") } func runDeploy(cmd *cobra.Command, args []string) error { @@ -93,6 +95,13 @@ func runDeploy(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to deploy server: %w", err) } + if deployProviderID != "local" && !deployNoWait { + fmt.Printf("Waiting for server '%s' to become ready...\n", deployment.ServerName) + if err := common.WaitForDeploymentReady(apiClient, deployment.ID); err != nil { + return err + } + } + fmt.Printf("\n✓ Deployed %s (%s) with providerId=%s\n", deployment.ServerName, common.FormatVersionForDisplay(deployment.Version), deployProviderID) if deployNamespace != "" { ns := deployNamespace From adf58ebc9fe324d4605d01560374ed33c8bc3f3d Mon Sep 17 00:00:00 2001 From: "Optimus (AI Agent)" Date: Fri, 6 Mar 2026 21:41:58 +0000 Subject: [PATCH 2/4] remove trivial constant test per review feedback Co-Authored-By: Claude Opus 4.6 --- internal/cli/common/wait_test.go | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 internal/cli/common/wait_test.go diff --git a/internal/cli/common/wait_test.go b/internal/cli/common/wait_test.go deleted file mode 100644 index bd85993a..00000000 --- a/internal/cli/common/wait_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package common - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestWaitConstants(t *testing.T) { - assert.Equal(t, 5*time.Minute, defaultWaitTimeout) - assert.Equal(t, 2*time.Second, defaultPollInterval) -} From 05010bd8385e0d65d0cbe87389f45cb0ddcfa4d0 Mon Sep 17 00:00:00 2001 From: "Optimus (AI Agent)" Date: Fri, 6 Mar 2026 22:03:04 +0000 Subject: [PATCH 3/4] fix: rename --no-wait to --wait (default true), remove trivial test Address review feedback: - Rename --no-wait flag to --wait with default true per reviewer suggestion - Remove duplicated success message by using single print after optional wait - Remove trivial TestWaitConstants that just asserts constant values Co-Authored-By: Claude Opus 4.6 --- internal/cli/agent/deploy.go | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/internal/cli/agent/deploy.go b/internal/cli/agent/deploy.go index f1862168..a30f0636 100644 --- a/internal/cli/agent/deploy.go +++ b/internal/cli/agent/deploy.go @@ -70,12 +70,12 @@ func runDeploy(cmd *cobra.Command, args []string) error { config["KAGENT_NAMESPACE"] = namespace } - noWait, _ := cmd.Flags().GetBool("no-wait") + wait, _ := cmd.Flags().GetBool("wait") if providerID == "local" { return deployLocal(name, version, config, providerID) } - return deployToProvider(name, version, config, namespace, providerID, noWait) + return deployToProvider(name, version, config, namespace, providerID, wait) } // buildDeployConfig creates the configuration map with all necessary environment variables @@ -115,7 +115,7 @@ func deployLocal(name, version string, config map[string]string, providerID stri } // deployToProvider deploys an agent to a non-local provider. -func deployToProvider(name, version string, config map[string]string, namespace string, providerID string, noWait bool) error { +func deployToProvider(name, version string, config map[string]string, namespace string, providerID string, wait bool) error { deployment, err := apiClient.DeployAgent(name, version, config, providerID) if err != nil { return fmt.Errorf("failed to deploy agent: %w", err) @@ -126,14 +126,11 @@ func deployToProvider(name, version string, config map[string]string, namespace ns = "(default)" } - if noWait { - fmt.Printf("Agent '%s' version '%s' submitted to providerId=%s in namespace '%s'\n", deployment.ServerName, deployment.Version, providerID, ns) - return nil - } - - fmt.Printf("Waiting for agent '%s' to become ready...\n", deployment.ServerName) - if err := cliCommon.WaitForDeploymentReady(apiClient, deployment.ID); err != nil { - return err + if wait { + fmt.Printf("Waiting for agent '%s' to become ready...\n", deployment.ServerName) + if err := cliCommon.WaitForDeploymentReady(apiClient, deployment.ID); err != nil { + return err + } } fmt.Printf("Agent '%s' version '%s' deployed to providerId=%s in namespace '%s'\n", deployment.ServerName, deployment.Version, providerID, ns) @@ -145,5 +142,5 @@ func init() { DeployCmd.Flags().String("provider-id", "", "Deployment target provider ID (defaults to local when omitted)") DeployCmd.Flags().Bool("prefer-remote", false, "Prefer using a remote source when available") DeployCmd.Flags().String("namespace", "", "Kubernetes namespace for agent deployment (defaults to current kubeconfig context)") - DeployCmd.Flags().Bool("no-wait", false, "Return immediately without waiting for the deployment to become ready") + DeployCmd.Flags().Bool("wait", true, "Wait for the deployment to become ready before returning") } From ff2a33f19f4c5dd078240e217c36625789671018 Mon Sep 17 00:00:00 2001 From: "Optimus (AI Agent)" Date: Sat, 7 Mar 2026 00:13:41 +0000 Subject: [PATCH 4/4] fix: update tests and MCP deploy to use --wait flag Update deploy_test.go to reference the renamed --wait flag (default true) instead of --no-wait. Also rename deployNoWait to deployWait in MCP deploy.go for consistency. Co-Authored-By: Claude Opus 4.6 --- internal/cli/agent/deploy_test.go | 10 +++++----- internal/cli/mcp/deploy.go | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/cli/agent/deploy_test.go b/internal/cli/agent/deploy_test.go index b2b3aca3..84e9b856 100644 --- a/internal/cli/agent/deploy_test.go +++ b/internal/cli/agent/deploy_test.go @@ -7,10 +7,10 @@ import ( "github.com/stretchr/testify/require" ) -func TestDeployCmd_NoWaitFlag(t *testing.T) { - f := DeployCmd.Flags().Lookup("no-wait") - require.NotNil(t, f, "--no-wait flag should be registered") - assert.Equal(t, "false", f.DefValue) +func TestDeployCmd_WaitFlag(t *testing.T) { + f := DeployCmd.Flags().Lookup("wait") + require.NotNil(t, f, "--wait flag should be registered") + assert.Equal(t, "true", f.DefValue) assert.Equal(t, "bool", f.Value.Type()) } @@ -24,7 +24,7 @@ func TestDeployCmd_Flags(t *testing.T) { {"provider-id", "provider-id", "string"}, {"namespace", "namespace", "string"}, {"prefer-remote", "prefer-remote", "bool"}, - {"no-wait", "no-wait", "bool"}, + {"wait", "wait", "bool"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/cli/mcp/deploy.go b/internal/cli/mcp/deploy.go index 51688e06..07eb73e5 100644 --- a/internal/cli/mcp/deploy.go +++ b/internal/cli/mcp/deploy.go @@ -17,7 +17,7 @@ var ( deployYes bool deployProviderID string deployNamespace string - deployNoWait bool + deployWait bool ) var DeployCmd = &cobra.Command{ @@ -39,7 +39,7 @@ func init() { DeployCmd.Flags().BoolVarP(&deployYes, "yes", "y", false, "Automatically accept all prompts (use default/latest version)") DeployCmd.Flags().StringVar(&deployProviderID, "provider-id", "", "Deployment target provider ID (defaults to local when omitted)") DeployCmd.Flags().StringVar(&deployNamespace, "namespace", "", "Kubernetes namespace for deployment (if provider targets Kubernetes)") - DeployCmd.Flags().BoolVar(&deployNoWait, "no-wait", false, "Return immediately without waiting for the deployment to become ready") + DeployCmd.Flags().BoolVar(&deployWait, "wait", true, "Wait for the deployment to become ready before returning") } func runDeploy(cmd *cobra.Command, args []string) error { @@ -95,7 +95,7 @@ func runDeploy(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to deploy server: %w", err) } - if deployProviderID != "local" && !deployNoWait { + if deployProviderID != "local" && deployWait { fmt.Printf("Waiting for server '%s' to become ready...\n", deployment.ServerName) if err := common.WaitForDeploymentReady(apiClient, deployment.ID); err != nil { return err