From 240fd9813b9d0d0f29129d688ba502ef9f63c111 Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Thu, 12 Feb 2026 15:52:30 -0800 Subject: [PATCH 1/4] fix e2e test --- temporalcloudcli/commands.namespace_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/temporalcloudcli/commands.namespace_test.go b/temporalcloudcli/commands.namespace_test.go index 8376e7b..6bb9913 100644 --- a/temporalcloudcli/commands.namespace_test.go +++ b/temporalcloudcli/commands.namespace_test.go @@ -13,6 +13,7 @@ import ( "go.temporal.io/api/temporalproto" "go.temporal.io/cloud-sdk/api/cloudservice/v1" namespace "go.temporal.io/cloud-sdk/api/namespace/v1" + resource "go.temporal.io/cloud-sdk/api/resource/v1" "google.golang.org/protobuf/encoding/protojson" ) @@ -215,6 +216,11 @@ func (s *SharedServerSuite) cleanupNamespaces() { pageToken = res.NextPageToken } for _, ns := range namespacesToClean { + // AIDEV-NOTE: Only delete namespaces that are in ACTIVE state (value 3). + // Namespaces in other states (DELETING, DELETED, etc.) cannot be deleted. + if ns.State != resource.ResourceState_RESOURCE_STATE_ACTIVE { + continue + } res, err := cloudClient.CloudService().DeleteNamespace(s.Context, &cloudservice.DeleteNamespaceRequest{ ResourceVersion: ns.ResourceVersion, Namespace: ns.Namespace, From 2bc323275e3c82ede076bc4b6eec5c8009356d25 Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Thu, 12 Feb 2026 15:52:40 -0800 Subject: [PATCH 2/4] fix .env loading --- Makefile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 088e81b..821f746 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,7 @@ # Load .env file if it exists (for local development) # In CI/CD, environment variables are provided by the environment -include .env -ifneq (,$(wildcard .env)) -export $(shell sed 's/=.*//' .env) -endif +export all: gen build test From d5053e7998e894667289f937be306b52ec785876 Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Thu, 12 Feb 2026 16:57:11 -0800 Subject: [PATCH 3/4] Fix error handling --- temporalcloudcli/commands_test.go | 5 ----- temporalcloudcli/common.go | 24 ++++++++++++++++++------ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/temporalcloudcli/commands_test.go b/temporalcloudcli/commands_test.go index 8e40585..d23d301 100644 --- a/temporalcloudcli/commands_test.go +++ b/temporalcloudcli/commands_test.go @@ -268,8 +268,3 @@ func (s *SharedServerSuite) pollAsyncOperation( } } } - -type mutationResult struct { - asyncOp *operation.AsyncOperation - ID string -} diff --git a/temporalcloudcli/common.go b/temporalcloudcli/common.go index 9c051d7..400ac1a 100644 --- a/temporalcloudcli/common.go +++ b/temporalcloudcli/common.go @@ -192,22 +192,34 @@ func pollAsyncOperation( }, printer.StructuredOptions{}) case operation.AsyncOperation_STATE_FAILED: progressString = fmt.Sprintf("[%s] Operation failed: %s\n", time.Now().Format("15:04:05"), asyncOp.FailureReason) - return cctx.Printer.PrintStructured(MutationResult{ + // Print the structured output first, then return error for proper exit code + if err := cctx.Printer.PrintStructured(MutationResult{ ID: id, AsyncOp: asyncOp, - }, printer.StructuredOptions{}) + }, printer.StructuredOptions{}); err != nil { + return err + } + return fmt.Errorf("async operation failed: %s", asyncOp.FailureReason) case operation.AsyncOperation_STATE_CANCELLED: progressString = fmt.Sprintf("[%s] Operation cancelled\n", time.Now().Format("15:04:05")) - return cctx.Printer.PrintStructured(MutationResult{ + // Print the structured output first, then return error for proper exit code + if err := cctx.Printer.PrintStructured(MutationResult{ ID: id, AsyncOp: asyncOp, - }, printer.StructuredOptions{}) + }, printer.StructuredOptions{}); err != nil { + return err + } + return fmt.Errorf("async operation cancelled") case operation.AsyncOperation_STATE_REJECTED: progressString = fmt.Sprintf("[%s] Operation rejected\n", time.Now().Format("15:04:05")) - return cctx.Printer.PrintStructured(MutationResult{ + // Print the structured output first, then return error for proper exit code + if err := cctx.Printer.PrintStructured(MutationResult{ ID: id, AsyncOp: asyncOp, - }, printer.StructuredOptions{}) + }, printer.StructuredOptions{}); err != nil { + return err + } + return fmt.Errorf("async operation rejected") default: progressString = fmt.Sprintf("[%s] Operation pending...\n", time.Now().Format("15:04:05")) } From c884b5bdca99b637eb939ab203064752ca37c96a Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Thu, 12 Feb 2026 17:11:40 -0800 Subject: [PATCH 4/4] refactor async polling --- temporalcloudcli/commands.namespace.go | 6 +-- .../commands.namespace.lifecycle.go | 2 +- .../commands.namespace.retention.go | 2 +- temporalcloudcli/commands_test.go | 50 ++++--------------- temporalcloudcli/common.go | 2 +- 5 files changed, 17 insertions(+), 45 deletions(-) diff --git a/temporalcloudcli/commands.namespace.go b/temporalcloudcli/commands.namespace.go index 7bc5930..a9e1ac1 100644 --- a/temporalcloudcli/commands.namespace.go +++ b/temporalcloudcli/commands.namespace.go @@ -91,7 +91,7 @@ func (c *CloudNamespaceEditCommand) run(cctx *CommandContext, _ []string) error } // Poll for completion - return pollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace) + return PollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace) } func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error { @@ -182,7 +182,7 @@ func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error } // Step 7: Poll for completion - return pollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace) + return PollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace) } func (c *CloudNamespaceDeleteCommand) run(cctx *CommandContext, _ []string) error { @@ -234,7 +234,7 @@ func (c *CloudNamespaceDeleteCommand) run(cctx *CommandContext, _ []string) erro } // Poll for completion - return pollAsyncOperation(cctx, cloudClient, asyncOp.Id, c.Namespace) + return PollAsyncOperation(cctx, cloudClient, asyncOp.Id, c.Namespace) } func (c *CloudNamespaceListCommand) run(cctx *CommandContext, _ []string) error { diff --git a/temporalcloudcli/commands.namespace.lifecycle.go b/temporalcloudcli/commands.namespace.lifecycle.go index 6e2898b..c5281b6 100644 --- a/temporalcloudcli/commands.namespace.lifecycle.go +++ b/temporalcloudcli/commands.namespace.lifecycle.go @@ -105,5 +105,5 @@ func (c *CloudNamespaceLifecycleSetCommand) run(cctx *CommandContext, _ []string } // Poll for completion - return pollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace) + return PollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace) } diff --git a/temporalcloudcli/commands.namespace.retention.go b/temporalcloudcli/commands.namespace.retention.go index 907a450..5dca245 100644 --- a/temporalcloudcli/commands.namespace.retention.go +++ b/temporalcloudcli/commands.namespace.retention.go @@ -66,7 +66,7 @@ func (c *CloudNamespaceRetentionSetCommand) run(cctx *CommandContext, _ []string } // Poll for completion - return pollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace) + return PollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace) } func (c *CloudNamespaceRetentionGetCommand) run(cctx *CommandContext, _ []string) error { diff --git a/temporalcloudcli/commands_test.go b/temporalcloudcli/commands_test.go index d23d301..c8d53b0 100644 --- a/temporalcloudcli/commands_test.go +++ b/temporalcloudcli/commands_test.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "fmt" + "io" "math/rand" "os" "regexp" @@ -17,8 +18,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/temporalio/cloud-cli/temporalcloudcli" - "go.temporal.io/cloud-sdk/api/cloudservice/v1" - operation "go.temporal.io/cloud-sdk/api/operation/v1" + "github.com/temporalio/cloud-cli/temporalcloudcli/internal/printer" "go.temporal.io/cloud-sdk/cloudclient" ) @@ -225,46 +225,18 @@ func (s *SharedServerSuite) getCloudClient() *cloudclient.Client { } // pollAsyncOperation polls an async operation until it reaches a terminal state. -// It prints status updates every second and returns the final AsyncOperation. +// This is a test wrapper around the PollAsyncOperation function. func (s *SharedServerSuite) pollAsyncOperation( cloudClient *cloudclient.Client, operationID string, ) error { - - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - - for { - select { - case <-s.Context.Done(): - return fmt.Errorf("operation polling cancelled: %w", s.Context.Err()) - case <-ticker.C: - // Get the current state of the operation - resp, err := cloudClient.CloudService().GetAsyncOperation(s.Context, &cloudservice.GetAsyncOperationRequest{ - AsyncOperationId: operationID, - }) - if err != nil { - return fmt.Errorf("failed to get async operation status: %w", err) - } - - asyncOp := resp.GetAsyncOperation() - if asyncOp == nil { - return fmt.Errorf("async operation not found") - } - - // Print current state - switch asyncOp.State { - case operation.AsyncOperation_STATE_PENDING, operation.AsyncOperation_STATE_IN_PROGRESS: - case operation.AsyncOperation_STATE_FULFILLED: - return nil - case operation.AsyncOperation_STATE_FAILED: - return fmt.Errorf("async operation failed: %s", asyncOp.FailureReason) - case operation.AsyncOperation_STATE_CANCELLED: - return fmt.Errorf("async operation cancelled") - case operation.AsyncOperation_STATE_REJECTED: - return fmt.Errorf("async operation rejected") - default: - } - } + // Create a minimal CommandContext for testing (with discard printer to skip output) + cctx := &temporalcloudcli.CommandContext{ + Context: s.Context, + Printer: &printer.Printer{ + Output: io.Discard, // Discard all output for tests + }, } + + return temporalcloudcli.PollAsyncOperation(cctx, cloudClient, operationID, "") } diff --git a/temporalcloudcli/common.go b/temporalcloudcli/common.go index 400ac1a..9503f02 100644 --- a/temporalcloudcli/common.go +++ b/temporalcloudcli/common.go @@ -150,7 +150,7 @@ func promptApplyResource(cctx *CommandContext, existing, actual proto.Message, v // // AIDEV-NOTE: This function takes a pre-built cloudClient. Commands should // build the client using cctx.BuildCloudClient() and pass it directly. -func pollAsyncOperation( +func PollAsyncOperation( cctx *CommandContext, cloudClient *cloudclient.Client, operationID string,