From c10c5c23e9906047d79cfd88d6fc82049e154726 Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Thu, 12 Feb 2026 20:18:57 -0800 Subject: [PATCH 01/14] Use verbose diff flag --- temporalcloudcli/commands.gen.go | 22 +++++++++++++++++-- temporalcloudcli/commands.namespace.go | 2 +- .../commands.namespace.lifecycle.go | 2 +- .../commands.namespace.retention.go | 2 +- temporalcloudcli/commands.yml | 17 +++++++++----- 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/temporalcloudcli/commands.gen.go b/temporalcloudcli/commands.gen.go index 8163c17..64e5b74 100644 --- a/temporalcloudcli/commands.gen.go +++ b/temporalcloudcli/commands.gen.go @@ -29,6 +29,16 @@ func (v *ClientOptions) BuildFlags(f *pflag.FlagSet) { _ = f.MarkHidden("server") } +type DiffOptions struct { + VerboseDiff bool + FlagSet *pflag.FlagSet +} + +func (v *DiffOptions) BuildFlags(f *pflag.FlagSet) { + v.FlagSet = f + f.BoolVar(&v.VerboseDiff, "verbose-diff", false, "Show detailed differences between the current and desired namespace configurations when changes are detected.") +} + type CloudCommand struct { Command cobra.Command ClientOptions @@ -153,11 +163,11 @@ type CloudNamespaceApplyCommand struct { Parent *CloudNamespaceCommand Command cobra.Command ClientOptions + DiffOptions Spec string AsyncOperationId string Idempotent bool Async bool - VerboseDiff bool ResourceVersion string } @@ -178,9 +188,9 @@ func NewCloudNamespaceApplyCommand(cctx *CommandContext, parent *CloudNamespaceC s.Command.Flags().StringVar(&s.AsyncOperationId, "async-operation-id", "", "Custom identifier for tracking this async operation. If not provided, a unique ID is generated automatically.") s.Command.Flags().BoolVar(&s.Idempotent, "idempotent", false, "Succeed silently if the namespace already matches the specification. Without this flag, the command errors when no changes are needed.") s.Command.Flags().BoolVar(&s.Async, "async", false, "Return immediately after initiating the operation instead of waiting for completion. Use the returned operation ID to check status later.") - s.Command.Flags().BoolVar(&s.VerboseDiff, "verbose-diff", false, "Show detailed differences between the current and desired namespace configurations when changes are detected.") s.Command.Flags().StringVarP(&s.ResourceVersion, "resource-version", "v", "", "Resource version for optimistic concurrency control. If not provided, the current version is fetched automatically.") s.ClientOptions.BuildFlags(s.Command.Flags()) + s.DiffOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -193,6 +203,7 @@ type CloudNamespaceDeleteCommand struct { Parent *CloudNamespaceCommand Command cobra.Command ClientOptions + DiffOptions Namespace string AsyncOperationId string Async bool @@ -219,6 +230,7 @@ func NewCloudNamespaceDeleteCommand(cctx *CommandContext, parent *CloudNamespace s.Command.Flags().BoolVar(&s.Idempotent, "idempotent", false, "Succeed silently if the namespace does not exist. Without this flag, the command errors if the namespace is not found.") s.Command.Flags().StringVarP(&s.ResourceVersion, "resource-version", "v", "", "Resource version for optimistic concurrency control. If not provided, the current version is fetched automatically.") s.ClientOptions.BuildFlags(s.Command.Flags()) + s.DiffOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -231,6 +243,7 @@ type CloudNamespaceEditCommand struct { Parent *CloudNamespaceCommand Command cobra.Command ClientOptions + DiffOptions Namespace string AsyncOperationId string Idempotent bool @@ -257,6 +270,7 @@ func NewCloudNamespaceEditCommand(cctx *CommandContext, parent *CloudNamespaceCo s.Command.Flags().BoolVar(&s.Async, "async", false, "Return immediately after initiating the operation instead of waiting for completion. Use the returned operation ID to check status later.") s.Command.Flags().StringVarP(&s.ResourceVersion, "resource-version", "v", "", "Resource version for optimistic concurrency control. If not provided, the current version is fetched automatically.") s.ClientOptions.BuildFlags(s.Command.Flags()) + s.DiffOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -348,6 +362,7 @@ type CloudNamespaceLifecycleSetCommand struct { Parent *CloudNamespaceLifecycleCommand Command cobra.Command ClientOptions + DiffOptions Namespace string EnableDeleteProtection bool AsyncOperationId string @@ -377,6 +392,7 @@ func NewCloudNamespaceLifecycleSetCommand(cctx *CommandContext, parent *CloudNam s.Command.Flags().BoolVar(&s.Idempotent, "idempotent", false, "Succeed silently if the lifecycle configuration is already set to the specified value. Without this flag, the command errors when no change is needed.") s.Command.Flags().StringVar(&s.ResourceVersion, "resource-version", "", "Resource version for optimistic concurrency control. If not provided, the current version is fetched automatically.") s.ClientOptions.BuildFlags(s.Command.Flags()) + s.DiffOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -469,6 +485,7 @@ type CloudNamespaceRetentionSetCommand struct { Parent *CloudNamespaceRetentionCommand Command cobra.Command ClientOptions + DiffOptions Namespace string AsyncOperationId string Async bool @@ -498,6 +515,7 @@ func NewCloudNamespaceRetentionSetCommand(cctx *CommandContext, parent *CloudNam _ = cobra.MarkFlagRequired(s.Command.Flags(), "retention-days") s.Command.Flags().StringVar(&s.ResourceVersion, "resource-version", "", "Resource version for optimistic concurrency control. If not provided, the current version is fetched automatically.") s.ClientOptions.BuildFlags(s.Command.Flags()) + s.DiffOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) diff --git a/temporalcloudcli/commands.namespace.go b/temporalcloudcli/commands.namespace.go index 7bc5930..423e76b 100644 --- a/temporalcloudcli/commands.namespace.go +++ b/temporalcloudcli/commands.namespace.go @@ -46,7 +46,7 @@ func (c *CloudNamespaceEditCommand) run(cctx *CommandContext, _ []string) error return err } - err = promptApplyResource(cctx, ns.Spec, newSpec, cctx.RootCommand.AutoConfirm) + err = promptApplyResource(cctx, ns.Spec, newSpec, c.VerboseDiff) if err != nil { return err } diff --git a/temporalcloudcli/commands.namespace.lifecycle.go b/temporalcloudcli/commands.namespace.lifecycle.go index 6e2898b..fff543d 100644 --- a/temporalcloudcli/commands.namespace.lifecycle.go +++ b/temporalcloudcli/commands.namespace.lifecycle.go @@ -61,7 +61,7 @@ func (c *CloudNamespaceLifecycleSetCommand) run(cctx *CommandContext, _ []string newSpec.Lifecycle.EnableDeleteProtection = c.EnableDeleteProtection // Show diff - err = promptApplyResource(cctx, ns.Spec, newSpec, cctx.RootCommand.AutoConfirm) + err = promptApplyResource(cctx, ns.Spec, newSpec, c.VerboseDiff) if err != nil { return err } diff --git a/temporalcloudcli/commands.namespace.retention.go b/temporalcloudcli/commands.namespace.retention.go index 907a450..4716929 100644 --- a/temporalcloudcli/commands.namespace.retention.go +++ b/temporalcloudcli/commands.namespace.retention.go @@ -23,7 +23,7 @@ func (c *CloudNamespaceRetentionSetCommand) run(cctx *CommandContext, _ []string newSpec := proto.Clone(ns.Spec).(*namespace.NamespaceSpec) newSpec.RetentionDays = int32(c.RetentionDays) - err = promptApplyResource(cctx, ns.Spec, newSpec, cctx.RootCommand.AutoConfirm) + err = promptApplyResource(cctx, ns.Spec, newSpec, c.VerboseDiff) if err != nil { return err } diff --git a/temporalcloudcli/commands.yml b/temporalcloudcli/commands.yml index bc4ae09..16c49e4 100644 --- a/temporalcloudcli/commands.yml +++ b/temporalcloudcli/commands.yml @@ -187,6 +187,7 @@ commands: has-init: false option-sets: - client + - diff options: - name: spec type: string @@ -209,11 +210,6 @@ commands: description: | Return immediately after initiating the operation instead of waiting for completion. Use the returned operation ID to check status later. - - name: verbose-diff - type: bool - description: | - Show detailed differences between the current and desired namespace - configurations when changes are detected. - name: resource-version type: string short: v @@ -238,6 +234,7 @@ commands: has-init: false option-sets: - client + - diff options: - name: namespace type: string @@ -282,6 +279,7 @@ commands: has-init: false option-sets: - client + - diff options: - name: namespace type: string @@ -382,6 +380,7 @@ commands: has-init: false option-sets: - client + - diff options: - name: namespace type: string @@ -460,6 +459,7 @@ commands: has-init: false option-sets: - client + - diff options: - name: namespace type: string @@ -512,5 +512,12 @@ option-sets: to non-production environments. hidden: true default: saas-api.tmprl-test.cloud:443 + - name: diff + options: + - name: verbose-diff + type: bool + description: | + Show detailed differences between the current and desired namespace + configurations when changes are detected. - name: common external-package: github.com/temporalio/cli/cliext From 1db6912768946a2015db2adf50cc2287be743848 Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Thu, 12 Feb 2026 20:29:09 -0800 Subject: [PATCH 02/14] Clean up temp file --- temporalcloudcli/common.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/temporalcloudcli/common.go b/temporalcloudcli/common.go index 9c051d7..9b993b9 100644 --- a/temporalcloudcli/common.go +++ b/temporalcloudcli/common.go @@ -10,9 +10,9 @@ import ( "strings" "time" - "go.temporal.io/cloud-sdk/cloudclient" cloudservice "go.temporal.io/cloud-sdk/api/cloudservice/v1" operation "go.temporal.io/cloud-sdk/api/operation/v1" + "go.temporal.io/cloud-sdk/cloudclient" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -96,6 +96,11 @@ func runEditor(existing []byte) ([]byte, error) { return nil, fmt.Errorf("unable to create temp file for editing: %v", err) } + defer func() { + // Clean up temp file. + _ = os.Remove(f.Name()) + }() + if _, err := f.Write(existing); err != nil { return nil, fmt.Errorf("unable to write existing data to temp file for editing: %v", err) } From 13399727c35a8b7125fca755f7f153b1dbb18ffa Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Thu, 12 Feb 2026 20:34:13 -0800 Subject: [PATCH 03/14] Use correct namespace field --- temporalcloudcli/namespace.go | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/temporalcloudcli/namespace.go b/temporalcloudcli/namespace.go index 8d671c0..d1644f8 100644 --- a/temporalcloudcli/namespace.go +++ b/temporalcloudcli/namespace.go @@ -172,22 +172,24 @@ type applyNamespaceResponse struct { } func (c *namespaceClient) applyNamespace(ctx context.Context, params applyNamespaceParams) (applyNamespaceResponse, error) { - existing, err := c.getNamespaceByName(ctx, params.spec.Name) - if err != nil && !isNotFoundErr(err) { - return applyNamespaceResponse{}, err - } else if err != nil && isNotFoundErr(err) { - // create the namespace - res, err := c.createNamespace(ctx, createNamespaceParams{ - spec: params.spec, - asyncOperationID: params.asyncOperationID, - }) - if err != nil { - return applyNamespaceResponse{}, err + existing, err := c.getNamespaceByName(ctx, params.namespace) + if err != nil { + if isNotFoundErr(err) { + // create the namespace + res, err := c.createNamespace(ctx, createNamespaceParams{ + spec: params.spec, + asyncOperationID: params.asyncOperationID, + }) + if err != nil { + return applyNamespaceResponse{}, err + } + return applyNamespaceResponse{ + asyncOp: res.asyncOp, + Namespace: res.Namespace, + }, nil } - return applyNamespaceResponse{ - asyncOp: res.asyncOp, - Namespace: res.Namespace, - }, nil + + return applyNamespaceResponse{}, err } // update the namespace From 173d3aada2700ef6d19186b86d4faec192649976 Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Thu, 12 Feb 2026 20:46:15 -0800 Subject: [PATCH 04/14] Set common options --- temporalcloudcli/cloud.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/temporalcloudcli/cloud.go b/temporalcloudcli/cloud.go index fc3de2c..bcfb8ce 100644 --- a/temporalcloudcli/cloud.go +++ b/temporalcloudcli/cloud.go @@ -51,7 +51,9 @@ func (b *CloudOptionsBuilder) Build(ctx context.Context) (*CloudOptions, error) } } - cloudOpts := &CloudOptions{} + cloudOpts := &CloudOptions{ + CommonOptions: common, + } // Set logger if provided. if b.Logger != nil { From c6ae870983fd1cc7572b7075892b96de6ae74f44 Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Thu, 12 Feb 2026 20:47:56 -0800 Subject: [PATCH 05/14] Check correct field --- temporalcloudcli/commands_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/temporalcloudcli/commands_test.go b/temporalcloudcli/commands_test.go index 8e40585..178454b 100644 --- a/temporalcloudcli/commands_test.go +++ b/temporalcloudcli/commands_test.go @@ -182,7 +182,7 @@ func (s *SharedServerSuite) SetupSuite() { s.apiKey = os.Getenv("TEMPORAL_API_KEY") s.Suite.Require().NotEmpty(s.apiKey, "Could not load TEMPORAL_API_KEY. Are you running with `mise run test` and have you filled out your .env? See README.md for details.") s.server = os.Getenv("TEMPORAL_CLOUD_SERVER") - s.Suite.Require().NotEmpty(s.apiKey, "Could not load TEMPORAL_CLOUD_SERVER. Are you running with `mise run test` and have you filled out your .env? See README.md for details.") + s.Suite.Require().NotEmpty(s.server, "Could not load TEMPORAL_CLOUD_SERVER. Are you running with `mise run test` and have you filled out your .env? See README.md for details.") } func (s *SharedServerSuite) TearDownSuite() { From b4e7250994718a315d2dbedaf932a85e62b021ae Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Thu, 12 Feb 2026 20:55:52 -0800 Subject: [PATCH 06/14] Remove unused funcs --- temporalcloudcli/commands.go | 69 ------------------------------------ temporalcloudcli/common.go | 17 +-------- 2 files changed, 1 insertion(+), 85 deletions(-) diff --git a/temporalcloudcli/commands.go b/temporalcloudcli/commands.go index 755424d..14bbd03 100644 --- a/temporalcloudcli/commands.go +++ b/temporalcloudcli/commands.go @@ -4,7 +4,6 @@ import ( "bufio" "context" "encoding/json" - "errors" "fmt" "io" "log/slog" @@ -21,17 +20,13 @@ import ( "github.com/spf13/pflag" "github.com/temporalio/cloud-cli/temporalcloudcli/internal/printer" "go.temporal.io/api/common/v1" - commonpb "go.temporal.io/api/common/v1" "go.temporal.io/api/failure/v1" "go.temporal.io/api/temporalproto" "go.temporal.io/cloud-sdk/cloudclient" "go.temporal.io/sdk/contrib/envconfig" - "go.temporal.io/sdk/converter" - "go.temporal.io/sdk/temporal" "golang.org/x/term" "google.golang.org/grpc" "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/timestamppb" ) // Version is the value put as the default command version. This is often @@ -546,15 +541,6 @@ func (c *CloudCommand) preRun(cctx *CommandContext) error { return nil } -func aliasNormalizer(aliases map[string]string) func(f *pflag.FlagSet, name string) pflag.NormalizedName { - return func(f *pflag.FlagSet, name string) pflag.NormalizedName { - if actual := aliases[name]; actual != "" { - name = actual - } - return pflag.NormalizedName(name) - } -} - func newNopLogger() *slog.Logger { return slog.New(discardLogHandler{}) } type discardLogHandler struct{} @@ -564,61 +550,6 @@ func (discardLogHandler) Handle(context.Context, slog.Record) error { return nil func (d discardLogHandler) WithAttrs([]slog.Attr) slog.Handler { return d } func (d discardLogHandler) WithGroup(string) slog.Handler { return d } -func timestampToTime(t *timestamppb.Timestamp) time.Time { - if t == nil { - return time.Time{} - } - return t.AsTime() -} - type nopWriter struct{} func (nopWriter) Write(b []byte) (int, error) { return len(b), nil } - -type structuredError struct { - Message string `json:"message"` - Type string `json:"type,omitempty"` - Details any `json:"details,omitempty"` -} - -func fromApplicationError(err *temporal.ApplicationError) (*structuredError, error) { - var deets any - if err := err.Details(&deets); err != nil && !errors.Is(err, temporal.ErrNoData) { - return nil, err - } - return &structuredError{ - Message: err.Error(), - Type: err.Type(), - Details: deets, - }, nil -} - -func encodeMapToPayloads(in map[string]any) (map[string]*commonpb.Payload, error) { - if len(in) == 0 { - return nil, nil - } - // search attributes always use default dataconverter - dc := converter.GetDefaultDataConverter() - out := make(map[string]*commonpb.Payload, len(in)) - for key, val := range in { - payload, err := dc.ToPayload(val) - if err != nil { - return nil, err - } - out[key] = payload - } - return out, nil -} - -type overrideDisplayTypeFlagValue struct { - pflag.Value - displayType string -} - -func (o *overrideDisplayTypeFlagValue) Type() string { - return o.displayType -} - -func overrideFlagDisplayType(flag *pflag.Flag, displayType string) { - flag.Value = &overrideDisplayTypeFlagValue{Value: flag.Value, displayType: displayType} -} diff --git a/temporalcloudcli/common.go b/temporalcloudcli/common.go index 9b993b9..6c20b0a 100644 --- a/temporalcloudcli/common.go +++ b/temporalcloudcli/common.go @@ -3,7 +3,6 @@ package temporalcloudcli import ( "bytes" "cmp" - "encoding/json" "fmt" "os" "os/exec" @@ -47,9 +46,7 @@ func isNotFoundErr(e error) bool { // or treats the input as inline JSON. Returns the parsed data as a byte slice. func loadJSONSpec(spec string) ([]byte, error) { // Check if spec starts with '@' indicating file path - if strings.HasPrefix(spec, "@") { - // Remove '@' prefix and read file - filePath := strings.TrimPrefix(spec, "@") + if filePath, ok := strings.CutPrefix(spec, "@"); ok { data, err := os.ReadFile(filePath) if err != nil { return nil, fmt.Errorf("failed to read spec file %q: %w", filePath, err) @@ -61,18 +58,6 @@ func loadJSONSpec(spec string) ([]byte, error) { return []byte(spec), nil } -func runEditorForJSONEdit(existing, valuePtr any) error { - existingBytes, err := json.MarshalIndent(existing, "", " ") - if err != nil { - return fmt.Errorf("unable to convert existing object to json: %v", err) - } - updatedBytes, err := runEditor(existingBytes) - if err != nil { - return err - } - return json.Unmarshal(updatedBytes, valuePtr) -} - func runEditorForJSONEditForProtos(existing, value proto.Message) error { marshaler := protojson.MarshalOptions{ EmitUnpopulated: true, From ea3ea16044183cba5f7b04817aa2e2d00a69eb8f Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Thu, 12 Feb 2026 20:58:44 -0800 Subject: [PATCH 07/14] Guard against panic --- temporalcloudcli/commands.login.go | 1 + 1 file changed, 1 insertion(+) diff --git a/temporalcloudcli/commands.login.go b/temporalcloudcli/commands.login.go index 2450a2c..d491d93 100644 --- a/temporalcloudcli/commands.login.go +++ b/temporalcloudcli/commands.login.go @@ -19,6 +19,7 @@ func (c *CloudLoginCommand) run(cctx *CommandContext, _ []string) error { return fmt.Errorf("failed to load profile: %w", err) } if loadClientOauthRes.OAuth != nil && + loadClientOauthRes.OAuth.Token != nil && !reflect.DeepEqual(*loadClientOauthRes.OAuth, cliext.OAuthConfig{}) && !reflect.DeepEqual(*loadClientOauthRes.OAuth.ClientConfig, oauth2.Config{}) && !c.Reset { From 12d3f26906bfba65ca5531ea07b4f0c12f1e8bf1 Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Thu, 12 Feb 2026 21:10:50 -0800 Subject: [PATCH 08/14] setup go --- .github/workflows/build.yml | 12 ++++++++++++ .github/workflows/test.yml | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index df1e339..765a35c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,6 +13,12 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v5 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: "go.mod" + check-latest: true + cache: false - name: Add Go bin to PATH run: echo "$HOME/go/bin" >> $GITHUB_PATH - name: Run commands generation command @@ -34,5 +40,11 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v5 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: "go.mod" + check-latest: true + cache: false - name: Build the cli binary run: make build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6790412..1e08e67 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,6 +23,13 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: "go.mod" + check-latest: true + cache: false + - name: Setup mise uses: jdx/mise-action@v2 with: From a6f2911024881f890549643d8b935ec22cdabc2c Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Thu, 12 Feb 2026 21:27:02 -0800 Subject: [PATCH 09/14] Remove unused option set --- temporalcloudcli/commands.gen.go | 2 -- temporalcloudcli/commands.yml | 1 - 2 files changed, 3 deletions(-) diff --git a/temporalcloudcli/commands.gen.go b/temporalcloudcli/commands.gen.go index 64e5b74..56fef4f 100644 --- a/temporalcloudcli/commands.gen.go +++ b/temporalcloudcli/commands.gen.go @@ -203,7 +203,6 @@ type CloudNamespaceDeleteCommand struct { Parent *CloudNamespaceCommand Command cobra.Command ClientOptions - DiffOptions Namespace string AsyncOperationId string Async bool @@ -230,7 +229,6 @@ func NewCloudNamespaceDeleteCommand(cctx *CommandContext, parent *CloudNamespace s.Command.Flags().BoolVar(&s.Idempotent, "idempotent", false, "Succeed silently if the namespace does not exist. Without this flag, the command errors if the namespace is not found.") s.Command.Flags().StringVarP(&s.ResourceVersion, "resource-version", "v", "", "Resource version for optimistic concurrency control. If not provided, the current version is fetched automatically.") s.ClientOptions.BuildFlags(s.Command.Flags()) - s.DiffOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) diff --git a/temporalcloudcli/commands.yml b/temporalcloudcli/commands.yml index 16c49e4..9329a53 100644 --- a/temporalcloudcli/commands.yml +++ b/temporalcloudcli/commands.yml @@ -279,7 +279,6 @@ commands: has-init: false option-sets: - client - - diff options: - name: namespace type: string From 28181174b984f0ad966924da505cf30a0a3e6db9 Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Thu, 12 Feb 2026 21:27:33 -0800 Subject: [PATCH 10/14] Fix create namespace with apply --- temporalcloudcli/commands.namespace.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/temporalcloudcli/commands.namespace.go b/temporalcloudcli/commands.namespace.go index 423e76b..a537d90 100644 --- a/temporalcloudcli/commands.namespace.go +++ b/temporalcloudcli/commands.namespace.go @@ -125,11 +125,11 @@ func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error existingResourceVersion := "" var existingSpec *namespace.NamespaceSpec - existingNamespaceIdentifier := "" + namespaceIdentifier := spec.Name if found { existingResourceVersion = existing.ResourceVersion existingSpec = existing.Spec - existingNamespaceIdentifier = existing.Namespace + namespaceIdentifier = existing.Namespace } // Step 5: Confirm apply if not forced @@ -146,7 +146,7 @@ func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error } params := applyNamespaceParams{ - namespace: existingNamespaceIdentifier, + namespace: namespaceIdentifier, spec: spec, resourceVersion: resourceVersion, @@ -167,7 +167,7 @@ func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error Namespace string }{ Status: "unchanged", - Namespace: existingNamespaceIdentifier, + Namespace: namespaceIdentifier, } return cctx.Printer.PrintStructured(result, printer.StructuredOptions{}) } From 4a4423fe8d688237e351c41eb12f4a0ad255b01c Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Thu, 12 Feb 2026 21:31:42 -0800 Subject: [PATCH 11/14] Remove redundancy --- temporalcloudcli/commands.namespace.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/temporalcloudcli/commands.namespace.go b/temporalcloudcli/commands.namespace.go index a537d90..62511eb 100644 --- a/temporalcloudcli/commands.namespace.go +++ b/temporalcloudcli/commands.namespace.go @@ -125,11 +125,10 @@ func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error existingResourceVersion := "" var existingSpec *namespace.NamespaceSpec - namespaceIdentifier := spec.Name + if found { existingResourceVersion = existing.ResourceVersion existingSpec = existing.Spec - namespaceIdentifier = existing.Namespace } // Step 5: Confirm apply if not forced @@ -146,7 +145,7 @@ func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error } params := applyNamespaceParams{ - namespace: namespaceIdentifier, + namespace: spec.Name, spec: spec, resourceVersion: resourceVersion, @@ -167,7 +166,7 @@ func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error Namespace string }{ Status: "unchanged", - Namespace: namespaceIdentifier, + Namespace: spec.Name, } return cctx.Printer.PrintStructured(result, printer.StructuredOptions{}) } From 1ac9c9b865a6fb9351166e86a34fe9f90dcdac03 Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Fri, 13 Feb 2026 09:39:59 -0800 Subject: [PATCH 12/14] Add gha content block --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 765a35c..7e57b40 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,9 @@ on: branches: - main +permissions: + contents: read + jobs: verify-generation: runs-on: ubuntu-latest From 2b42153bb52543d998b83a237c3c90457915e2ed Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Fri, 13 Feb 2026 11:03:52 -0800 Subject: [PATCH 13/14] Fix create vs update behavior --- temporalcloudcli/commands.namespace.go | 70 ++++++++++++------- .../commands.namespace.lifecycle.go | 12 ++-- .../commands.namespace.retention.go | 12 ++-- temporalcloudcli/namespace.go | 61 ---------------- 4 files changed, 56 insertions(+), 99 deletions(-) diff --git a/temporalcloudcli/commands.namespace.go b/temporalcloudcli/commands.namespace.go index 62511eb..17b100a 100644 --- a/temporalcloudcli/commands.namespace.go +++ b/temporalcloudcli/commands.namespace.go @@ -4,6 +4,7 @@ import ( "fmt" namespace "go.temporal.io/cloud-sdk/api/namespace/v1" + operation "go.temporal.io/cloud-sdk/api/operation/v1" "github.com/temporalio/cloud-cli/temporalcloudcli/internal/printer" ) @@ -57,7 +58,7 @@ func (c *CloudNamespaceEditCommand) run(cctx *CommandContext, _ []string) error resourceVersion = ns.ResourceVersion } - res, err := client.applyNamespace(cctx.Context, applyNamespaceParams{ + asyncOp, err := client.updateNamespace(cctx.Context, updateNamespaceParams{ namespace: c.Namespace, spec: newSpec, asyncOperationID: c.AsyncOperationId, @@ -69,14 +70,14 @@ func (c *CloudNamespaceEditCommand) run(cctx *CommandContext, _ []string) error } // TODO: (gmankes) remove this -- clean up and make shareable - if res.asyncOp == nil { + if asyncOp == nil { // Nothing changed (idempotent case) result := struct { Status string Namespace string }{ Status: "unchanged", - Namespace: newSpec.Name, + Namespace: c.Namespace, } return cctx.Printer.PrintStructured(result, printer.StructuredOptions{}) } @@ -85,13 +86,13 @@ func (c *CloudNamespaceEditCommand) run(cctx *CommandContext, _ []string) error if c.Async { // Return immediately with the async operation return cctx.Printer.PrintStructured(MutationResult{ - AsyncOp: res.asyncOp, - ID: res.Namespace, + AsyncOp: asyncOp, + ID: c.Namespace, }, printer.StructuredOptions{}) } // Poll for completion - return pollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace) + return pollAsyncOperation(cctx, cloudClient, asyncOp.Id, c.Namespace) } func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error { @@ -125,10 +126,12 @@ func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error existingResourceVersion := "" var existingSpec *namespace.NamespaceSpec + existingNamespaceIdentifier := "" if found { existingResourceVersion = existing.ResourceVersion existingSpec = existing.Spec + existingNamespaceIdentifier = existing.Namespace } // Step 5: Confirm apply if not forced @@ -137,51 +140,66 @@ func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error return err } - // Step 5: Apply the namespace (create or update) + // Step 6: Apply the namespace (create or update) // Use provided resource version, or use fetched version resourceVersion := c.ResourceVersion if resourceVersion == "" { resourceVersion = existingResourceVersion } - params := applyNamespaceParams{ - namespace: spec.Name, - spec: spec, - - resourceVersion: resourceVersion, - asyncOperationID: c.AsyncOperationId, // Use the flag value if provided - idempotent: c.Idempotent, // Use the flag value - } + var asyncOp *operation.AsyncOperation + var namespaceID string - res, err := client.applyNamespace(cctx.Context, params) - if err != nil { - return fmt.Errorf("failed to apply namespace: %w", err) + if found { + // Update existing namespace + asyncOp, err = client.updateNamespace(cctx.Context, updateNamespaceParams{ + namespace: existingNamespaceIdentifier, + spec: spec, + asyncOperationID: c.AsyncOperationId, + idempotent: c.Idempotent, + resourceVersion: resourceVersion, + }) + if err != nil { + return fmt.Errorf("failed to update namespace: %w", err) + } + namespaceID = existingNamespaceIdentifier + } else { + // Create new namespace + res, err := client.createNamespace(cctx.Context, createNamespaceParams{ + spec: spec, + asyncOperationID: c.AsyncOperationId, + }) + if err != nil { + return fmt.Errorf("failed to create namespace: %w", err) + } + asyncOp = res.asyncOp + namespaceID = res.Namespace } - // Step 5: Handle result - if res.asyncOp == nil { + // Step 7: Handle result + if asyncOp == nil { // Nothing changed (idempotent case) result := struct { Status string Namespace string }{ Status: "unchanged", - Namespace: spec.Name, + Namespace: namespaceID, } return cctx.Printer.PrintStructured(result, printer.StructuredOptions{}) } - // Step 6: Handle async flag + // Step 8: Handle async flag if c.Async { // Return immediately with the async operation return cctx.Printer.PrintStructured(MutationResult{ - AsyncOp: res.asyncOp, - ID: res.Namespace, + AsyncOp: asyncOp, + ID: namespaceID, }, printer.StructuredOptions{}) } - // Step 7: Poll for completion - return pollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace) + // Step 9: Poll for completion + return pollAsyncOperation(cctx, cloudClient, asyncOp.Id, namespaceID) } func (c *CloudNamespaceDeleteCommand) run(cctx *CommandContext, _ []string) error { diff --git a/temporalcloudcli/commands.namespace.lifecycle.go b/temporalcloudcli/commands.namespace.lifecycle.go index fff543d..0ad336c 100644 --- a/temporalcloudcli/commands.namespace.lifecycle.go +++ b/temporalcloudcli/commands.namespace.lifecycle.go @@ -72,7 +72,7 @@ func (c *CloudNamespaceLifecycleSetCommand) run(cctx *CommandContext, _ []string resourceVersion = ns.ResourceVersion } - res, err := client.applyNamespace(cctx.Context, applyNamespaceParams{ + asyncOp, err := client.updateNamespace(cctx.Context, updateNamespaceParams{ namespace: c.Namespace, spec: newSpec, asyncOperationID: c.AsyncOperationId, @@ -83,14 +83,14 @@ func (c *CloudNamespaceLifecycleSetCommand) run(cctx *CommandContext, _ []string return err } - if res.asyncOp == nil { + if asyncOp == nil { // Nothing changed (idempotent case) result := struct { Status string Namespace string }{ Status: "unchanged", - Namespace: newSpec.Name, + Namespace: c.Namespace, } return cctx.Printer.PrintStructured(result, printer.StructuredOptions{}) } @@ -99,11 +99,11 @@ func (c *CloudNamespaceLifecycleSetCommand) run(cctx *CommandContext, _ []string if c.Async { // Return immediately with the async operation return cctx.Printer.PrintStructured(MutationResult{ - AsyncOp: res.asyncOp, - ID: res.Namespace, + AsyncOp: asyncOp, + ID: c.Namespace, }, printer.StructuredOptions{}) } // Poll for completion - return pollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace) + return pollAsyncOperation(cctx, cloudClient, asyncOp.Id, c.Namespace) } diff --git a/temporalcloudcli/commands.namespace.retention.go b/temporalcloudcli/commands.namespace.retention.go index 4716929..5a342e5 100644 --- a/temporalcloudcli/commands.namespace.retention.go +++ b/temporalcloudcli/commands.namespace.retention.go @@ -34,7 +34,7 @@ func (c *CloudNamespaceRetentionSetCommand) run(cctx *CommandContext, _ []string resourceVersion = ns.ResourceVersion } - res, err := client.applyNamespace(cctx.Context, applyNamespaceParams{ + asyncOp, err := client.updateNamespace(cctx.Context, updateNamespaceParams{ namespace: c.Namespace, spec: newSpec, asyncOperationID: c.AsyncOperationId, @@ -44,14 +44,14 @@ func (c *CloudNamespaceRetentionSetCommand) run(cctx *CommandContext, _ []string if err != nil { return err } - if res.asyncOp == nil { + if asyncOp == nil { // Nothing changed (idempotent case) result := struct { Status string Namespace string }{ Status: "unchanged", - Namespace: newSpec.Name, + Namespace: c.Namespace, } return cctx.Printer.PrintStructured(result, printer.StructuredOptions{}) } @@ -60,13 +60,13 @@ func (c *CloudNamespaceRetentionSetCommand) run(cctx *CommandContext, _ []string if c.Async { // Return immediately with the async operation return cctx.Printer.PrintStructured(MutationResult{ - AsyncOp: res.asyncOp, - ID: res.Namespace, + AsyncOp: asyncOp, + ID: c.Namespace, }, printer.StructuredOptions{}) } // Poll for completion - return pollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace) + return pollAsyncOperation(cctx, cloudClient, asyncOp.Id, c.Namespace) } func (c *CloudNamespaceRetentionGetCommand) run(cctx *CommandContext, _ []string) error { diff --git a/temporalcloudcli/namespace.go b/temporalcloudcli/namespace.go index d1644f8..c9af2be 100644 --- a/temporalcloudcli/namespace.go +++ b/temporalcloudcli/namespace.go @@ -157,67 +157,6 @@ func (c *namespaceClient) deleteNamespace(ctx context.Context, params deleteName return res.AsyncOperation, nil } -type applyNamespaceParams struct { - namespace string - spec *namespace.NamespaceSpec - - resourceVersion string // optional, if empty, will be fetched - asyncOperationID string - idempotent bool -} - -type applyNamespaceResponse struct { - asyncOp *operation.AsyncOperation - Namespace string -} - -func (c *namespaceClient) applyNamespace(ctx context.Context, params applyNamespaceParams) (applyNamespaceResponse, error) { - existing, err := c.getNamespaceByName(ctx, params.namespace) - if err != nil { - if isNotFoundErr(err) { - // create the namespace - res, err := c.createNamespace(ctx, createNamespaceParams{ - spec: params.spec, - asyncOperationID: params.asyncOperationID, - }) - if err != nil { - return applyNamespaceResponse{}, err - } - return applyNamespaceResponse{ - asyncOp: res.asyncOp, - Namespace: res.Namespace, - }, nil - } - - return applyNamespaceResponse{}, err - } - - // update the namespace - if params.resourceVersion == "" { - params.resourceVersion = existing.ResourceVersion - } - - // update - // Namespace exists, update it using the current resource version - updateParams := updateNamespaceParams{ - namespace: params.namespace, - spec: params.spec, - - asyncOperationID: params.asyncOperationID, - idempotent: params.idempotent, - resourceVersion: params.resourceVersion, - } - - res, err := c.updateNamespace(ctx, updateParams) - if err != nil { - return applyNamespaceResponse{}, err - } - return applyNamespaceResponse{ - asyncOp: res, - Namespace: existing.Namespace, - }, nil -} - func (c *namespaceClient) getNamespaceByName(ctx context.Context, name string) (*namespace.Namespace, error) { namespaces, err := c.listNamespacesWithName(ctx, name, true) if err != nil { From 7178a70e928c752135cbf18a061b95bb2d02fbae Mon Sep 17 00:00:00 2001 From: Kohei Hisakuni Date: Wed, 18 Feb 2026 12:17:01 -0800 Subject: [PATCH 14/14] fix --- temporalcloudcli/commands.login.go | 1 + temporalcloudcli/commands.namespace.go | 4 ++-- temporalcloudcli/commands.namespace.lifecycle.go | 2 +- temporalcloudcli/commands.namespace.retention.go | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/temporalcloudcli/commands.login.go b/temporalcloudcli/commands.login.go index d491d93..babedae 100644 --- a/temporalcloudcli/commands.login.go +++ b/temporalcloudcli/commands.login.go @@ -20,6 +20,7 @@ func (c *CloudLoginCommand) run(cctx *CommandContext, _ []string) error { } if loadClientOauthRes.OAuth != nil && loadClientOauthRes.OAuth.Token != nil && + loadClientOauthRes.OAuth.ClientConfig != nil && !reflect.DeepEqual(*loadClientOauthRes.OAuth, cliext.OAuthConfig{}) && !reflect.DeepEqual(*loadClientOauthRes.OAuth.ClientConfig, oauth2.Config{}) && !c.Reset { diff --git a/temporalcloudcli/commands.namespace.go b/temporalcloudcli/commands.namespace.go index 1f69609..419a5c2 100644 --- a/temporalcloudcli/commands.namespace.go +++ b/temporalcloudcli/commands.namespace.go @@ -92,7 +92,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, asyncOp.Id, c.Namespace) } func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error { @@ -199,7 +199,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, asyncOp.Id, namespaceID) } func (c *CloudNamespaceDeleteCommand) run(cctx *CommandContext, _ []string) error { diff --git a/temporalcloudcli/commands.namespace.lifecycle.go b/temporalcloudcli/commands.namespace.lifecycle.go index 956b0d6..9e22a48 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, asyncOp.Id, c.Namespace) } diff --git a/temporalcloudcli/commands.namespace.retention.go b/temporalcloudcli/commands.namespace.retention.go index e28a1a0..c2b3fce 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, asyncOp.Id, c.Namespace) } func (c *CloudNamespaceRetentionGetCommand) run(cctx *CommandContext, _ []string) error {