Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
module github.com/temporalio/cli

go 1.26.3
go 1.26.4

replace (
go.temporal.io/api => ../api-go-saa-batch-cmds
go.temporal.io/sdk => ../sdk-go
go.temporal.io/server => ../orig-temporal
)
Comment on lines +5 to +9

require (
github.com/BurntSushi/toml v1.4.0
Expand All @@ -17,13 +23,13 @@ require (
github.com/stretchr/testify v1.11.1
github.com/temporalio/cli/cliext v0.0.0
github.com/temporalio/ui-server/v2 v2.49.1
go.temporal.io/api v1.62.13
go.temporal.io/api v1.62.15-0.20260615235047-378792ab2240
go.temporal.io/sdk v1.44.1
go.temporal.io/sdk/contrib/envconfig v1.0.2
go.temporal.io/server v1.32.0-157.0
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
golang.org/x/mod v0.35.0
golang.org/x/term v0.42.0
golang.org/x/term v0.43.0
golang.org/x/tools v0.44.0
google.golang.org/grpc v1.81.1
google.golang.org/protobuf v1.36.11
Expand Down Expand Up @@ -201,12 +207,12 @@ require (
go.uber.org/zap v1.27.1 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/api v0.276.0 // indirect
google.golang.org/genproto v0.0.0-20260420184626-e10c466a9529 // indirect
Expand Down
26 changes: 10 additions & 16 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -469,16 +469,10 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.temporal.io/api v1.62.13 h1:xMa8Nt5oAMX+LvlCJA44wjTCc1H09i2rG9poB1/xvH4=
go.temporal.io/api v1.62.13/go.mod h1:0k75tRljEuELWGeXjEZZO7zYqBln4+1FrG6+IMOMy7Q=
go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2 h1:1hKeH3GyR6YD6LKMHGCZ76t6h1Sgha0hXVQBxWi3dlQ=
go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2/go.mod h1:T8dnzVPeO+gaUTj9eDgm/lT2lZH4+JXNvrGaQGyVi50=
go.temporal.io/sdk v1.44.1 h1:Mt2OZLZpqkzDIdg9YyQzO0Rb/HqCDnnqHlIAGAJ5gqM=
go.temporal.io/sdk v1.44.1/go.mod h1:vkApR12F9/Y8OR+hkxe7WyXQFuCX6clhzqnAk6rzDAM=
go.temporal.io/sdk/contrib/envconfig v1.0.2 h1:MGHfsuPUtsf7X9M6WYn3zYJj/mWsuYHnA1uuiL0KEuE=
go.temporal.io/sdk/contrib/envconfig v1.0.2/go.mod h1:MuMiH7hksps2uXnmKuAWaP9P6WbkSDy62kl64t1VJVg=
go.temporal.io/server v1.32.0-157.0 h1:nzFqNwx+5lXsT0/DSiFyR5vHMnDcT3PVAvmRDqCUn38=
go.temporal.io/server v1.32.0-157.0/go.mod h1:a76wf30/s28JXh+3nDQtQi8KzOfRQEddpebvmr/oQL4=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
Expand Down Expand Up @@ -509,8 +503,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
Expand Down Expand Up @@ -543,8 +537,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand All @@ -571,24 +565,24 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
Expand Down
232 changes: 216 additions & 16 deletions internal/temporalcli/commands.activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/fatih/color"
"github.com/google/uuid"
"github.com/temporalio/cli/internal/printer"
activitypb "go.temporal.io/api/activity/v1"
"go.temporal.io/api/batch/v1"
Expand Down Expand Up @@ -40,6 +41,8 @@ type (
}
)

const activityDeleteWarning = "WARNING: Deleting Standalone Activity 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 *TemporalActivityStartCommand) run(cctx *CommandContext, args []string) error {
cl, err := dialClient(cctx, &c.Parent.ClientOptions)
if err != nil {
Expand Down Expand Up @@ -525,21 +528,114 @@ func (c *TemporalActivityCountCommand) run(cctx *CommandContext, args []string)
return nil
}

func (s *ActivityReferenceOrBatchOptions) activityExecOrBatch(
cctx *CommandContext,
namespace string,
cl client.Client,
yesFlag bool,
overrides singleOrBatchOverrides,
) (*client.GetActivityHandleOptions, *workflowservice.StartBatchOperationRequest, error) {
// If activity is set, we return activity handle options with activity ID and run ID
if s.ActivityId != "" {
if s.Query != "" {
return nil, nil, fmt.Errorf("cannot set query when activity ID is set")
} else if yesFlag && !overrides.AllowYesWithActivityID {
return nil, nil, fmt.Errorf("cannot set 'yes' when activity ID is set")
} else if s.Rps != 0 {
return nil, nil, fmt.Errorf("cannot set rps when activity ID is set")
}
return &client.GetActivityHandleOptions{
ActivityID: s.ActivityId,
RunID: s.RunId,
}, nil, nil
}

// Check query is set properly
if s.Query == "" {
return nil, nil, fmt.Errorf("must set either activity ID or query")
} else if s.ActivityId != "" { // This is redundant, but kept for completeness
return nil, nil, fmt.Errorf("cannot set activity ID when query is set")
} else if s.RunId != "" {
return nil, nil, fmt.Errorf("cannot set run ID when query is set")
}

// The count is only used in the confirmation prompt; skip the request when --yes
// bypasses it, so batch jobs can still proceed if the visibility API is timing out.
var promptMessage string
if yesFlag {
promptMessage = fmt.Sprintf("Start batch against standalone activities matching query %q? y/N", s.Query)
} else {
count, err := cl.CountActivities(cctx, client.CountActivitiesOptions{Query: s.Query})
if err != nil {
return nil, nil, fmt.Errorf("failed counting standalone activities from query: %w", err)
}
promptMessage = fmt.Sprintf("Start batch against approximately %v standalone activities(s)? y/N", count.Count)
}
isYes, err := cctx.promptYes(promptMessage, yesFlag)
if err != nil {
return nil, nil, err
} else if !isYes {
// We consider this a command failure
return nil, nil, fmt.Errorf("user denied confirmation")
}

return nil, &workflowservice.StartBatchOperationRequest{
MaxOperationsPerSecond: s.Rps,
Namespace: namespace,
JobId: uuid.NewString(),
VisibilityQuery: s.Query,
}, nil
}

func (c *TemporalActivityCancelCommand) run(cctx *CommandContext, args []string) error {
cl, err := dialClient(cctx, &c.Parent.ClientOptions)
if err != nil {
return err
}
defer cl.Close()

handle := cl.GetActivityHandle(client.GetActivityHandleOptions{
ActivityID: c.ActivityId,
RunID: c.RunId,
})
if err := handle.Cancel(cctx, client.CancelActivityOptions{Reason: c.Reason}); err != nil {
return fmt.Errorf("failed to request activity cancellation: %w", err)
opts := ActivityReferenceOrBatchOptions{
ActivityId: c.ActivityId,
RunId: c.RunId,
Query: c.Query,
Rps: c.Rps,
}

// TODO: should --yes be required if -o json or jsonl is used?

activityOptions, batchReq, err := opts.activityExecOrBatch(cctx, c.Parent.Namespace, cl, c.Yes, singleOrBatchOverrides{})
if err != nil {
return err
}

if activityOptions != nil {
handle := cl.GetActivityHandle(*activityOptions)
if err := handle.Cancel(cctx, client.CancelActivityOptions{Reason: c.Reason}); err != nil {
return fmt.Errorf("failed to request activity cancellation: %w", err)
}
cctx.Printer.Println("Cancellation requested")
} else { // batchReq != nil
cancelActivitiesOperation := &batch.BatchOperationCancelActivities{
Identity: c.Parent.Identity,
// do not fallback to defaultReason, to be consistent with single activity cancel
Reason: c.Reason,
}

batchReq.Operation = &workflowservice.StartBatchOperationRequest_CancelActivitiesOperation{
CancelActivitiesOperation: cancelActivitiesOperation,
}

// Reason in batch request falls back to defaultReason
if c.Reason != "" {
batchReq.Reason = c.Reason
} else {
batchReq.Reason = defaultReason()
}

if err := startBatchJob(cctx, cl, batchReq); err != nil {
return err
}
}
cctx.Printer.Println("Cancellation requested")
return nil
}

Expand All @@ -550,24 +646,128 @@ func (c *TemporalActivityTerminateCommand) run(cctx *CommandContext, args []stri
}
defer cl.Close()

// The CLI adds a default for terminate but not cancel.
// This matches the behavior for workflows.
opts := ActivityReferenceOrBatchOptions{
ActivityId: c.ActivityId,
RunId: c.RunId,
Query: c.Query,
Rps: c.Rps,
}

activityOptions, batchReq, err := opts.activityExecOrBatch(cctx, c.Parent.Namespace, cl, c.Yes, singleOrBatchOverrides{})
if err != nil {
return err
}

// Reason for single terminate or batch request falls back to defaultReason
reason := c.Reason
if reason == "" {
reason = defaultReason()
}
handle := cl.GetActivityHandle(client.GetActivityHandleOptions{
ActivityID: c.ActivityId,
RunID: c.RunId,

if activityOptions != nil {
// The CLI adds a default for terminate but not cancel.
// This matches the behavior for workflows.
handle := cl.GetActivityHandle(*activityOptions)
// Terminate may fail if the activity doesn't exist or has already completed.
if err := handle.Terminate(cctx, client.TerminateActivityOptions{Reason: reason}); err != nil {
return fmt.Errorf("failed to terminate activity: %w", err)
}
cctx.Printer.Println("Activity terminated")
} else { // batchReq != nil
terminateActivitiesOperation := &batch.BatchOperationTerminateActivities{
Identity: c.Parent.Identity,
Reason: reason,
}

batchReq.Reason = reason

batchReq.Operation = &workflowservice.StartBatchOperationRequest_TerminateActivitiesOperation{
TerminateActivitiesOperation: terminateActivitiesOperation,
}

if err := startBatchJob(cctx, cl, batchReq); err != nil {
return err
}
}

return nil
}

func (c *TemporalActivityDeleteCommand) run(cctx *CommandContext, args []string) error {
cl, err := dialClient(cctx, &c.Parent.ClientOptions)
if err != nil {
return err
}
defer cl.Close()

// TODO: do we need this warning, similar to workflow delete?
// Only warn when the namespace is global, or can't get the namespace info
nsResp, nsErr := cl.WorkflowService().DescribeNamespace(cctx, &workflowservice.DescribeNamespaceRequest{
Namespace: c.Parent.Namespace,
})
// Terminate may fail if the activity doesn't exist or has already completed.
if err := handle.Terminate(cctx, client.TerminateActivityOptions{Reason: reason}); err != nil {
return fmt.Errorf("failed to terminate activity: %w", err)
if nsErr != nil || nsResp.GetIsGlobalNamespace() {
fmt.Fprintln(cctx.Options.Stderr, activityDeleteWarning)
Comment thread
ks-temporal marked this conversation as resolved.
}
cctx.Printer.Println("Activity terminated")

opts := ActivityReferenceOrBatchOptions{
ActivityId: c.ActivityId,
RunId: c.RunId,
Query: c.Query,
Rps: c.Rps,
}

activityOptions, batchReq, err := opts.activityExecOrBatch(cctx, c.Parent.Namespace, cl, c.Yes, singleOrBatchOverrides{
AllowYesWithActivityID: true,
})
if err != nil {
return err
}

if activityOptions != nil {
yes, err := cctx.promptYes(activityDeleteSingleConfirmationMessage(activityOptions), c.Yes)
if err != nil {
return err
} else if !yes {
return fmt.Errorf("user denied confirmation")
}
_, err = cl.WorkflowService().DeleteActivityExecution(cctx, &workflowservice.DeleteActivityExecutionRequest{
Namespace: c.Parent.Namespace,
ActivityId: c.ActivityId,
RunId: c.RunId,
})
if err != nil {
return fmt.Errorf("failed to delete standalone activity: %w", err)
}
cctx.Printer.Println("Delete activity succeeded")
} else { // batchReq != nil
deleteActivitiesOperation := &batch.BatchOperationDeleteActivities{}

if c.Reason != "" {
batchReq.Reason = c.Reason
} else {
batchReq.Reason = defaultReason()
}

batchReq.Operation = &workflowservice.StartBatchOperationRequest_DeleteActivitiesOperation{
DeleteActivitiesOperation: deleteActivitiesOperation,
}

if err := startBatchJob(cctx, cl, batchReq); err != nil {
return err
}
}

return nil
}

func activityDeleteSingleConfirmationMessage(activityOptions *client.GetActivityHandleOptions) string {
action := fmt.Sprintf("Delete Standalone Activity %q", activityOptions.ActivityID)
if activityOptions.RunID != "" {
action += fmt.Sprintf(" with Run ID %q", activityOptions.RunID)
}
return fmt.Sprintf("%s? y/N", action)
}

func (c *TemporalActivityCompleteCommand) run(cctx *CommandContext, args []string) error {
cl, err := dialClient(cctx, &c.Parent.ClientOptions)
if err != nil {
Expand Down
Loading
Loading