From 555d51cffbfa8201a9c640fc16047710051dd917 Mon Sep 17 00:00:00 2001 From: jiechenz Date: Mon, 18 May 2026 12:25:32 -0700 Subject: [PATCH 1/3] Add warning message to workflow deleteion --- internal/temporalcli/commands.gen.go | 4 +- internal/temporalcli/commands.workflow.go | 31 ++++++++++++-- .../temporalcli/commands.workflow_test.go | 40 +++++++++++++++++++ internal/temporalcli/commands.yaml | 7 +++- 4 files changed, 76 insertions(+), 6 deletions(-) diff --git a/internal/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go index 08a68010e..03424647a 100644 --- a/internal/temporalcli/commands.gen.go +++ b/internal/temporalcli/commands.gen.go @@ -3756,9 +3756,9 @@ func NewTemporalWorkflowDeleteCommand(cctx *CommandContext, parent *TemporalWork s.Command.Use = "delete [flags]" s.Command.Short = "Remove Workflow Execution" if hasHighlighting { - s.Command.Long = "Delete a Workflow Executions and its Event History:\n\n\x1b[1mtemporal workflow delete \\\n --workflow-id YourWorkflowId\x1b[0m\n\nThe removal executes asynchronously. If the Execution is Running, the Service\nterminates it before deletion.\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See \x1b[1mtemporal batch --help\x1b[0m for a quick reference." + s.Command.Long = "Delete a Workflow Execution and its Event History:\n\n\x1b[1mtemporal workflow delete \\\n --workflow-id YourWorkflowId\x1b[0m\n\nThe removal executes asynchronously. If the Execution is Running, the Service\nterminates it before deletion.\n\nWARNING: Deleting Workflow Executions in a global Namespace removes them from\nall replicas. Requests sent to a passive cluster are forwarded to the active\ncluster by default; to target the passive cluster directly, specify\n\x1b[1m--grpc-meta xdc-redirection=false\x1b[0m.\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See \x1b[1mtemporal batch --help\x1b[0m for a quick reference." } else { - s.Command.Long = "Delete a Workflow Executions and its Event History:\n\n```\ntemporal workflow delete \\\n --workflow-id YourWorkflowId\n```\n\nThe removal executes asynchronously. If the Execution is Running, the Service\nterminates it before deletion.\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See `temporal batch --help` for a quick reference." + s.Command.Long = "Delete a Workflow Execution and its Event History:\n\n```\ntemporal workflow delete \\\n --workflow-id YourWorkflowId\n```\n\nThe removal executes asynchronously. If the Execution is Running, the Service\nterminates it before deletion.\n\nWARNING: Deleting Workflow Executions in a global Namespace removes them from\nall replicas. Requests sent to a passive cluster are forwarded to the active\ncluster by default; to target the passive cluster directly, specify\n`--grpc-meta xdc-redirection=false`.\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See `temporal batch --help` for a quick reference." } s.Command.Args = cobra.NoArgs s.SingleWorkflowOrBatchOptions.BuildFlags(s.Command.Flags()) diff --git a/internal/temporalcli/commands.workflow.go b/internal/temporalcli/commands.workflow.go index 5a98d6ffe..62cc9d728 100644 --- a/internal/temporalcli/commands.workflow.go +++ b/internal/temporalcli/commands.workflow.go @@ -30,6 +30,8 @@ import ( const metadataQueryName = "__temporal_workflow_metadata" +const workflowDeleteWarning = "WARNING: Deleting Workflow Executions in a global Namespace removes them from all replicas. Requests sent to a passive cluster are forwarded to the active cluster by default; to target the passive cluster directly, specify `--grpc-meta xdc-redirection=false`." + func (c *TemporalWorkflowCancelCommand) run(cctx *CommandContext, args []string) error { cl, err := dialClient(cctx, &c.Parent.ClientOptions) if err != nil { @@ -69,13 +71,23 @@ func (c *TemporalWorkflowDeleteCommand) run(cctx *CommandContext, args []string) } defer cl.Close() - exec, batchReq, err := c.workflowExecOrBatch(cctx, c.Parent.Namespace, cl, singleOrBatchOverrides{}) + cctx.Printer.Println(workflowDeleteWarning) + + exec, batchReq, err := c.workflowExecOrBatch(cctx, c.Parent.Namespace, cl, singleOrBatchOverrides{ + AllowYesWithWorkflowID: true, + }) // Run single or batch if err != nil { return err } else if exec != nil { - _, err := cl.WorkflowService().DeleteWorkflowExecution(cctx, &workflowservice.DeleteWorkflowExecutionRequest{ + yes, err := cctx.promptYes(workflowDeleteSingleConfirmationMessage(exec), c.Yes) + if err != nil { + return err + } else if !yes { + return fmt.Errorf("user denied confirmation") + } + _, err = cl.WorkflowService().DeleteWorkflowExecution(cctx, &workflowservice.DeleteWorkflowExecutionRequest{ Namespace: c.Parent.Namespace, WorkflowExecution: &common.WorkflowExecution{WorkflowId: c.WorkflowId, RunId: c.RunId}, }) @@ -96,6 +108,18 @@ func (c *TemporalWorkflowDeleteCommand) run(cctx *CommandContext, args []string) return nil } +func workflowDeleteConfirmationMessage(action string) string { + return fmt.Sprintf("%s? y/N", action) +} + +func workflowDeleteSingleConfirmationMessage(exec *common.WorkflowExecution) string { + action := fmt.Sprintf("Delete Workflow %q", exec.GetWorkflowId()) + if exec.GetRunId() != "" { + action += fmt.Sprintf(" with Run ID %q", exec.GetRunId()) + } + return workflowDeleteConfirmationMessage(action) +} + func (c *TemporalWorkflowUpdateOptionsCommand) run(cctx *CommandContext, args []string) error { cl, err := dialClient(cctx, &c.Parent.ClientOptions) if err != nil { @@ -511,6 +535,7 @@ func defaultReason() string { type singleOrBatchOverrides struct { AllowReasonWithWorkflowID bool + AllowYesWithWorkflowID bool } func (s *SingleWorkflowOrBatchOptions) workflowExecOrBatch( @@ -525,7 +550,7 @@ func (s *SingleWorkflowOrBatchOptions) workflowExecOrBatch( return nil, nil, fmt.Errorf("cannot set query when workflow ID is set") } else if s.Reason != "" && !overrides.AllowReasonWithWorkflowID { return nil, nil, fmt.Errorf("cannot set reason when workflow ID is set") - } else if s.Yes { + } else if s.Yes && !overrides.AllowYesWithWorkflowID { return nil, nil, fmt.Errorf("cannot set 'yes' when workflow ID is set") } else if s.Rps != 0 { return nil, nil, fmt.Errorf("cannot set rps when workflow ID is set") diff --git a/internal/temporalcli/commands.workflow_test.go b/internal/temporalcli/commands.workflow_test.go index 5a11f3627..001167e63 100644 --- a/internal/temporalcli/commands.workflow_test.go +++ b/internal/temporalcli/commands.workflow_test.go @@ -169,6 +169,43 @@ func (s *SharedServerSuite) testSignalBatchWorkflow(json bool) *CommandResult { return res } +func (s *SharedServerSuite) TestWorkflow_Delete_SingleWorkflowRequiresConfirmation() { + res := s.Execute( + "workflow", "delete", + "--address", s.Address(), + "--workflow-id", "delete-confirmation-test", + ) + s.EqualError(res.Err, "user denied confirmation") + s.Contains(res.Stdout.String(), "Deleting Workflow Executions in a global Namespace") + s.Contains(res.Stdout.String(), "--grpc-meta xdc-redirection=false") +} + +func (s *SharedServerSuite) TestWorkflow_Delete_SingleWorkflowSuccess() { + s.Worker().OnDevWorkflow(func(ctx workflow.Context, a any) (any, error) { + ctx.Done().Receive(ctx, nil) + return nil, ctx.Err() + }) + + run, err := s.Client.ExecuteWorkflow( + s.Context, + client.StartWorkflowOptions{TaskQueue: s.Worker().Options.TaskQueue}, + DevWorkflow, + "ignored", + ) + s.NoError(err) + + res := s.Execute( + "workflow", "delete", + "--address", s.Address(), + "--workflow-id", run.GetID(), + "--yes", + ) + s.NoError(res.Err) + s.Contains(res.Stdout.String(), "Deleting Workflow Executions in a global Namespace") + s.Contains(res.Stdout.String(), "--grpc-meta xdc-redirection=false") + s.Contains(res.Stdout.String(), "Delete workflow succeeded") +} + func (s *SharedServerSuite) TestWorkflow_Delete_BatchWorkflowSuccess() { s.Worker().OnDevWorkflow(func(ctx workflow.Context, a any) (any, error) { ctx.Done().Receive(ctx, nil) @@ -212,6 +249,9 @@ func (s *SharedServerSuite) TestWorkflow_Delete_BatchWorkflowSuccess() { "-y", ) s.NoError(res.Err) + s.Contains(res.Stdout.String(), "Start batch against approximately 2 workflow(s)") + s.Contains(res.Stdout.String(), "Deleting Workflow Executions in a global Namespace") + s.Contains(res.Stdout.String(), "--grpc-meta xdc-redirection=false") // Confirm workflows were deleted s.Eventually(func() bool { diff --git a/internal/temporalcli/commands.yaml b/internal/temporalcli/commands.yaml index 32af0d955..78bdf3513 100644 --- a/internal/temporalcli/commands.yaml +++ b/internal/temporalcli/commands.yaml @@ -3665,7 +3665,7 @@ commands: - name: temporal workflow delete summary: Remove Workflow Execution description: | - Delete a Workflow Executions and its Event History: + Delete a Workflow Execution and its Event History: ``` temporal workflow delete \ @@ -3675,6 +3675,11 @@ commands: The removal executes asynchronously. If the Execution is Running, the Service terminates it before deletion. + WARNING: Deleting Workflow Executions in a global Namespace removes them from + all replicas. Requests sent to a passive cluster are forwarded to the active + cluster by default; to target the passive cluster directly, specify + `--grpc-meta xdc-redirection=false`. + Visit https://docs.temporal.io/visibility to read more about Search Attributes and Query creation. See `temporal batch --help` for a quick reference. option-sets: From 616179f8c49add7a3ae9768427ad571b6eb38ee9 Mon Sep 17 00:00:00 2001 From: jiechenz Date: Mon, 18 May 2026 17:08:59 -0700 Subject: [PATCH 2/3] address comments --- internal/temporalcli/commands.workflow.go | 14 ++++++++------ internal/temporalcli/commands.workflow_test.go | 13 +++++++------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/internal/temporalcli/commands.workflow.go b/internal/temporalcli/commands.workflow.go index 62cc9d728..20ebec8d1 100644 --- a/internal/temporalcli/commands.workflow.go +++ b/internal/temporalcli/commands.workflow.go @@ -71,7 +71,13 @@ func (c *TemporalWorkflowDeleteCommand) run(cctx *CommandContext, args []string) } defer cl.Close() - cctx.Printer.Println(workflowDeleteWarning) + // Only warn when the namespace is global. + nsResp, nsErr := cl.WorkflowService().DescribeNamespace(cctx, &workflowservice.DescribeNamespaceRequest{ + Namespace: c.Parent.Namespace, + }) + if nsErr != nil || nsResp.GetIsGlobalNamespace() { + fmt.Fprintln(cctx.Options.Stderr, workflowDeleteWarning) + } exec, batchReq, err := c.workflowExecOrBatch(cctx, c.Parent.Namespace, cl, singleOrBatchOverrides{ AllowYesWithWorkflowID: true, @@ -108,16 +114,12 @@ func (c *TemporalWorkflowDeleteCommand) run(cctx *CommandContext, args []string) return nil } -func workflowDeleteConfirmationMessage(action string) string { - return fmt.Sprintf("%s? y/N", action) -} - func workflowDeleteSingleConfirmationMessage(exec *common.WorkflowExecution) string { action := fmt.Sprintf("Delete Workflow %q", exec.GetWorkflowId()) if exec.GetRunId() != "" { action += fmt.Sprintf(" with Run ID %q", exec.GetRunId()) } - return workflowDeleteConfirmationMessage(action) + return fmt.Sprintf("%s? y/N", action) } func (c *TemporalWorkflowUpdateOptionsCommand) run(cctx *CommandContext, args []string) error { diff --git a/internal/temporalcli/commands.workflow_test.go b/internal/temporalcli/commands.workflow_test.go index 001167e63..20d6d256b 100644 --- a/internal/temporalcli/commands.workflow_test.go +++ b/internal/temporalcli/commands.workflow_test.go @@ -176,8 +176,9 @@ func (s *SharedServerSuite) TestWorkflow_Delete_SingleWorkflowRequiresConfirmati "--workflow-id", "delete-confirmation-test", ) s.EqualError(res.Err, "user denied confirmation") - s.Contains(res.Stdout.String(), "Deleting Workflow Executions in a global Namespace") - s.Contains(res.Stdout.String(), "--grpc-meta xdc-redirection=false") + // Dev-server's default namespace is non-global, so the warning is suppressed. + s.NotContains(res.Stdout.String(), "Deleting Workflow Executions in a global Namespace") + s.NotContains(res.Stderr.String(), "Deleting Workflow Executions in a global Namespace") } func (s *SharedServerSuite) TestWorkflow_Delete_SingleWorkflowSuccess() { @@ -201,8 +202,8 @@ func (s *SharedServerSuite) TestWorkflow_Delete_SingleWorkflowSuccess() { "--yes", ) s.NoError(res.Err) - s.Contains(res.Stdout.String(), "Deleting Workflow Executions in a global Namespace") - s.Contains(res.Stdout.String(), "--grpc-meta xdc-redirection=false") + s.NotContains(res.Stdout.String(), "Deleting Workflow Executions in a global Namespace") + s.NotContains(res.Stderr.String(), "Deleting Workflow Executions in a global Namespace") s.Contains(res.Stdout.String(), "Delete workflow succeeded") } @@ -250,8 +251,8 @@ func (s *SharedServerSuite) TestWorkflow_Delete_BatchWorkflowSuccess() { ) s.NoError(res.Err) s.Contains(res.Stdout.String(), "Start batch against approximately 2 workflow(s)") - s.Contains(res.Stdout.String(), "Deleting Workflow Executions in a global Namespace") - s.Contains(res.Stdout.String(), "--grpc-meta xdc-redirection=false") + s.NotContains(res.Stdout.String(), "Deleting Workflow Executions in a global Namespace") + s.NotContains(res.Stderr.String(), "Deleting Workflow Executions in a global Namespace") // Confirm workflows were deleted s.Eventually(func() bool { From e489a175b307c95a4117ba1f330a665d5fabcdfb Mon Sep 17 00:00:00 2001 From: jiechenz Date: Mon, 18 May 2026 17:10:45 -0700 Subject: [PATCH 3/3] :boom: workflow delete now prompts for confirmation Single-workflow `temporal workflow delete` now requires interactive confirmation (or `--yes`/`-y`). Scripts that previously relied on the command running non-interactively will break unless they pass `--yes`. Co-Authored-By: Claude Opus 4.7 (1M context)