From c48e6dfe9913b4f40593e20eab2a58017ffdcfd2 Mon Sep 17 00:00:00 2001 From: Sean Kane Date: Tue, 29 Jul 2025 22:25:36 +0200 Subject: [PATCH 01/28] activity: support batch operations in reset and update-options (#831) * The `temporal activity {pause,update-options}` support batch operations through `--query` * Consistently passing the `--identity` parameter to server requests Improved developer experience, can batch these requests through a visibility query instead of sending multiple requests 2. How was this tested: New unit tests 3. Any docs updates needed? Documentation updates are automatically generated with releases. --- go.mod | 9 +- go.sum | 26 ++- temporalcli/client.go | 25 ++- temporalcli/commands.activity.go | 223 ++++++++++++++-------- temporalcli/commands.activity_test.go | 86 ++++++++- temporalcli/commands.batch.go | 2 +- temporalcli/commands.gen.go | 80 ++++---- temporalcli/commands.worker.deployment.go | 8 +- temporalcli/commands.workflow.go | 14 +- temporalcli/commands.workflow_exec.go | 2 +- temporalcli/commands.workflow_reset.go | 2 +- temporalcli/commandsgen/commands.yml | 110 ++++++----- 12 files changed, 383 insertions(+), 204 deletions(-) diff --git a/go.mod b/go.mod index f7d1f9238..26af6c26c 100644 --- a/go.mod +++ b/go.mod @@ -16,11 +16,11 @@ require ( github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 github.com/temporalio/ui-server/v2 v2.39.0 - go.temporal.io/api v1.50.0 + go.temporal.io/api v1.51.0 go.temporal.io/sdk v1.35.0 go.temporal.io/sdk/contrib/envconfig v0.1.0 - go.temporal.io/server v1.28.0 - google.golang.org/grpc v1.71.0 + go.temporal.io/server v1.29.0-135.0.0.20250725000618-7e01f6c035c9 + google.golang.org/grpc v1.72.2 google.golang.org/protobuf v1.36.6 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.34.1 @@ -121,6 +121,7 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/sony/gobreaker v1.0.0 // indirect github.com/spf13/cast v1.7.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7 // indirect github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb // indirect @@ -130,6 +131,7 @@ require ( github.com/uber-go/tally/v4 v4.1.17 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect @@ -144,7 +146,6 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect - go.temporal.io/version v0.3.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/dig v1.18.1 // indirect go.uber.org/fx v1.23.0 // indirect diff --git a/go.sum b/go.sum index a18e8152e..63dc1f148 100644 --- a/go.sum +++ b/go.sum @@ -215,6 +215,8 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -254,6 +256,10 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= @@ -320,6 +326,8 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -360,8 +368,12 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/collector/pdata v1.34.0 h1:2vwYftckXe7pWxI9mfSo+tw3wqdGNrYpMbDx/5q6rw8= +go.opentelemetry.io/collector/pdata v1.34.0/go.mod h1:StPHMFkhLBellRWrULq0DNjv4znCDJZP6La4UuC+JHI= go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= @@ -390,16 +402,14 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= -go.temporal.io/api v1.50.0 h1:7s8Cn+fKfNx9G0v2Ge9We6X2WiCA3JvJ9JryeNbx1Bc= -go.temporal.io/api v1.50.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/api v1.51.0 h1:9+e14GrIa7nWoWoudqj/PSwm33yYjV+u8TAR9If7s/g= +go.temporal.io/api v1.51.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/sdk v1.35.0 h1:lRNAQ5As9rLgYa7HBvnmKyzxLcdElTuoFJ0FXM/AsLQ= go.temporal.io/sdk v1.35.0/go.mod h1:1q5MuLc2MEJ4lneZTHJzpVebW2oZnyxoIOWX3oFVebw= go.temporal.io/sdk/contrib/envconfig v0.1.0 h1:s+G/Ujph+Xl2jzLiiIm2T1vuijDkUL4Kse49dgDVGBE= go.temporal.io/sdk/contrib/envconfig v0.1.0/go.mod h1:FQEO3C56h9C7M6sDgSanB8HnBTmopw9qgVx4F1S6pJk= -go.temporal.io/server v1.28.0 h1:1rLPrT21ZwpsRjElJqSgThj1NZSAtAPyi8nKX+EAkgo= -go.temporal.io/server v1.28.0/go.mod h1:yri8PdZoAtwI9p65hzvABf11WqXelHl/HabbrnJSu+g= -go.temporal.io/version v0.3.0 h1:dMrei9l9NyHt8nG6EB8vAwDLLTwx2SvRyucCSumAiig= -go.temporal.io/version v0.3.0/go.mod h1:UA9S8/1LaKYae6TyD9NaPMJTZb911JcbqghI2CBSP78= +go.temporal.io/server v1.29.0-135.0.0.20250725000618-7e01f6c035c9 h1:jJV/LmX6msjAQj+TrPIM+qVZZMu8EPnLENM4nNiJq9k= +go.temporal.io/server v1.29.0-135.0.0.20250725000618-7e01f6c035c9/go.mod h1:qRq3Ei+nk7eXw+Dw60GaHdCDo7dbbqGa7LnSJqPaIlk= 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= @@ -569,8 +579,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/temporalcli/client.go b/temporalcli/client.go index d03c2277e..cc8c73018 100644 --- a/temporalcli/client.go +++ b/temporalcli/client.go @@ -27,6 +27,18 @@ func (c *ClientOptions) dialClient(cctx *CommandContext) (client.Client, error) return nil, fmt.Errorf("root command unexpectedly missing when dialing client") } + if c.Identity == "" { + hostname, err := os.Hostname() + if err != nil { + hostname = "unknown-host" + } + username := "unknown-user" + if u, err := user.Current(); err == nil { + username = u.Username + } + c.Identity = "temporal-cli:" + username + "@" + hostname + } + // Load a client config profile var clientProfile envconfig.ClientConfigProfile if !cctx.RootCommand.DisableConfigFile || !cctx.RootCommand.DisableConfigEnv { @@ -178,7 +190,6 @@ func (c *ClientOptions) dialClient(cctx *CommandContext) (client.Client, error) } clientOptions.Logger = log.NewStructuredLogger(cctx.Logger) - clientOptions.Identity = clientIdentity() // We do not put codec on data converter here, it is applied via // interceptor. Same for failure conversion. // XXX: If this is altered to be more dynamic, have to also update @@ -270,18 +281,6 @@ func payloadCodecInterceptor( ) } -func clientIdentity() string { - hostname, err := os.Hostname() - if err != nil { - hostname = "unknown-host" - } - username := "unknown-user" - if u, err := user.Current(); err == nil { - username = u.Username - } - return "temporal-cli:" + username + "@" + hostname -} - var DataConverterWithRawValue = converter.NewCompositeDataConverter( rawValuePayloadConverter{}, converter.NewNilPayloadConverter(), diff --git a/temporalcli/commands.activity.go b/temporalcli/commands.activity.go index f2c26b9ca..6940727ae 100644 --- a/temporalcli/commands.activity.go +++ b/temporalcli/commands.activity.go @@ -50,7 +50,7 @@ func (c *TemporalActivityCompleteCommand) run(cctx *CommandContext, args []strin RunId: c.RunId, ActivityId: c.ActivityId, Result: resultPayloads, - Identity: c.Identity, + Identity: c.Parent.Identity, }) if err != nil { return fmt.Errorf("unable to complete Activity: %w", err) @@ -86,7 +86,7 @@ func (c *TemporalActivityFailCommand) run(cctx *CommandContext, args []string) e Details: detailPayloads, }}, }, - Identity: c.Identity, + Identity: c.Parent.Identity, }) if err != nil { return fmt.Errorf("unable to fail Activity: %w", err) @@ -156,39 +156,79 @@ func (c *TemporalActivityUpdateOptionsCommand) run(cctx *CommandContext, args [] updatePath = append(updatePath, "retry_policy.maximum_attempts") } - result, err := cl.WorkflowService().UpdateActivityOptions(cctx, &workflowservice.UpdateActivityOptionsRequest{ - Namespace: c.Parent.Namespace, - Execution: &common.WorkflowExecution{ - WorkflowId: c.WorkflowId, - RunId: c.RunId, - }, - Activity: &workflowservice.UpdateActivityOptionsRequest_Id{Id: c.ActivityId}, - ActivityOptions: activityOptions, - UpdateMask: &fieldmaskpb.FieldMask{ - Paths: updatePath, - }, - Identity: c.Identity, - }) + opts := SingleWorkflowOrBatchOptions{ + WorkflowId: c.WorkflowId, + RunId: c.RunId, + Query: c.Query, + Reason: c.Reason, + Yes: c.Yes, + Rps: c.Rps, + } + + exec, batchReq, err := opts.workflowExecOrBatch(cctx, c.Parent.Namespace, cl, singleOrBatchOverrides{}) if err != nil { - return fmt.Errorf("unable to update Activity options: %w", err) + return err } - updatedOptions := updateOptionsDescribe{ - TaskQueue: result.GetActivityOptions().TaskQueue.GetName(), + if exec != nil { + result, err := cl.WorkflowService().UpdateActivityOptions(cctx, &workflowservice.UpdateActivityOptionsRequest{ + Namespace: c.Parent.Namespace, + Execution: &common.WorkflowExecution{ + WorkflowId: c.WorkflowId, + RunId: c.RunId, + }, + Activity: &workflowservice.UpdateActivityOptionsRequest_Id{Id: c.ActivityId}, + ActivityOptions: activityOptions, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: updatePath, + }, + Identity: c.Parent.Identity, + }) + if err != nil { + return fmt.Errorf("unable to update Activity options: %w", err) + } - ScheduleToCloseTimeout: result.GetActivityOptions().ScheduleToCloseTimeout.AsDuration(), - ScheduleToStartTimeout: result.GetActivityOptions().ScheduleToStartTimeout.AsDuration(), - StartToCloseTimeout: result.GetActivityOptions().StartToCloseTimeout.AsDuration(), - HeartbeatTimeout: result.GetActivityOptions().HeartbeatTimeout.AsDuration(), + updatedOptions := updateOptionsDescribe{ + TaskQueue: result.GetActivityOptions().TaskQueue.GetName(), - InitialInterval: result.GetActivityOptions().RetryPolicy.InitialInterval.AsDuration(), - BackoffCoefficient: result.GetActivityOptions().RetryPolicy.BackoffCoefficient, - MaximumInterval: result.GetActivityOptions().RetryPolicy.MaximumInterval.AsDuration(), - MaximumAttempts: result.GetActivityOptions().RetryPolicy.MaximumAttempts, - } + ScheduleToCloseTimeout: result.GetActivityOptions().ScheduleToCloseTimeout.AsDuration(), + ScheduleToStartTimeout: result.GetActivityOptions().ScheduleToStartTimeout.AsDuration(), + StartToCloseTimeout: result.GetActivityOptions().StartToCloseTimeout.AsDuration(), + HeartbeatTimeout: result.GetActivityOptions().HeartbeatTimeout.AsDuration(), - _ = cctx.Printer.PrintStructured(updatedOptions, printer.StructuredOptions{}) + InitialInterval: result.GetActivityOptions().RetryPolicy.InitialInterval.AsDuration(), + BackoffCoefficient: result.GetActivityOptions().RetryPolicy.BackoffCoefficient, + MaximumInterval: result.GetActivityOptions().RetryPolicy.MaximumInterval.AsDuration(), + MaximumAttempts: result.GetActivityOptions().RetryPolicy.MaximumAttempts, + } + _ = cctx.Printer.PrintStructured(updatedOptions, printer.StructuredOptions{}) + } else { + updateActivitiesOperation := &batch.BatchOperationUpdateActivityOptions{ + Identity: c.Parent.Identity, + Activity: &batch.BatchOperationUpdateActivityOptions_Type{Type: c.ActivityType}, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: updatePath, + }, + RestoreOriginal: c.RestoreOriginalOptions, + } + + if c.ActivityType != "" { + updateActivitiesOperation.Activity = &batch.BatchOperationUpdateActivityOptions_Type{Type: c.ActivityType} + } else if c.MatchAll { + updateActivitiesOperation.Activity = &batch.BatchOperationUpdateActivityOptions_MatchAll{MatchAll: true} + } else { + return fmt.Errorf("either Activity Type must be provided or MatchAll must be set to true") + } + + batchReq.Operation = &workflowservice.StartBatchOperationRequest_UpdateActivityOptionsOperation{ + UpdateActivityOptionsOperation: updateActivitiesOperation, + } + + if err := startBatchJob(cctx, cl, batchReq); err != nil { + return err + } + } return nil } @@ -205,19 +245,15 @@ func (c *TemporalActivityPauseCommand) run(cctx *CommandContext, args []string) WorkflowId: c.WorkflowId, RunId: c.RunId, }, - Identity: c.Identity, + Identity: c.Parent.Identity, } if c.ActivityId != "" && c.ActivityType != "" { return fmt.Errorf("either Activity Type or Activity Id, but not both") - } - - if c.ActivityType != "" { + } else if c.ActivityType != "" { request.Activity = &workflowservice.PauseActivityRequest_Type{Type: c.ActivityType} } else if c.ActivityId != "" { request.Activity = &workflowservice.PauseActivityRequest_Id{Id: c.ActivityId} - } else { - return fmt.Errorf("either Activity Type or Activity Id must be provided") } _, err = cl.WorkflowService().PauseActivity(cctx, request) @@ -244,10 +280,7 @@ func (c *TemporalActivityUnpauseCommand) run(cctx *CommandContext, args []string Rps: c.Rps, } - exec, batchReq, err := opts.workflowExecOrBatch(cctx, c.Parent.Namespace, cl, singleOrBatchOverrides{ - // You're allowed to specify a reason when terminating a workflow - AllowReasonWithWorkflowID: true, - }) + exec, batchReq, err := opts.workflowExecOrBatch(cctx, c.Parent.Namespace, cl, singleOrBatchOverrides{}) if err != nil { return err } @@ -262,35 +295,31 @@ func (c *TemporalActivityUnpauseCommand) run(cctx *CommandContext, args []string ResetAttempts: c.ResetAttempts, ResetHeartbeat: c.ResetHeartbeats, Jitter: durationpb.New(c.Jitter.Duration()), - Identity: c.Identity, + Identity: c.Parent.Identity, } if c.ActivityId != "" && c.ActivityType != "" { return fmt.Errorf("either Activity Type or Activity Id, but not both") - } - - if c.ActivityType != "" { + } else if c.ActivityType != "" { request.Activity = &workflowservice.UnpauseActivityRequest_Type{Type: c.ActivityType} } else if c.ActivityId != "" { request.Activity = &workflowservice.UnpauseActivityRequest_Id{Id: c.ActivityId} - } else { - return fmt.Errorf("either Activity Type or Activity Id must be provided") } _, err = cl.WorkflowService().UnpauseActivity(cctx, request) if err != nil { - return fmt.Errorf("unable to uppause an Activity: %w", err) + return fmt.Errorf("unable to unpause an Activity: %w", err) } } else { // batch operation unpauseActivitiesOperation := &batch.BatchOperationUnpauseActivities{ - Identity: clientIdentity(), + Identity: c.Parent.Identity, ResetAttempts: c.ResetAttempts, ResetHeartbeat: c.ResetHeartbeats, Jitter: durationpb.New(c.Jitter.Duration()), } if c.ActivityType != "" { unpauseActivitiesOperation.Activity = &batch.BatchOperationUnpauseActivities_Type{Type: c.ActivityType} - } else if c.MatchAll == true { + } else if c.MatchAll { unpauseActivitiesOperation.Activity = &batch.BatchOperationUnpauseActivities_MatchAll{MatchAll: true} } else { return fmt.Errorf("either Activity Type must be provided or MatchAll must be set to true") @@ -315,45 +344,81 @@ func (c *TemporalActivityResetCommand) run(cctx *CommandContext, args []string) } defer cl.Close() - request := &workflowservice.ResetActivityRequest{ - Namespace: c.Parent.Namespace, - Execution: &common.WorkflowExecution{ - WorkflowId: c.WorkflowId, - RunId: c.RunId, - }, - Identity: c.Identity, - KeepPaused: c.KeepPaused, - ResetHeartbeat: c.ResetHeartbeats, + opts := SingleWorkflowOrBatchOptions{ + WorkflowId: c.WorkflowId, + RunId: c.RunId, + Query: c.Query, + Reason: c.Reason, + Yes: c.Yes, + Rps: c.Rps, } - if c.ActivityId != "" && c.ActivityType != "" { - return fmt.Errorf("either Activity Type or Activity Id, but not both") + exec, batchReq, err := opts.workflowExecOrBatch(cctx, c.Parent.Namespace, cl, singleOrBatchOverrides{}) + if err != nil { + return err } - if c.ActivityType != "" { - request.Activity = &workflowservice.ResetActivityRequest_Type{Type: c.ActivityType} - } else if c.ActivityId != "" { - request.Activity = &workflowservice.ResetActivityRequest_Id{Id: c.ActivityId} - } else { - return fmt.Errorf("either Activity Type or Activity Id must be provided") - } + if exec != nil { // single workflow operation + request := &workflowservice.ResetActivityRequest{ + Namespace: c.Parent.Namespace, + Execution: &common.WorkflowExecution{ + WorkflowId: c.WorkflowId, + RunId: c.RunId, + }, + Identity: c.Parent.Identity, + KeepPaused: c.KeepPaused, + ResetHeartbeat: c.ResetHeartbeats, + } - resp, err := cl.WorkflowService().ResetActivity(cctx, request) - if err != nil { - return fmt.Errorf("unable to reset an Activity: %w", err) - } + if c.ActivityId != "" && c.ActivityType != "" { + return fmt.Errorf("either Activity Type or Activity Id, but not both") + } else if c.ActivityType != "" { + request.Activity = &workflowservice.ResetActivityRequest_Type{Type: c.ActivityType} + } else if c.ActivityId != "" { + request.Activity = &workflowservice.ResetActivityRequest_Id{Id: c.ActivityId} + } - resetResponse := struct { - KeepPaused bool `json:"keepPaused"` - ResetHeartbeats bool `json:"resetHeartbeats"` - ServerResponse bool `json:"-"` - }{ - ServerResponse: resp != nil, - KeepPaused: c.KeepPaused, - ResetHeartbeats: c.ResetHeartbeats, - } + resp, err := cl.WorkflowService().ResetActivity(cctx, request) + if err != nil { + return fmt.Errorf("unable to reset an Activity: %w", err) + } - _ = cctx.Printer.PrintStructured(resetResponse, printer.StructuredOptions{}) + resetResponse := struct { + KeepPaused bool `json:"keepPaused"` + ResetHeartbeats bool `json:"resetHeartbeats"` + ServerResponse bool `json:"-"` + }{ + ServerResponse: resp != nil, + KeepPaused: c.KeepPaused, + ResetHeartbeats: c.ResetHeartbeats, + } + + _ = cctx.Printer.PrintStructured(resetResponse, printer.StructuredOptions{}) + } else { // batch operation + resetActivitiesOperation := &batch.BatchOperationResetActivities{ + Identity: c.Parent.Identity, + ResetAttempts: c.ResetAttempts, + ResetHeartbeat: c.ResetHeartbeats, + KeepPaused: c.KeepPaused, + Jitter: durationpb.New(c.Jitter.Duration()), + RestoreOriginalOptions: c.RestoreOriginalOptions, + } + if c.ActivityType != "" { + resetActivitiesOperation.Activity = &batch.BatchOperationResetActivities_Type{Type: c.ActivityType} + } else if c.MatchAll { + resetActivitiesOperation.Activity = &batch.BatchOperationResetActivities_MatchAll{MatchAll: true} + } else { + return fmt.Errorf("either Activity Type must be provided or MatchAll must be set to true") + } + + batchReq.Operation = &workflowservice.StartBatchOperationRequest_ResetActivitiesOperation{ + ResetActivitiesOperation: resetActivitiesOperation, + } + + if err := startBatchJob(cctx, cl, batchReq); err != nil { + return err + } + } return nil } diff --git a/temporalcli/commands.activity_test.go b/temporalcli/commands.activity_test.go index b56dc3130..6ce214472 100644 --- a/temporalcli/commands.activity_test.go +++ b/temporalcli/commands.activity_test.go @@ -356,7 +356,6 @@ func (s *SharedServerSuite) TestUnpauseActivity_BatchSuccess() { var failActivity atomic.Bool failActivity.Store(true) s.Worker().OnDevActivity(func(ctx context.Context, a any) (any, error) { - time.Sleep(100 * time.Millisecond) if failActivity.Load() { return nil, fmt.Errorf("update workflow received non-float input") } @@ -436,3 +435,88 @@ func (s *SharedServerSuite) TestUnpauseActivity_BatchSuccess() { // unblock the activities to let them finish failActivity.Store(false) } + + +func (s *SharedServerSuite) TestResetActivity_BatchSuccess() { + var failActivity atomic.Bool + failActivity.Store(true) + s.Worker().OnDevActivity(func(ctx context.Context, a any) (any, error) { + if failActivity.Load() { + return nil, fmt.Errorf("update workflow received non-float input") + } + return nil, nil + }) + + s.Worker().OnDevWorkflow(func(ctx workflow.Context, a any) (any, error) { + // override the activity options to allow activity to constantly fail + ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ + ActivityID: activityId, + StartToCloseTimeout: 1 * time.Minute, + RetryPolicy: &temporal.RetryPolicy{ + MaximumAttempts: 0, + }, + }) + var res any + err := workflow.ExecuteActivity(ctx, DevActivity).Get(ctx, &res) + return res, err + }) + + run1 := waitWorkflowStarted(s) + run2 := waitWorkflowStarted(s) + + // Wait for all to appear in list + query := fmt.Sprintf("WorkflowId = '%s' OR WorkflowId = '%s'", run1.GetID(), run2.GetID()) + s.Eventually(func() bool { + resp, err := s.Client.ListWorkflow(s.Context, &workflowservice.ListWorkflowExecutionsRequest{ + Query: query, + }) + s.NoError(err) + return len(resp.Executions) == 2 + }, 3*time.Second, 100*time.Millisecond) + + // Pause the activities + res := sendActivityCommand("pause", run1, s, "--activity-id", activityId) + s.NoError(res.Err) + res = sendActivityCommand("pause", run2, s, "--activity-id", activityId) + s.NoError(res.Err) + + // wait for activities to be paused + checkActivitiesPaused(s, run1) + checkActivitiesPaused(s, run2) + + var lastRequestLock sync.Mutex + var startBatchRequest *workflowservice.StartBatchOperationRequest + s.CommandHarness.Options.AdditionalClientGRPCDialOptions = append( + s.CommandHarness.Options.AdditionalClientGRPCDialOptions, + grpc.WithChainUnaryInterceptor(func( + ctx context.Context, + method string, req, reply any, + cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption, + ) error { + lastRequestLock.Lock() + if r, ok := req.(*workflowservice.StartBatchOperationRequest); ok { + startBatchRequest = r + } + lastRequestLock.Unlock() + return invoker(ctx, method, req, reply, cc, opts...) + }), + ) + + // Send reset activity unpause + cmdRes := s.Execute("activity", "reset", + "--rps", "1", + "--address", s.Address(), + "--query", query, + "--reason", "unpause-test", + "--yes", "--match-all", + ) + s.NoError(cmdRes.Err) + s.NotEmpty(startBatchRequest.JobId) + + // check activities are running + checkActivitiesRunning(s, run1) + checkActivitiesRunning(s, run2) + + // unblock the activities to let them finish + failActivity.Store(false) +} diff --git a/temporalcli/commands.batch.go b/temporalcli/commands.batch.go index 1f0a1e2c0..93686732c 100644 --- a/temporalcli/commands.batch.go +++ b/temporalcli/commands.batch.go @@ -143,7 +143,7 @@ func (c TemporalBatchTerminateCommand) run(cctx *CommandContext, args []string) Namespace: c.Parent.Namespace, JobId: c.JobId, Reason: c.Reason, - Identity: clientIdentity(), + Identity: c.Parent.Identity, }) var notFound *serviceerror.NotFound diff --git a/temporalcli/commands.gen.go b/temporalcli/commands.gen.go index 4b5d3328b..d88b740e5 100644 --- a/temporalcli/commands.gen.go +++ b/temporalcli/commands.gen.go @@ -34,6 +34,7 @@ type ClientOptions struct { CodecEndpoint string CodecAuth string CodecHeader []string + Identity string } func (v *ClientOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSet) { @@ -54,6 +55,7 @@ func (v *ClientOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSet) { f.StringVar(&v.CodecEndpoint, "codec-endpoint", "", "Remote Codec Server endpoint.") f.StringVar(&v.CodecAuth, "codec-auth", "", "Authorization header for Codec Server requests.") f.StringArrayVar(&v.CodecHeader, "codec-header", nil, "HTTP headers for requests to codec server. Format as a `KEY=VALUE` pair. May be passed multiple times to set multiple headers.") + f.StringVar(&v.Identity, "identity", "", "The identity of the user or client submitting this request. Defaults to \"temporal-cli:$USER@$HOST\".") } type OverlapPolicyOptions struct { @@ -427,7 +429,6 @@ type TemporalActivityCompleteCommand struct { WorkflowReferenceOptions ActivityId string Result string - Identity string } func NewTemporalActivityCompleteCommand(cctx *CommandContext, parent *TemporalActivityCommand) *TemporalActivityCompleteCommand { @@ -446,7 +447,6 @@ func NewTemporalActivityCompleteCommand(cctx *CommandContext, parent *TemporalAc _ = cobra.MarkFlagRequired(s.Command.Flags(), "activity-id") s.Command.Flags().StringVar(&s.Result, "result", "", "Result `JSON` to return. Required.") _ = cobra.MarkFlagRequired(s.Command.Flags(), "result") - s.Command.Flags().StringVar(&s.Identity, "identity", "", "Identity of the user submitting this request.") s.WorkflowReferenceOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { @@ -462,7 +462,6 @@ type TemporalActivityFailCommand struct { WorkflowReferenceOptions ActivityId string Detail string - Identity string Reason string } @@ -481,7 +480,6 @@ func NewTemporalActivityFailCommand(cctx *CommandContext, parent *TemporalActivi s.Command.Flags().StringVar(&s.ActivityId, "activity-id", "", "Activity ID to fail. Required.") _ = cobra.MarkFlagRequired(s.Command.Flags(), "activity-id") s.Command.Flags().StringVar(&s.Detail, "detail", "", "Reason for failing the Activity (JSON).") - s.Command.Flags().StringVar(&s.Identity, "identity", "", "Identity of the user submitting this request.") s.Command.Flags().StringVar(&s.Reason, "reason", "", "Reason for failing the Activity.") s.WorkflowReferenceOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { @@ -514,7 +512,7 @@ func NewTemporalActivityPauseCommand(cctx *CommandContext, parent *TemporalActiv } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVarP(&s.ActivityId, "activity-id", "a", "", "The Activity ID to pause. Either `activity-id` or `activity-type` must be provided, but not both.") - s.Command.Flags().StringVarP(&s.ActivityType, "activity-type", "g", "", "All activities of the Activity Type will be paused. Either `activity-id` or `activity-type` must be provided, but not both. Note: Pausing Activity by Type is an experimental feature and may change in the future.") + s.Command.Flags().StringVar(&s.ActivityType, "activity-type", "", "All activities of the Activity Type will be paused. Either `activity-id` or `activity-type` must be provided, but not both. Note: Pausing Activity by Type is an experimental feature and may change in the future.") s.Command.Flags().StringVar(&s.Identity, "identity", "", "The identity of the user or client submitting this request.") s.WorkflowReferenceOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { @@ -528,12 +526,15 @@ func NewTemporalActivityPauseCommand(cctx *CommandContext, parent *TemporalActiv type TemporalActivityResetCommand struct { Parent *TemporalActivityCommand Command cobra.Command - WorkflowReferenceOptions - ActivityId string - ActivityType string - Identity string - KeepPaused bool - ResetHeartbeats bool + SingleWorkflowOrBatchOptions + ActivityId string + ActivityType string + KeepPaused bool + ResetAttempts bool + ResetHeartbeats bool + MatchAll bool + Jitter Duration + RestoreOriginalOptions bool } func NewTemporalActivityResetCommand(cctx *CommandContext, parent *TemporalActivityCommand) *TemporalActivityResetCommand { @@ -543,17 +544,21 @@ func NewTemporalActivityResetCommand(cctx *CommandContext, parent *TemporalActiv s.Command.Use = "reset [flags]" s.Command.Short = "Reset an Activity" if hasHighlighting { - s.Command.Long = "Reset an activity. This restarts the activity as if it were first being\nscheduled. That is, it will reset both the number of attempts and the\nactivity timeout, as well as, optionally, the\nheartbeat details.\n\nIf the activity may be executing (i.e. it has not yet timed out), the\nreset will take effect the next time it fails, heartbeats, or times out.\nIf is waiting for a retry (i.e. has failed or timed out), the reset\nwill apply immediately.\n\nIf the activity is already paused, it will be unpaused by default.\nYou can specify \x1b[1mkeep_paused\x1b[0m to prevent this.\n\nIf the activity is paused and the \x1b[1mkeep_paused\x1b[0m flag is not provided,\nit will be unpaused. If the activity is paused and \x1b[1mkeep_paused\x1b[0m flag\nis provided - it will stay paused.\n\nActivities can be specified by their Activity ID or Activity Type.\n\n### Resetting activities that heartbeat {#reset-heartbeats}\n\nActivities that heartbeat will receive a Canceled failure\nthe next time they heartbeat after a reset.\n\nIf, in your Activity, you need to do any cleanup when an Activity is\nreset, handle this error and then re-throw it when you've cleaned up.\n\nIf the \x1b[1mreset_heartbeats\x1b[0m flag is set, the heartbeat details will also be cleared.\n\nSpecify the Activity Type of ID and Workflow IDs:\n\n\x1b[1mtemporal activity reset \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId\n --keep-paused\n --reset-heartbeats\x1b[0m" + s.Command.Long = "Reset an activity. This restarts the activity as if it were first being\nscheduled. That is, it will reset both the number of attempts and the\nactivity timeout, as well as, optionally, the\nheartbeat details.\n\nIf the activity may be executing (i.e. it has not yet timed out), the\nreset will take effect the next time it fails, heartbeats, or times out.\nIf is waiting for a retry (i.e. has failed or timed out), the reset\nwill apply immediately.\n\nIf the activity is already paused, it will be unpaused by default.\nYou can specify \x1b[1mkeep_paused\x1b[0m to prevent this.\n\nIf the activity is paused and the \x1b[1mkeep_paused\x1b[0m flag is not provided,\nit will be unpaused. If the activity is paused and \x1b[1mkeep_paused\x1b[0m flag\nis provided - it will stay paused.\n\nActivities can be specified by their Activity ID or Activity Type.\n\n### Resetting activities that heartbeat {#reset-heartbeats}\n\nActivities that heartbeat will receive a Canceled failure\nthe next time they heartbeat after a reset.\n\nIf, in your Activity, you need to do any cleanup when an Activity is\nreset, handle this error and then re-throw it when you've cleaned up.\n\nIf the \x1b[1mreset_heartbeats\x1b[0m flag is set, the heartbeat details will also be cleared.\n\nSpecify the Activity Type of ID and Workflow IDs:\n\n\x1b[1mtemporal activity reset \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId\n --keep-paused\n --reset-heartbeats\x1b[0m\n\nEither \x1b[1mactivity-id\x1b[0m, \x1b[1mactivity-type\x1b[0m, or \x1b[1m--match-all\x1b[0m must be specified.\n\nActivities can be reset in bulk with a visibility query list filter. \nFor example, if you want to reset activities of type Foo:\n\n\x1b[1mtemporal activity reset \\\n --query 'TemporalResetInfo=\"property:activityType=Foo\"'\x1b[0m" } else { - s.Command.Long = "Reset an activity. This restarts the activity as if it were first being\nscheduled. That is, it will reset both the number of attempts and the\nactivity timeout, as well as, optionally, the\nheartbeat details.\n\nIf the activity may be executing (i.e. it has not yet timed out), the\nreset will take effect the next time it fails, heartbeats, or times out.\nIf is waiting for a retry (i.e. has failed or timed out), the reset\nwill apply immediately.\n\nIf the activity is already paused, it will be unpaused by default.\nYou can specify `keep_paused` to prevent this.\n\nIf the activity is paused and the `keep_paused` flag is not provided,\nit will be unpaused. If the activity is paused and `keep_paused` flag\nis provided - it will stay paused.\n\nActivities can be specified by their Activity ID or Activity Type.\n\n### Resetting activities that heartbeat {#reset-heartbeats}\n\nActivities that heartbeat will receive a Canceled failure\nthe next time they heartbeat after a reset.\n\nIf, in your Activity, you need to do any cleanup when an Activity is\nreset, handle this error and then re-throw it when you've cleaned up.\n\nIf the `reset_heartbeats` flag is set, the heartbeat details will also be cleared.\n\nSpecify the Activity Type of ID and Workflow IDs:\n\n```\ntemporal activity reset \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId\n --keep-paused\n --reset-heartbeats\n```" + s.Command.Long = "Reset an activity. This restarts the activity as if it were first being\nscheduled. That is, it will reset both the number of attempts and the\nactivity timeout, as well as, optionally, the\nheartbeat details.\n\nIf the activity may be executing (i.e. it has not yet timed out), the\nreset will take effect the next time it fails, heartbeats, or times out.\nIf is waiting for a retry (i.e. has failed or timed out), the reset\nwill apply immediately.\n\nIf the activity is already paused, it will be unpaused by default.\nYou can specify `keep_paused` to prevent this.\n\nIf the activity is paused and the `keep_paused` flag is not provided,\nit will be unpaused. If the activity is paused and `keep_paused` flag\nis provided - it will stay paused.\n\nActivities can be specified by their Activity ID or Activity Type.\n\n### Resetting activities that heartbeat {#reset-heartbeats}\n\nActivities that heartbeat will receive a Canceled failure\nthe next time they heartbeat after a reset.\n\nIf, in your Activity, you need to do any cleanup when an Activity is\nreset, handle this error and then re-throw it when you've cleaned up.\n\nIf the `reset_heartbeats` flag is set, the heartbeat details will also be cleared.\n\nSpecify the Activity Type of ID and Workflow IDs:\n\n```\ntemporal activity reset \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId\n --keep-paused\n --reset-heartbeats\n```\n\nEither `activity-id`, `activity-type`, or `--match-all` must be specified.\n\nActivities can be reset in bulk with a visibility query list filter. \nFor example, if you want to reset activities of type Foo:\n\n```\ntemporal activity reset \\\n --query 'TemporalResetInfo=\"property:activityType=Foo\"'\n```" } s.Command.Args = cobra.NoArgs - s.Command.Flags().StringVarP(&s.ActivityId, "activity-id", "a", "", "The Activity ID to reset. Either `activity-id` or `activity-type` must be provided, but not both.") - s.Command.Flags().StringVarP(&s.ActivityType, "activity-type", "g", "", "The Activity Type to reset. Either `activity-id` or `activity-type` must be provided, but not both.") - s.Command.Flags().StringVar(&s.Identity, "identity", "", "The identity of the user or client submitting this request.") + s.Command.Flags().StringVarP(&s.ActivityId, "activity-id", "a", "", "The Activity ID to reset. Mutually exclusive with `--query`, `--match-all`, and `--activity-type`. Requires `--workflow-id` to be specified.") + s.Command.Flags().StringVar(&s.ActivityType, "activity-type", "", "Activities of this Type will be reset. Mutually exclusive with `--match-all` and `activity-id`.") s.Command.Flags().BoolVar(&s.KeepPaused, "keep-paused", false, "If the activity was paused, it will stay paused.") - s.Command.Flags().BoolVar(&s.ResetHeartbeats, "reset-heartbeats", false, "Clear the Activity's heartbeat details.") - s.WorkflowReferenceOptions.buildFlags(cctx, s.Command.Flags()) + s.Command.Flags().BoolVar(&s.ResetAttempts, "reset-attempts", false, "Reset the activity attempts.") + s.Command.Flags().BoolVar(&s.ResetHeartbeats, "reset-heartbeats", false, "Reset the Activity's heartbeats. Only works with --reset-attempts.") + s.Command.Flags().BoolVar(&s.MatchAll, "match-all", false, "Every activity should be reset. Every activity should be updated. Mutually exclusive with `--activity-id` and `--activity-type`.") + s.Jitter = 0 + s.Command.Flags().Var(&s.Jitter, "jitter", "The activity will reset at random a time within the specified duration. Can only be used with --query.") + s.Command.Flags().BoolVar(&s.RestoreOriginalOptions, "restore-original-options", false, "Restore the original options of the activity.") + s.SingleWorkflowOrBatchOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -568,7 +573,6 @@ type TemporalActivityUnpauseCommand struct { SingleWorkflowOrBatchOptions ActivityId string ActivityType string - Identity string ResetAttempts bool ResetHeartbeats bool MatchAll bool @@ -587,14 +591,13 @@ func NewTemporalActivityUnpauseCommand(cctx *CommandContext, parent *TemporalAct s.Command.Long = "Re-schedule a previously-paused Activity for execution.\n\nIf the Activity is not running and is past its retry timeout, it will be\nscheduled immediately. Otherwise, it will be scheduled after its retry\ntimeout expires.\n\nUse `--reset-attempts` to reset the number of previous run attempts to\nzero. For example, if an Activity is near the maximum number of attempts\nN specified in its retry policy, `--reset-attempts` will allow the\nActivity to be retried another N times after unpausing.\n\nUse `--reset-heartbeat` to reset the Activity's heartbeats.\n\nActivities can be specified by their Activity ID or Activity Type.\nOne of those parameters must be provided.\n\nSpecify the Activity ID or Type and Workflow IDs:\n\n```\ntemporal activity unpause \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId\n --reset-attempts\n --reset-heartbeats\n```\n\nActivities can be unpaused in bulk via a visibility Query list filter.\nFor example, if you want to unpause activities of type Foo that you\npreviously paused, do:\n\n```\ntemporal activity unpause \\\n --query 'TemporalPauseInfo=\"property:activityType=Foo\"'\n```" } s.Command.Args = cobra.NoArgs - s.Command.Flags().StringVarP(&s.ActivityId, "activity-id", "a", "", "The Activity ID to unpause. Can only be used without --query or --match-all. Either `activity-id` or `activity-type` must be provided, but not both.") - s.Command.Flags().StringVarP(&s.ActivityType, "activity-type", "g", "", "Activities of this Type will unpause. Can only be used without --match-all. Either `activity-id` or `activity-type` must be provided, but not both.") - s.Command.Flags().StringVar(&s.Identity, "identity", "", "The identity of the user or client submitting this request.") - s.Command.Flags().BoolVar(&s.ResetAttempts, "reset-attempts", false, "Also reset the activity attempts.") + s.Command.Flags().StringVarP(&s.ActivityId, "activity-id", "a", "", "The Activity ID to unpause. Mutually exclusive with `--query`, `--match-all`, and `--activity-type`. Requires `--workflow-id` to be specified.") + s.Command.Flags().StringVar(&s.ActivityType, "activity-type", "", "Activities of this Type will unpause. Can only be used without --match-all. Either `activity-id` or `activity-type` must be provided, but not both.") + s.Command.Flags().BoolVar(&s.ResetAttempts, "reset-attempts", false, "Reset the activity attempts.") s.Command.Flags().BoolVar(&s.ResetHeartbeats, "reset-heartbeats", false, "Reset the Activity's heartbeats. Only works with --reset-attempts.") - s.Command.Flags().BoolVar(&s.MatchAll, "match-all", false, "Every paused activity should be unpaused. This flag is ignored if activity-type is provided. Can only be used with --query.") + s.Command.Flags().BoolVar(&s.MatchAll, "match-all", false, "Every paused activity should be unpaused. This flag is ignored if activity-type is provided.") s.Jitter = 0 - s.Command.Flags().VarP(&s.Jitter, "jitter", "j", "The activity will start at random a time within the specified duration. Can only be used with --query.") + s.Command.Flags().Var(&s.Jitter, "jitter", "The activity will start at random a time within the specified duration. Can only be used with --query.") s.SingleWorkflowOrBatchOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { @@ -607,8 +610,10 @@ func NewTemporalActivityUnpauseCommand(cctx *CommandContext, parent *TemporalAct type TemporalActivityUpdateOptionsCommand struct { Parent *TemporalActivityCommand Command cobra.Command - WorkflowReferenceOptions + SingleWorkflowOrBatchOptions ActivityId string + ActivityType string + MatchAll bool TaskQueue string ScheduleToCloseTimeout Duration ScheduleToStartTimeout Duration @@ -618,7 +623,7 @@ type TemporalActivityUpdateOptionsCommand struct { RetryMaximumInterval Duration RetryBackoffCoefficient float32 RetryMaximumAttempts int - Identity string + RestoreOriginalOptions bool } func NewTemporalActivityUpdateOptionsCommand(cctx *CommandContext, parent *TemporalActivityCommand) *TemporalActivityUpdateOptionsCommand { @@ -628,13 +633,14 @@ func NewTemporalActivityUpdateOptionsCommand(cctx *CommandContext, parent *Tempo s.Command.Use = "update-options [flags]" s.Command.Short = "Update Activity options" if hasHighlighting { - s.Command.Long = "Update the options of a running Activity that were passed into it from\n a Workflow. Updates are incremental, only changing the specified\n options.\n\nFor example:\n\n\x1b[1mtemporal activity update-options \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId \\\n --task-queue NewTaskQueueName \\\n --schedule-to-close-timeout DURATION \\\n --schedule-to-start-timeout DURATION \\\n --start-to-close-timeout DURATION \\\n --heartbeat-timeout DURATION \\\n --retry-initial-interval DURATION \\\n --retry-maximum-interval DURATION \\\n --retry-backoff-coefficient NewBackoffCoefficient \\\n --retry-maximum-attempts NewMaximumAttempts\x1b[0m\n\nYou may follow this command with \x1b[1mtemporal activity reset\x1b[0m, and the new values will apply after the reset." + s.Command.Long = "Update the options of a running Activity that were passed into it from\na Workflow. Updates are incremental, only changing the specified \noptions.\n\nFor example:\n\n\x1b[1mtemporal activity update-options \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId \\\n --task-queue NewTaskQueueName \\\n --schedule-to-close-timeout DURATION \\\n --schedule-to-start-timeout DURATION \\\n --start-to-close-timeout DURATION \\\n --heartbeat-timeout DURATION \\\n --retry-initial-interval DURATION \\\n --retry-maximum-interval DURATION \\\n --retry-backoff-coefficient NewBackoffCoefficient \\\n --retry-maximum-attempts NewMaximumAttempts\x1b[0m\n\nYou may follow this command with \x1b[1mtemporal activity reset\x1b[0m, and the new values will apply after the reset.\n\nEither \x1b[1mactivity-id\x1b[0m, \x1b[1mactivity-type\x1b[0m, or \x1b[1m--match-all\x1b[0m must be specified.\n\nActivity options can be updated in bulk with a visibility query list filter. \nFor example, if you want to reset for activities of type Foo, do:\n\n\x1b[1mtemporal activity update-options \\\n --query 'TemporalPauseInfo=\"property:activityType=Foo\"'\n ...\x1b[0m" } else { - s.Command.Long = "Update the options of a running Activity that were passed into it from\n a Workflow. Updates are incremental, only changing the specified\n options.\n\nFor example:\n\n```\ntemporal activity update-options \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId \\\n --task-queue NewTaskQueueName \\\n --schedule-to-close-timeout DURATION \\\n --schedule-to-start-timeout DURATION \\\n --start-to-close-timeout DURATION \\\n --heartbeat-timeout DURATION \\\n --retry-initial-interval DURATION \\\n --retry-maximum-interval DURATION \\\n --retry-backoff-coefficient NewBackoffCoefficient \\\n --retry-maximum-attempts NewMaximumAttempts\n```\n\nYou may follow this command with `temporal activity reset`, and the new values will apply after the reset." + s.Command.Long = "Update the options of a running Activity that were passed into it from\na Workflow. Updates are incremental, only changing the specified \noptions.\n\nFor example:\n\n```\ntemporal activity update-options \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId \\\n --task-queue NewTaskQueueName \\\n --schedule-to-close-timeout DURATION \\\n --schedule-to-start-timeout DURATION \\\n --start-to-close-timeout DURATION \\\n --heartbeat-timeout DURATION \\\n --retry-initial-interval DURATION \\\n --retry-maximum-interval DURATION \\\n --retry-backoff-coefficient NewBackoffCoefficient \\\n --retry-maximum-attempts NewMaximumAttempts\n```\n\nYou may follow this command with `temporal activity reset`, and the new values will apply after the reset.\n\nEither `activity-id`, `activity-type`, or `--match-all` must be specified.\n\nActivity options can be updated in bulk with a visibility query list filter. \nFor example, if you want to reset for activities of type Foo, do:\n\n```\ntemporal activity update-options \\\n --query 'TemporalPauseInfo=\"property:activityType=Foo\"'\n ...\n```" } s.Command.Args = cobra.NoArgs - s.Command.Flags().StringVar(&s.ActivityId, "activity-id", "", "Activity ID. Required.") - _ = cobra.MarkFlagRequired(s.Command.Flags(), "activity-id") + s.Command.Flags().StringVarP(&s.ActivityId, "activity-id", "a", "", "The Activity ID to update options. Mutually exclusive with `--query`, `--match-all`, and `--activity-type`. Requires `--workflow-id` to be specified.") + s.Command.Flags().StringVar(&s.ActivityType, "activity-type", "", "Activities of this Type will be updated. Mutually exclusive with `--match-all` and `activity-id`.") + s.Command.Flags().BoolVar(&s.MatchAll, "match-all", false, "Every activity should be updated. Mutually exclusive with `--activity-id` and `--activity-type`.") s.Command.Flags().StringVar(&s.TaskQueue, "task-queue", "", "Name of the task queue for the Activity.") s.ScheduleToCloseTimeout = 0 s.Command.Flags().Var(&s.ScheduleToCloseTimeout, "schedule-to-close-timeout", "Indicates how long the caller is willing to wait for an activity completion. Limits how long retries will be attempted.") @@ -650,8 +656,8 @@ func NewTemporalActivityUpdateOptionsCommand(cctx *CommandContext, parent *Tempo s.Command.Flags().Var(&s.RetryMaximumInterval, "retry-maximum-interval", "Maximum interval between retries. Exponential backoff leads to interval increase. This value is the cap of the increase.") s.Command.Flags().Float32Var(&s.RetryBackoffCoefficient, "retry-backoff-coefficient", 0, "Coefficient used to calculate the next retry interval. The next retry interval is previous interval multiplied by the backoff coefficient. Must be 1 or larger.") s.Command.Flags().IntVar(&s.RetryMaximumAttempts, "retry-maximum-attempts", 0, "Maximum number of attempts. When exceeded the retries stop even if not expired yet. Setting this value to 1 disables retries. Setting this value to 0 means unlimited attempts(up to the timeouts).") - s.Command.Flags().StringVar(&s.Identity, "identity", "", "Identity of the user submitting this request.") - s.WorkflowReferenceOptions.buildFlags(cctx, s.Command.Flags()) + s.Command.Flags().BoolVar(&s.RestoreOriginalOptions, "restore-original-options", false, "Restore the original options of the activity.") + s.SingleWorkflowOrBatchOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -2791,7 +2797,6 @@ type TemporalWorkerDeploymentDeleteCommand struct { Parent *TemporalWorkerDeploymentCommand Command cobra.Command DeploymentNameOptions - Identity string } func NewTemporalWorkerDeploymentDeleteCommand(cctx *CommandContext, parent *TemporalWorkerDeploymentCommand) *TemporalWorkerDeploymentDeleteCommand { @@ -2806,7 +2811,6 @@ func NewTemporalWorkerDeploymentDeleteCommand(cctx *CommandContext, parent *Temp s.Command.Long = "```\n+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\n```\n\nRemove a Worker Deployment given its Deployment Name.\nA Deployment can only be deleted if it has no Version in it.\n\n```\ntemporal worker deployment delete [options]\n```\n\nFor example, setting the user identity that removed the deployment:\n\n```\ntemporal worker deployment delete \\\n --name YourDeploymentName \\\n --identity YourIdentity\n\n```" } s.Command.Args = cobra.NoArgs - s.Command.Flags().StringVar(&s.Identity, "identity", "", "Identity of the user submitting this request.") s.DeploymentNameOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { @@ -2820,7 +2824,6 @@ type TemporalWorkerDeploymentDeleteVersionCommand struct { Parent *TemporalWorkerDeploymentCommand Command cobra.Command DeploymentVersionOptions - Identity string SkipDrainage bool } @@ -2836,7 +2839,6 @@ func NewTemporalWorkerDeploymentDeleteVersionCommand(cctx *CommandContext, paren s.Command.Long = "```\n+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\n```\n\nRemove a Worker Deployment Version given its fully-qualified identifier.\nThis is rarely needed during normal operation\nsince unused Versions are eventually garbage collected.\nThe client can delete a Version only when all of the following conditions\nare met:\n - It is not the Current or Ramping Version for this Deployment.\n - It has no active pollers, i.e., none of the task queues in the\n Version have pollers.\n - It is not draining. This requirement can be ignored with the option\n`--skip-drainage`.\n```\ntemporal worker deployment delete-version [options]\n```\n\nFor example, skipping the drainage restriction:\n\n```\ntemporal worker deployment delete-version \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --skip-drainage\n```" } s.Command.Args = cobra.NoArgs - s.Command.Flags().StringVar(&s.Identity, "identity", "", "Identity of the user submitting this request.") s.Command.Flags().BoolVar(&s.SkipDrainage, "skip-drainage", false, "Ignore the deletion requirement of not draining.") s.DeploymentVersionOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { @@ -2930,7 +2932,6 @@ type TemporalWorkerDeploymentSetCurrentVersionCommand struct { Parent *TemporalWorkerDeploymentCommand Command cobra.Command DeploymentVersionOrUnversionedOptions - Identity string IgnoreMissingTaskQueues bool Yes bool } @@ -2947,7 +2948,6 @@ func NewTemporalWorkerDeploymentSetCurrentVersionCommand(cctx *CommandContext, p s.Command.Long = "```\n+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\n```\n\nSet the Current Version for a Deployment.\nWhen a Version is current, Workers of that Deployment Version will receive\ntasks from new Workflows, and from existing AutoUpgrade Workflows that\nare running on this Deployment.\n\nIf not all the expected Task Queues are being polled by Workers in the\nnew Version the request will fail. To override this protection use\n`--ignore-missing-task-queues`. Note that this would ignore task queues\nin a deployment that are not yet discovered, leading to inconsistent task\nqueue configuration.\n\n```\ntemporal worker deployment set-current-version [options]\n```\n\nFor example, to set the Current Version of a deployment\n`YourDeploymentName`, with a version with Build ID `YourBuildID`, and\nin the default namespace:\n\n```\ntemporal worker deployment set-current-version \\\n --deployment-name YourDeploymentName --build-id YourBuildID\n```\n\nThe target of set-current-version can also be unversioned workers:\n\n```\ntemporal worker deployment set-current-version \\\n --deployment-name YourDeploymentName --unversioned\n```" } s.Command.Args = cobra.NoArgs - s.Command.Flags().StringVar(&s.Identity, "identity", "", "Identity of the user submitting this request.") s.Command.Flags().BoolVar(&s.IgnoreMissingTaskQueues, "ignore-missing-task-queues", false, "Override protection to accidentally remove task queues.") s.Command.Flags().BoolVarP(&s.Yes, "yes", "y", false, "Don't prompt to confirm set Current Version.") s.DeploymentVersionOrUnversionedOptions.buildFlags(cctx, s.Command.Flags()) @@ -2965,7 +2965,6 @@ type TemporalWorkerDeploymentSetRampingVersionCommand struct { DeploymentVersionOrUnversionedOptions Percentage float32 Delete bool - Identity string IgnoreMissingTaskQueues bool Yes bool } @@ -2984,7 +2983,6 @@ func NewTemporalWorkerDeploymentSetRampingVersionCommand(cctx *CommandContext, p s.Command.Args = cobra.NoArgs s.Command.Flags().Float32Var(&s.Percentage, "percentage", 0, "Percentage of tasks redirected to the Ramping Version. Valid range [0,100].") s.Command.Flags().BoolVar(&s.Delete, "delete", false, "Delete the Ramping Version.") - s.Command.Flags().StringVar(&s.Identity, "identity", "", "Identity of the user submitting this request.") s.Command.Flags().BoolVar(&s.IgnoreMissingTaskQueues, "ignore-missing-task-queues", false, "Override protection to accidentally remove task queues.") s.Command.Flags().BoolVarP(&s.Yes, "yes", "y", false, "Don't prompt to confirm set Ramping Version.") s.DeploymentVersionOrUnversionedOptions.buildFlags(cctx, s.Command.Flags()) diff --git a/temporalcli/commands.worker.deployment.go b/temporalcli/commands.worker.deployment.go index 4f9a11d89..072fd838f 100644 --- a/temporalcli/commands.worker.deployment.go +++ b/temporalcli/commands.worker.deployment.go @@ -415,7 +415,7 @@ func (c *TemporalWorkerDeploymentDeleteCommand) run(cctx *CommandContext, args [ _, err = cl.WorkerDeploymentClient().Delete(cctx, client.WorkerDeploymentDeleteOptions{ Name: c.Name, - Identity: c.Identity, + Identity: c.Parent.Parent.Identity, }) if err != nil { return fmt.Errorf("error deleting worker deployment: %w", err) @@ -503,7 +503,7 @@ func (c *TemporalWorkerDeploymentDeleteVersionCommand) run(cctx *CommandContext, _, err = dHandle.DeleteVersion(cctx, client.WorkerDeploymentDeleteVersionOptions{ BuildID: c.BuildId, SkipDrainage: c.SkipDrainage, - Identity: c.Identity, + Identity: c.Parent.Parent.Identity, }) if err != nil { return fmt.Errorf("error deleting worker deployment version: %w", err) @@ -556,7 +556,7 @@ func (c *TemporalWorkerDeploymentSetCurrentVersionCommand) run(cctx *CommandCont dHandle := cl.WorkerDeploymentClient().GetHandle(c.DeploymentName) _, err = dHandle.SetCurrentVersion(cctx, client.WorkerDeploymentSetCurrentVersionOptions{ BuildID: c.BuildId, - Identity: c.Identity, + Identity: c.Parent.Parent.Identity, IgnoreMissingTaskQueues: c.IgnoreMissingTaskQueues, ConflictToken: token, }) @@ -594,7 +594,7 @@ func (c *TemporalWorkerDeploymentSetRampingVersionCommand) run(cctx *CommandCont BuildID: c.BuildId, Percentage: percentage, ConflictToken: token, - Identity: c.Identity, + Identity: c.Parent.Parent.Identity, IgnoreMissingTaskQueues: c.IgnoreMissingTaskQueues, }) if err != nil { diff --git a/temporalcli/commands.workflow.go b/temporalcli/commands.workflow.go index 4be89054c..6b9b8714b 100644 --- a/temporalcli/commands.workflow.go +++ b/temporalcli/commands.workflow.go @@ -50,7 +50,7 @@ func (c *TemporalWorkflowCancelCommand) run(cctx *CommandContext, args []string) } else { // batchReq != nil batchReq.Operation = &workflowservice.StartBatchOperationRequest_CancellationOperation{ CancellationOperation: &batch.BatchOperationCancellation{ - Identity: clientIdentity(), + Identity: c.Parent.Identity, }, } if err := startBatchJob(cctx, cl, batchReq); err != nil { @@ -85,7 +85,7 @@ func (c *TemporalWorkflowDeleteCommand) run(cctx *CommandContext, args []string) } else { // batchReq != nil batchReq.Operation = &workflowservice.StartBatchOperationRequest_DeletionOperation{ DeletionOperation: &batch.BatchOperationDeletion{ - Identity: clientIdentity(), + Identity: c.Parent.Identity, }, } if err := startBatchJob(cctx, cl, batchReq); err != nil { @@ -174,7 +174,7 @@ func (c *TemporalWorkflowUpdateOptionsCommand) run(cctx *CommandContext, args [] batchReq.Operation = &workflowservice.StartBatchOperationRequest_UpdateWorkflowOptionsOperation{ UpdateWorkflowOptionsOperation: &batch.BatchOperationUpdateWorkflowExecutionOptions{ - Identity: clientIdentity(), + Identity: c.Parent.Identity, WorkflowExecutionOptions: &workflowpb.WorkflowExecutionOptions{ VersioningOverride: protoVerOverride, }, @@ -223,7 +223,7 @@ func (c *TemporalWorkflowSignalCommand) run(cctx *CommandContext, args []string) WorkflowExecution: &common.WorkflowExecution{WorkflowId: c.WorkflowId, RunId: c.RunId}, SignalName: c.Name, Input: input, - Identity: clientIdentity(), + Identity: c.Parent.Identity, }) if err != nil { return fmt.Errorf("failed signalling workflow: %w", err) @@ -234,7 +234,7 @@ func (c *TemporalWorkflowSignalCommand) run(cctx *CommandContext, args []string) SignalOperation: &batch.BatchOperationSignal{ Signal: c.Name, Input: input, - Identity: clientIdentity(), + Identity: c.Parent.Identity, }, } if err := startBatchJob(cctx, cl, batchReq); err != nil { @@ -287,7 +287,7 @@ func (c *TemporalWorkflowTerminateCommand) run(cctx *CommandContext, _ []string) } else { // batchReq != nil batchReq.Operation = &workflowservice.StartBatchOperationRequest_TerminationOperation{ TerminationOperation: &batch.BatchOperationTermination{ - Identity: clientIdentity(), + Identity: c.Parent.Identity, }, } if err := startBatchJob(cctx, cl, batchReq); err != nil { @@ -386,7 +386,7 @@ func (c *TemporalWorkflowUpdateDescribeCommand) run(cctx *CommandContext, args [ }, UpdateId: c.UpdateId, }, - Identity: clientIdentity(), + Identity: c.Parent.Parent.Identity, // WaitPolicy omitted intentionally for nonblocking } resp, err := cl.WorkflowService().PollWorkflowExecutionUpdate(cctx, pollReq) diff --git a/temporalcli/commands.workflow_exec.go b/temporalcli/commands.workflow_exec.go index 4c5be6d20..7580d3c2f 100644 --- a/temporalcli/commands.workflow_exec.go +++ b/temporalcli/commands.workflow_exec.go @@ -173,7 +173,7 @@ func (c *TemporalWorkflowSignalWithStartCommand) run(cctx *CommandContext, _ []s WorkflowTaskTimeout: durationpb.New(wfStartOpts.WorkflowTaskTimeout), SignalName: c.SignalName, SignalInput: signalInput, - Identity: clientIdentity(), + Identity: c.Parent.Identity, RetryPolicy: retryPolicy, CronSchedule: wfStartOpts.CronSchedule, Memo: memo, diff --git a/temporalcli/commands.workflow_reset.go b/temporalcli/commands.workflow_reset.go index 47112a2c0..9a4b6f135 100644 --- a/temporalcli/commands.workflow_reset.go +++ b/temporalcli/commands.workflow_reset.go @@ -130,7 +130,7 @@ func (c *TemporalWorkflowResetCommand) runBatchResetWithPostOps(cctx *CommandCon } request.Operation = &workflowservice.StartBatchOperationRequest_ResetOperation{ ResetOperation: &batch.BatchOperationReset{ - Identity: clientIdentity(), + Identity: c.Parent.Identity, Options: batchResetOptions, PostResetOperations: postOps, }, diff --git a/temporalcli/commandsgen/commands.yml b/temporalcli/commandsgen/commands.yml index bcfed3303..3a0d718dc 100644 --- a/temporalcli/commandsgen/commands.yml +++ b/temporalcli/commandsgen/commands.yml @@ -304,9 +304,6 @@ commands: type: string description: Result `JSON` to return. required: true - - name: identity - type: string - description: Identity of the user submitting this request. option-sets: - workflow-reference @@ -329,9 +326,6 @@ commands: - name: detail type: string description: Reason for failing the Activity (JSON). - - name: identity - type: string - description: Identity of the user submitting this request. - name: reason type: string description: Reason for failing the Activity. @@ -342,8 +336,8 @@ commands: summary: Update Activity options description: | Update the options of a running Activity that were passed into it from - a Workflow. Updates are incremental, only changing the specified - options. + a Workflow. Updates are incremental, only changing the specified + options. For example: @@ -363,11 +357,32 @@ commands: ``` You may follow this command with `temporal activity reset`, and the new values will apply after the reset. + + Either `activity-id`, `activity-type`, or `--match-all` must be specified. + + Activity options can be updated in bulk with a visibility query list filter. + For example, if you want to reset for activities of type Foo, do: + + ``` + temporal activity update-options \ + --query 'TemporalPauseInfo="property:activityType=Foo"' + ... + ``` options: + - name: activity-id + short: a type: string - description: Activity ID. - required: true + description: | + The Activity ID to update options. Mutually exclusive with `--query`, `--match-all`, and `--activity-type`. Requires `--workflow-id` to be specified. + - name: activity-type + type: string + description: | + Activities of this Type will be updated. Mutually exclusive with `--match-all` and `activity-id`. + - name: match-all + type: bool + description: | + Every activity should be updated. Mutually exclusive with `--activity-id` and `--activity-type`. - name: task-queue type: string description: Name of the task queue for the Activity. @@ -420,11 +435,11 @@ commands: expired yet. Setting this value to 1 disables retries. Setting this value to 0 means unlimited attempts(up to the timeouts). - - name: identity - type: string - description: Identity of the user submitting this request. + - name: restore-original-options + type: bool + description: Restore the original options of the activity. option-sets: - - workflow-reference + - single-workflow-or-batch - name: temporal activity pause summary: Pause an Activity @@ -458,7 +473,6 @@ commands: type: string description: The Activity ID to pause. Either `activity-id` or `activity-type` must be provided, but not both. - name: activity-type - short: g type: string description: | All activities of the Activity Type will be paused. Either `activity-id` or `activity-type` must be provided, but not both. @@ -511,18 +525,14 @@ commands: short: a type: string description: | - The Activity ID to unpause. Can only be used without --query or --match-all. Either `activity-id` or `activity-type` must be provided, but not both. + The Activity ID to unpause. Mutually exclusive with `--query`, `--match-all`, and `--activity-type`. Requires `--workflow-id` to be specified. - name: activity-type - short: g type: string description: | Activities of this Type will unpause. Can only be used without --match-all. Either `activity-id` or `activity-type` must be provided, but not both. - - name: identity - type: string - description: The identity of the user or client submitting this request. - name: reset-attempts type: bool - description: Also reset the activity attempts. + description: Reset the activity attempts. - name: reset-heartbeats type: bool description: | @@ -531,10 +541,9 @@ commands: type: bool description: | Every paused activity should be unpaused. This flag is ignored if - activity-type is provided. Can only be used with --query. + activity-type is provided. - name: jitter type: duration - short: j description: | The activity will start at random a time within the specified duration. Can only be used with --query. @@ -582,26 +591,49 @@ commands: --keep-paused --reset-heartbeats ``` + + Either `activity-id`, `activity-type`, or `--match-all` must be specified. + + Activities can be reset in bulk with a visibility query list filter. + For example, if you want to reset activities of type Foo: + + ``` + temporal activity reset \ + --query 'TemporalResetInfo="property:activityType=Foo"' + ``` options: - name: activity-id short: a type: string - description: The Activity ID to reset. Either `activity-id` or `activity-type` must be provided, but not both. + description: The Activity ID to reset. Mutually exclusive with `--query`, `--match-all`, and `--activity-type`. Requires `--workflow-id` to be specified. - name: activity-type - short: g - type: string - description: The Activity Type to reset. Either `activity-id` or `activity-type` must be provided, but not both. - - name: identity type: string - description: The identity of the user or client submitting this request. + description: Activities of this Type will be reset. Mutually exclusive with `--match-all` and `activity-id`. - name: keep-paused type: bool description: If the activity was paused, it will stay paused. + - name: reset-attempts + type: bool + description: Reset the activity attempts. - name: reset-heartbeats type: bool - description: Clear the Activity's heartbeat details. + description: | + Reset the Activity's heartbeats. Only works with --reset-attempts. + - name: match-all + type: bool + description: | + Every activity should be reset. Every activity should be updated. Mutually exclusive with `--activity-id` and `--activity-type`. + - name: jitter + type: duration + description: | + The activity will reset at random a time within the specified duration. + Can only be used with --query. + - name: restore-original-options + type: bool + description: | + Restore the original options of the activity. option-sets: - - workflow-reference + - single-workflow-or-batch - name: temporal batch summary: Manage running batch jobs @@ -968,10 +1000,6 @@ commands: ``` option-sets: - deployment-name - options: - - name: identity - type: string - description: Identity of the user submitting this request. - name: temporal worker deployment list summary: Enumerate Worker Deployments in the client's namespace @@ -1060,9 +1088,6 @@ commands: option-sets: - deployment-version options: - - name: identity - type: string - description: Identity of the user submitting this request. - name: skip-drainage type: bool description: Ignore the deletion requirement of not draining. @@ -1110,9 +1135,6 @@ commands: option-sets: - deployment-version-or-unversioned options: - - name: identity - type: string - description: Identity of the user submitting this request. - name: ignore-missing-task-queues type: bool description: Override protection to accidentally remove task queues. @@ -1180,9 +1202,6 @@ commands: - name: delete type: bool description: Delete the Ramping Version. - - name: identity - type: string - description: Identity of the user submitting this request. - name: ignore-missing-task-queues type: bool description: Override protection to accidentally remove task queues. @@ -4204,6 +4223,9 @@ option-sets: HTTP headers for requests to codec server. Format as a `KEY=VALUE` pair. May be passed multiple times to set multiple headers. + - name: identity + type: string + description: The identity of the user or client submitting this request. Defaults to "temporal-cli:$USER@$HOST". - name: overlap-policy options: From ba7dc0e5eb0dfba74932fdbc6cc2dfb832014dc3 Mon Sep 17 00:00:00 2001 From: Roey Berman Date: Fri, 1 Aug 2025 14:22:58 -0600 Subject: [PATCH 02/28] Fix default value for address (#839) Not changing the actual default, just what is printed in the help output. --- temporalcli/commands.gen.go | 2 +- temporalcli/commandsgen/commands.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/temporalcli/commands.gen.go b/temporalcli/commands.gen.go index d88b740e5..017dccd7b 100644 --- a/temporalcli/commands.gen.go +++ b/temporalcli/commands.gen.go @@ -38,7 +38,7 @@ type ClientOptions struct { } func (v *ClientOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSet) { - f.StringVar(&v.Address, "address", "127.0.0.1:7233", "Temporal Service gRPC endpoint.") + f.StringVar(&v.Address, "address", "localhost:7233", "Temporal Service gRPC endpoint.") f.StringVar(&v.ClientAuthority, "client-authority", "", "Temporal gRPC client :authority pseudoheader.") f.StringVarP(&v.Namespace, "namespace", "n", "default", "Temporal Service Namespace.") f.StringVar(&v.ApiKey, "api-key", "", "API key for request.") diff --git a/temporalcli/commandsgen/commands.yml b/temporalcli/commandsgen/commands.yml index 3a0d718dc..8634f9233 100644 --- a/temporalcli/commandsgen/commands.yml +++ b/temporalcli/commandsgen/commands.yml @@ -4135,7 +4135,7 @@ option-sets: - name: address type: string description: Temporal Service gRPC endpoint. - default: 127.0.0.1:7233 + default: localhost:7233 implied-env: TEMPORAL_ADDRESS - name: client-authority type: string From aa8600285927a4c845cf57c3c7e3f8523810ad11 Mon Sep 17 00:00:00 2001 From: Roey Berman Date: Mon, 4 Aug 2025 15:40:50 -0600 Subject: [PATCH 03/28] Support multiple --input-meta flags for the same key (#836) Closes #833 --------- Co-authored-by: Spencer Judge --- temporalcli/commands.activity.go | 4 +-- temporalcli/commands.gen.go | 2 +- temporalcli/commands.workflow_exec.go | 14 ++++++++-- temporalcli/commands.workflow_test.go | 40 +++++++++++++++++++++++++++ temporalcli/commandsgen/commands.yml | 1 + temporalcli/payload.go | 23 +++++++++++---- 6 files changed, 74 insertions(+), 10 deletions(-) diff --git a/temporalcli/commands.activity.go b/temporalcli/commands.activity.go index 6940727ae..d9550b319 100644 --- a/temporalcli/commands.activity.go +++ b/temporalcli/commands.activity.go @@ -38,7 +38,7 @@ func (c *TemporalActivityCompleteCommand) run(cctx *CommandContext, args []strin } defer cl.Close() - metadata := map[string][]byte{"encoding": []byte("json/plain")} + metadata := map[string][][]byte{"encoding": {[]byte("json/plain")}} resultPayloads, err := CreatePayloads([][]byte{[]byte(c.Result)}, metadata, false) if err != nil { return err @@ -67,7 +67,7 @@ func (c *TemporalActivityFailCommand) run(cctx *CommandContext, args []string) e var detailPayloads *common.Payloads if len(c.Detail) > 0 { - metadata := map[string][]byte{"encoding": []byte("json/plain")} + metadata := map[string][][]byte{"encoding": {[]byte("json/plain")}} detailPayloads, err = CreatePayloads([][]byte{[]byte(c.Detail)}, metadata, false) if err != nil { return err diff --git a/temporalcli/commands.gen.go b/temporalcli/commands.gen.go index 017dccd7b..094f85d89 100644 --- a/temporalcli/commands.gen.go +++ b/temporalcli/commands.gen.go @@ -248,7 +248,7 @@ type PayloadInputOptions struct { func (v *PayloadInputOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSet) { f.StringArrayVarP(&v.Input, "input", "i", nil, "Input value. Use JSON content or set --input-meta to override. Can't be combined with --input-file. Can be passed multiple times to pass multiple arguments.") f.StringArrayVar(&v.InputFile, "input-file", nil, "A path or paths for input file(s). Use JSON content or set --input-meta to override. Can't be combined with --input. Can be passed multiple times to pass multiple arguments.") - f.StringArrayVar(&v.InputMeta, "input-meta", nil, "Input payload metadata as a `KEY=VALUE` pair. When the KEY is \"encoding\", this overrides the default (\"json/plain\"). Can be passed multiple times.") + f.StringArrayVar(&v.InputMeta, "input-meta", nil, "Input payload metadata as a `KEY=VALUE` pair. When the KEY is \"encoding\", this overrides the default (\"json/plain\"). Can be passed multiple times. Repeated metadata keys are applied to the corresponding inputs in the provided order.") f.BoolVar(&v.InputBase64, "input-base64", false, "Assume inputs are base64-encoded and attempt to decode them.") } diff --git a/temporalcli/commands.workflow_exec.go b/temporalcli/commands.workflow_exec.go index 7580d3c2f..ff9f73443 100644 --- a/temporalcli/commands.workflow_exec.go +++ b/temporalcli/commands.workflow_exec.go @@ -623,13 +623,23 @@ func (p *PayloadInputOptions) buildRawInputPayloads() (*common.Payloads, error) } // Build metadata - metadata := map[string][]byte{"encoding": []byte("json/plain")} + metadata := map[string][][]byte{} for _, meta := range p.InputMeta { metaPieces := strings.SplitN(meta, "=", 2) if len(metaPieces) != 2 { return nil, fmt.Errorf("metadata %v expected to have '='", meta) } - metadata[metaPieces[0]] = []byte(metaPieces[1]) + if vals, ok := metadata[metaPieces[0]]; ok { + if len(vals) == len(inData) { + return nil, fmt.Errorf("received more --input-meta flags for key %q than number of inputs", metaPieces[0]) + } + metadata[metaPieces[0]] = append(vals, []byte(metaPieces[1])) + } else { + metadata[metaPieces[0]] = [][]byte{[]byte(metaPieces[1])} + } + } + if _, ok := metadata["encoding"]; !ok { + metadata["encoding"] = [][]byte{[]byte("json/plain")} } return CreatePayloads(inData, metadata, p.InputBase64) } diff --git a/temporalcli/commands.workflow_test.go b/temporalcli/commands.workflow_test.go index c1a5b7bfd..775b7000e 100644 --- a/temporalcli/commands.workflow_test.go +++ b/temporalcli/commands.workflow_test.go @@ -56,6 +56,46 @@ func (s *SharedServerSuite) TestWorkflow_Signal_SingleWorkflowSuccess() { s.Equal(map[string]any{"foo": "bar"}, actual) } +func (s *SharedServerSuite) TestWorkflow_Signal_MultipleInputsWithComplexMetadata() { + // Start a random workflow + run, err := s.Client.ExecuteWorkflow( + s.Context, + client.StartWorkflowOptions{TaskQueue: s.Worker().Options.TaskQueue}, + DevWorkflow, + "ignored", + ) + s.NoError(err) + + // Send signal + res := s.Execute( + "workflow", "signal", + "--address", s.Address(), + "-w", run.GetID(), + "--name", "my-signal", + "-i", `{"foo": "bar"}`, + "-i", `{"bar": "baz"}`, + "--input-meta", "encoding=json/proto", + "--input-meta", "messageType=foo", + "--input-meta", "messageType=bar", + ) + s.NoError(res.Err) + s.NoError(s.Client.TerminateWorkflow(s.Context, run.GetID(), "", "")) + iter := s.Client.GetWorkflowHistory(s.Context, run.GetID(), "", false, enums.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) + for iter.HasNext() { + ev, err := iter.Next() + s.NoError(err) + if attr := ev.GetWorkflowExecutionSignaledEventAttributes(); attr != nil { + payloads := attr.GetInput().GetPayloads() + s.Equal("json/proto", string(payloads[0].Metadata["encoding"])) + s.Equal("json/proto", string(payloads[1].Metadata["encoding"])) + s.Equal("foo", string(payloads[0].Metadata["messageType"])) + s.Equal("bar", string(payloads[1].Metadata["messageType"])) + } + return + } + s.Fail("No signal event found in workflow history") +} + func (s *SharedServerSuite) TestWorkflow_Signal_BatchWorkflowSuccess() { res := s.testSignalBatchWorkflow(false) s.Contains(res.Stdout.String(), "approximately 5 workflow(s)") diff --git a/temporalcli/commandsgen/commands.yml b/temporalcli/commandsgen/commands.yml index 8634f9233..582cc0bf1 100644 --- a/temporalcli/commandsgen/commands.yml +++ b/temporalcli/commandsgen/commands.yml @@ -4531,6 +4531,7 @@ option-sets: Input payload metadata as a `KEY=VALUE` pair. When the KEY is "encoding", this overrides the default ("json/plain"). Can be passed multiple times. + Repeated metadata keys are applied to the corresponding inputs in the provided order. - name: input-base64 type: bool description: | diff --git a/temporalcli/payload.go b/temporalcli/payload.go index cb5eb1eda..a2f3bc9d8 100644 --- a/temporalcli/payload.go +++ b/temporalcli/payload.go @@ -9,12 +9,25 @@ import ( "go.temporal.io/api/common/v1" ) -func CreatePayloads(data [][]byte, metadata map[string][]byte, isBase64 bool) (*common.Payloads, error) { +// CreatePayloads creates API Payload objects from given data and metadata slices. +// If metadata has an entry at a data index, it is used, otherwise it uses the metadata entry at index 0. +func CreatePayloads(data [][]byte, metadata map[string][][]byte, isBase64 bool) (*common.Payloads, error) { ret := &common.Payloads{Payloads: make([]*common.Payload, len(data))} for i, in := range data { - // If it's JSON, validate it - if strings.HasPrefix(string(metadata["encoding"]), "json/") && !json.Valid(in) { - return nil, fmt.Errorf("input #%v is not valid JSON", i+1) + var metadataForIndex = make(map[string][]byte, len(metadata)) + for k, vals := range metadata { + if len(vals) == 0 { + continue + } + v := vals[0] + if len(vals) > i { + v = vals[i] + } + // If it's JSON, validate it + if k == "encoding" && strings.HasPrefix(string(v), "json/") && !json.Valid(in) { + return nil, fmt.Errorf("input #%v is not valid JSON", i+1) + } + metadataForIndex[k] = v } // Decode base64 if base64'd (std encoding only for now) if isBase64 { @@ -23,7 +36,7 @@ func CreatePayloads(data [][]byte, metadata map[string][]byte, isBase64 bool) (* return nil, fmt.Errorf("input #%v is not valid base64", i+1) } } - ret.Payloads[i] = &common.Payload{Data: in, Metadata: metadata} + ret.Payloads[i] = &common.Payload{Data: in, Metadata: metadataForIndex} } return ret, nil } From 16fbce85acc1588d5f5ec54cf9fb49c3f1cc1308 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Wed, 27 Aug 2025 10:21:48 -0700 Subject: [PATCH 04/28] Add priority options to workflow starting (#843) --- go.mod | 12 ++-- go.sum | 70 ++++------------------ temporalcli/commands.gen.go | 6 ++ temporalcli/commands.workflow_exec.go | 6 ++ temporalcli/commands.workflow_exec_test.go | 48 +++++++++++++++ temporalcli/commandsgen/commands.yml | 29 +++++++-- 6 files changed, 100 insertions(+), 71 deletions(-) diff --git a/go.mod b/go.mod index 26af6c26c..962d5464a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/temporalio/cli -go 1.24.4 +go 1.24.5 require ( github.com/BurntSushi/toml v1.4.0 @@ -16,10 +16,10 @@ require ( github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 github.com/temporalio/ui-server/v2 v2.39.0 - go.temporal.io/api v1.51.0 - go.temporal.io/sdk v1.35.0 + go.temporal.io/api v1.52.0 + go.temporal.io/sdk v1.36.0 go.temporal.io/sdk/contrib/envconfig v0.1.0 - go.temporal.io/server v1.29.0-135.0.0.20250725000618-7e01f6c035c9 + go.temporal.io/server v1.29.0-139.3 google.golang.org/grpc v1.72.2 google.golang.org/protobuf v1.36.6 gopkg.in/yaml.v3 v3.0.1 @@ -27,7 +27,7 @@ require ( ) require ( - cel.dev/expr v0.22.1 // indirect + cel.dev/expr v0.23.1 // indirect cloud.google.com/go v0.120.0 // indirect cloud.google.com/go/auth v0.15.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect @@ -76,7 +76,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect diff --git a/go.sum b/go.sum index 63dc1f148..c6e37dc53 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,5 @@ -cel.dev/expr v0.22.1 h1:xoFEsNh972Yzey8N9TCPx2nDvMN7TMhQEzxLuj/iRrI= -cel.dev/expr v0.22.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg= +cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= @@ -54,7 +53,6 @@ github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07 github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/benbjohnson/clock v0.0.0-20160125162948-a620c1cc9866/go.mod h1:UMqtWQTnOe4byzwe7Zhwh8f8s+36uszN51sJrSIZlTE= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -75,11 +73,8 @@ github.com/cactus/go-statsd-client/v5 v5.1.0 h1:sbbdfIl9PgisjEoXzvXI1lwUKWElngsj github.com/cactus/go-statsd-client/v5 v5.1.0/go.mod h1:COEvJ1E+/E2L4q6QE5CkjWPi4eeDw9maJBMIuMPBZbY= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coreos/go-oidc/v3 v3.13.0 h1:M66zd0pcc5VxvBNM4pB331Wrsanby+QomQYjN8HamW8= @@ -98,16 +93,12 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= @@ -126,8 +117,6 @@ github.com/go-faker/faker/v4 v4.6.0/go.mod h1:ZmrHuVtTTm2Em9e0Du6CJ9CADaLEzGXW62 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -136,7 +125,6 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI= github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus= github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -144,14 +132,9 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -159,7 +142,6 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk= github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -183,8 +165,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= @@ -288,7 +270,6 @@ github.com/prashantv/protectmem v0.0.0-20171002184600-e20412882b3a h1:AA9vgIBDjM github.com/prashantv/protectmem v0.0.0-20171002184600-e20412882b3a/go.mod h1:lzZQ3Noex5pfAy7mkAeCjcBDteYU85uWWnJ/y6gKU8k= github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= @@ -402,14 +383,14 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= -go.temporal.io/api v1.51.0 h1:9+e14GrIa7nWoWoudqj/PSwm33yYjV+u8TAR9If7s/g= -go.temporal.io/api v1.51.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= -go.temporal.io/sdk v1.35.0 h1:lRNAQ5As9rLgYa7HBvnmKyzxLcdElTuoFJ0FXM/AsLQ= -go.temporal.io/sdk v1.35.0/go.mod h1:1q5MuLc2MEJ4lneZTHJzpVebW2oZnyxoIOWX3oFVebw= +go.temporal.io/api v1.52.0 h1:Tn69z2nhQeXtofa1/j/MbwPHnFRM9+13xqYmFl/KFjM= +go.temporal.io/api v1.52.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/sdk v1.36.0 h1:WO9zetpybBNK7xsQth4Z+3Zzw1zSaM9MOUGrnnUjZMo= +go.temporal.io/sdk v1.36.0/go.mod h1:8BxGRF0LcQlfQrLLGkgVajbsKUp/PY7280XTdcKc18Y= go.temporal.io/sdk/contrib/envconfig v0.1.0 h1:s+G/Ujph+Xl2jzLiiIm2T1vuijDkUL4Kse49dgDVGBE= go.temporal.io/sdk/contrib/envconfig v0.1.0/go.mod h1:FQEO3C56h9C7M6sDgSanB8HnBTmopw9qgVx4F1S6pJk= -go.temporal.io/server v1.29.0-135.0.0.20250725000618-7e01f6c035c9 h1:jJV/LmX6msjAQj+TrPIM+qVZZMu8EPnLENM4nNiJq9k= -go.temporal.io/server v1.29.0-135.0.0.20250725000618-7e01f6c035c9/go.mod h1:qRq3Ei+nk7eXw+Dw60GaHdCDo7dbbqGa7LnSJqPaIlk= +go.temporal.io/server v1.29.0-139.3 h1:j4u7esLvLqzE67Q6s/ExvaFKOensFhskCWXe1VHyBdU= +go.temporal.io/server v1.29.0-139.3/go.mod h1:OKFCSB0xxCX1Bmi2rYutWJEKppIrwuqx+c3e5GeY54w= 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= @@ -418,19 +399,16 @@ go.uber.org/dig v1.18.1 h1:rLww6NuajVjeQn+49u5NcezUJEGwd5uXmyoCKW2g5Es= go.uber.org/dig v1.18.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.14.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -443,7 +421,6 @@ golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 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-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= @@ -452,9 +429,6 @@ golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+ golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -466,9 +440,6 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -482,11 +453,8 @@ 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.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -495,7 +463,6 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -507,7 +474,6 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -535,16 +501,12 @@ golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -563,22 +525,12 @@ gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6d gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs= google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 h1:qEFnJI6AnfZk0NNe8YTyXQh5i//Zxi4gBHwRgp76qpw= google.golang.org/genproto v0.0.0-20250324211829-b45e905df463/go.mod h1:SqIx1NV9hcvqdLHo7uNZDS5lrUJybQ3evo3+z/WBfA0= google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM= google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= @@ -601,8 +553,6 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic= modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= diff --git a/temporalcli/commands.gen.go b/temporalcli/commands.gen.go index 094f85d89..d186826d3 100644 --- a/temporalcli/commands.gen.go +++ b/temporalcli/commands.gen.go @@ -198,6 +198,9 @@ type SharedWorkflowStartOptions struct { Memo []string StaticSummary string StaticDetails string + PriorityKey int + FairnessKey string + FairnessWeight float32 } func (v *SharedWorkflowStartOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSet) { @@ -216,6 +219,9 @@ func (v *SharedWorkflowStartOptions) buildFlags(cctx *CommandContext, f *pflag.F f.StringArrayVar(&v.Memo, "memo", nil, "Memo using 'KEY=\"VALUE\"' pairs. Use JSON values.") f.StringVar(&v.StaticSummary, "static-summary", "", "Static Workflow summary for human consumption in UIs. Uses Temporal Markdown formatting, should be a single line. EXPERIMENTAL.") f.StringVar(&v.StaticDetails, "static-details", "", "Static Workflow details for human consumption in UIs. Uses Temporal Markdown formatting, may be multiple lines. EXPERIMENTAL.") + f.IntVar(&v.PriorityKey, "priority-key", 0, "Priority key (1-5, lower numbers = higher priority). Tasks in a queue should be processed in close-to-priority-order. Default is 3 when not specified.") + f.StringVar(&v.FairnessKey, "fairness-key", "", "Fairness key (max 64 bytes) for proportional task dispatch. Tasks with same key share capacity based on their weight.") + f.Float32Var(&v.FairnessWeight, "fairness-weight", 0, "Weight [0.001-1000] for this fairness key. Keys are dispatched proportionally to their weights.") } type WorkflowStartOptions struct { diff --git a/temporalcli/commands.workflow_exec.go b/temporalcli/commands.workflow_exec.go index ff9f73443..fce190b25 100644 --- a/temporalcli/commands.workflow_exec.go +++ b/temporalcli/commands.workflow_exec.go @@ -24,6 +24,7 @@ import ( "go.temporal.io/api/temporalproto" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/sdk/client" + "go.temporal.io/sdk/temporal" "google.golang.org/protobuf/types/known/durationpb" ) @@ -560,6 +561,11 @@ func buildStartOptions(sw *SharedWorkflowStartOptions, w *WorkflowStartOptions) StartDelay: w.StartDelay.Duration(), StaticSummary: sw.StaticSummary, StaticDetails: sw.StaticDetails, + Priority: temporal.Priority{ + PriorityKey: sw.PriorityKey, + FairnessKey: sw.FairnessKey, + FairnessWeight: sw.FairnessWeight, + }, } if w.IdReusePolicy.Value != "" { var err error diff --git a/temporalcli/commands.workflow_exec_test.go b/temporalcli/commands.workflow_exec_test.go index 108d8cb5d..690b4823f 100644 --- a/temporalcli/commands.workflow_exec_test.go +++ b/temporalcli/commands.workflow_exec_test.go @@ -17,6 +17,7 @@ import ( "github.com/google/uuid" "go.temporal.io/api/common/v1" "go.temporal.io/api/enums/v1" + "go.temporal.io/api/history/v1" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/sdk/client" "go.temporal.io/sdk/converter" @@ -953,3 +954,50 @@ func (s *SharedServerSuite) testStartUpdateWithStartHelper(opts updateWithStartT s.Equal(opts.expectedWfOutput["workflow"], wfReturn["workflow"]) s.Equal(opts.expectedWfOutput["update"], wfReturn["update"]) } + +func (s *SharedServerSuite) TestWorkflow_Start_WithPriorityOptions() { + s.Worker().OnDevWorkflow(func(ctx workflow.Context, input any) (any, error) { + return "success", nil + }) + + workflowId := "priority-test-" + uuid.New().String() + res := s.Execute( + "workflow", "start", + "--address", s.Address(), + "--task-queue", s.Worker().Options.TaskQueue, + "--type", "DevWorkflow", + "--workflow-id", workflowId, + "--priority-key", "2", + "--fairness-key", "high-priority-tenant", + "--fairness-weight", "5.5", + ) + s.NoError(res.Err) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + iter := s.Client.GetWorkflowHistory(ctx, + workflowId, "", false, enums.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) + + var startedEvent *history.HistoryEvent + for iter.HasNext() { + event, err := iter.Next() + s.NoError(err) + if event.EventType == enums.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED { + startedEvent = event + break + } + } + + s.NotNil(startedEvent, "WorkflowExecutionStarted event not found") + + startedAttrs := startedEvent.GetWorkflowExecutionStartedEventAttributes() + s.NotNil(startedAttrs, "WorkflowExecutionStarted attributes not found") + + priority := startedAttrs.GetPriority() + s.NotNil(priority, "Priority not found in WorkflowExecutionStarted event") + + s.Equal(int32(2), priority.GetPriorityKey()) + s.Equal("high-priority-tenant", priority.GetFairnessKey()) + s.Equal(float32(5.5), priority.GetFairnessWeight()) +} diff --git a/temporalcli/commandsgen/commands.yml b/temporalcli/commandsgen/commands.yml index 582cc0bf1..9cbf02389 100644 --- a/temporalcli/commandsgen/commands.yml +++ b/temporalcli/commandsgen/commands.yml @@ -3500,11 +3500,13 @@ commands: - auto_upgrade - name: versioning-override-deployment-name type: string - description: When overriding to a `pinned` behavior, specifies the Deployment Name of the + description: + When overriding to a `pinned` behavior, specifies the Deployment Name of the version to target. - name: versioning-override-build-id type: string - description: When overriding to a `pinned` behavior, specifies the Build ID of the + description: + When overriding to a `pinned` behavior, specifies the Build ID of the version to target. - name: temporal workflow query summary: Retrieve Workflow Execution state @@ -4473,6 +4475,22 @@ option-sets: description: | Static Workflow details for human consumption in UIs. Uses Temporal Markdown formatting, may be multiple lines. + - name: priority-key + type: int + description: | + Priority key (1-5, lower numbers = higher priority). + Tasks in a queue should be processed in close-to-priority-order. + Default is 3 when not specified. + - name: fairness-key + type: string + description: | + Fairness key (max 64 bytes) for proportional task dispatch. + Tasks with same key share capacity based on their weight. + - name: fairness-weight + type: float + description: | + Weight [0.001-1000] for this fairness key. + Keys are dispatched proportionally to their weights. - name: workflow-start options: @@ -4643,10 +4661,11 @@ option-sets: - auto_upgrade - name: versioning-override-deployment-name type: string - description: When overriding to a `pinned` behavior, specifies the Deployment Name of the + description: + When overriding to a `pinned` behavior, specifies the Deployment Name of the version to target. - name: versioning-override-build-id type: string - description: When overriding to a `pinned` behavior, specifies the Build ID of the + description: + When overriding to a `pinned` behavior, specifies the Build ID of the version to target. - From 9a9a69607cba04f90d14066e6955b8faa4a11b85 Mon Sep 17 00:00:00 2001 From: Siva Girish Ramesh <35769591+sivagirish81@users.noreply.github.com> Date: Thu, 2 Oct 2025 09:13:00 -0700 Subject: [PATCH 05/28] Update task queue config (#840) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What was changed + Added a new `report-config` field in the describe command to retrieve updated task queue configs if exists. + Implement display logic for config table along with truncation of large update reasons or update identities. **Note : These changes are only a part of the legacy mode of the describeTaskQueueApi.** ``` ./temporal task-queue describe \ --task-queue=test-display-queue \ --task-queue-type-legacy=activity \ -n=default \ --report-config \ --legacy-mode ``` Sample Response ``` ./temporal task-queue describe \ --task-queue=hello-world \ --task-queue-type-legacy=activity \ -n=default \ --report-config \ --legacy-mode Pollers: Identity LastAccessTime RatePerSecond Task Queue Configuration: Note: Long content may be truncated. Use --output json for full details. Setting Value Reason UpdatedBy UpdatedTime Fairness Key Rate Limit Default 100 requests/second Fairness key test api-tester 2025-08-12 04:31:46 ``` --- + Added separate config Subcommand for getting/setting configs. ``` ./temporal task-queue config --help Manage Task Queue configuration: temporal task-queue config [command] [options] Available commands: - get: Retrieve the current configuration for a task queue - set: Update the configuration for a task queue Usage: temporal task-queue config [command] Available Commands: get Get Task Queue configuration set Set Task Queue configuration ``` --- + Get Subcommand ``` ./temporal task-queue config get --help Retrieve the current configuration for a Task Queue: temporal task-queue config get \ --task-queue YourTaskQueue \ --task-queue-type activity This command returns the current configuration including: - Queue rate limit: The overall rate limit of the task queue. This setting overrides the worker rate limit if set. Unless modified, this is the system-defined rate limit. - Fairness key rate limit defaults: Default rate limits for fairness keys. If set, each individual fairness key will be limited to this rate, scaled by the weight of the fairness key. Usage: temporal task-queue config get [flags] ``` Sample Response ``` ./temporal task-queue config get \ --task-queue=hello-world \ --task-queue-type=activity \ --namespace=default Note: Long content may be truncated. Use --output json for full details. Setting Value Reason UpdatedBy UpdatedTime Fairness Key Rate Limit Default 100 requests/second Fairness key test api-tester 2025-08-12 04:31:46 ``` --- + Set Subcommand ``` ./temporal task-queue config set --help Update a Task Queue's overall rate limit and the default rate limit for all fairness keys: temporal task-queue config set \ --task-queue YourTaskQueue \ --task-queue-type activity \ --namespace YourNamespace \ --queue-rps-limit \ --queue-rps-limit-reason \ --fairness-key-rps-limit-default \ --fairness-key-rps-limit-reason This command supports updating: - Queue rate limits: Controls the overall rate limit of the task queue. This setting overrides the worker rate limit if set. Unless modified, this is the system-defined rate limit. - Fairness key rate limit defaults: Sets default rate limits for fairness keys. If set, each individual fairness key will be limited to this rate, scaled by the weight of the fairness key. To unset a rate limit, pass in 'default', for example: --queue-rps-limit default Usage: temporal task-queue config set [flags] Flags: --fairness-key-rps-limit-default float|default Fairness key rate limit default in requests per second. Accepts a float; or 'default' to unset. --fairness-key-rps-limit-reason string Reason for fairness key rate limit update. -h, --help help for set --queue-rps-limit float|default Queue rate limit in requests per second. Accepts a float; or 'default' to unset. --queue-rps-limit-reason string Reason for queue rate limit update. -t, --task-queue string Task Queue name. Required. --task-queue-type string Task Queue type. Accepted values: workflow, activity, nexus. Accepted values: workflow, activity, nexus. Required. ``` Sample response : ``` ./temporal task-queue config set \ --task-queue=hello-world \ --task-queue-type=activity \ --fairness-key-rps-limit-default=100 \ --fairness-key-rps-limit-reason="Fairness key test" \ --identity="api-tester" \ --namespace=default Successfully updated task queue configuration Config {"fairnessKeysRateLimitDefault":{"rateLimit":{"requestsPerSecond":100},"metadata":{"reason":"Fairness key test","updateIdentity":"api-tester","updateTime":"2025-08-12T04:31:46.640Z"}}} ``` ## Why? + Cli support for `UpdateTaskQueueConfig` api - new api that allows updates of rate limits against task queues. ## Checklist 1. How was this tested: + Added tests. 2. Any docs updates needed? Pending. --------- Co-authored-by: Stephan Behnke Co-authored-by: Stephan Behnke --- temporalcli/commands.gen.go | 98 +++++++ temporalcli/commands.go | 13 + temporalcli/commands.taskqueue.go | 38 ++- temporalcli/commands.taskqueue.helper.go | 100 +++++++ temporalcli/commands.taskqueue_config.go | 163 +++++++++++ temporalcli/commands.taskqueue_config_test.go | 256 ++++++++++++++++++ temporalcli/commandsgen/code.go | 9 +- temporalcli/commandsgen/commands.yml | 120 ++++++++ temporalcli/commandsgen/parse.go | 4 + 9 files changed, 794 insertions(+), 7 deletions(-) create mode 100644 temporalcli/commands.taskqueue.helper.go create mode 100644 temporalcli/commands.taskqueue_config.go create mode 100644 temporalcli/commands.taskqueue_config_test.go diff --git a/temporalcli/commands.gen.go b/temporalcli/commands.gen.go index d186826d3..35acc6370 100644 --- a/temporalcli/commands.gen.go +++ b/temporalcli/commands.gen.go @@ -2164,6 +2164,7 @@ func NewTemporalTaskQueueCommand(cctx *CommandContext, parent *TemporalCommand) s.Command.Long = "Inspect and update Task Queues, the queues that Workers poll for Workflow and\nActivity tasks:\n\n```\ntemporal task-queue [command] [command options] \\\n --task-queue YourTaskQueue\n```\n\nFor example:\n\n```\ntemporal task-queue describe \\\n --task-queue YourTaskQueue\n```" } s.Command.Args = cobra.NoArgs + s.Command.AddCommand(&NewTemporalTaskQueueConfigCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalTaskQueueDescribeCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalTaskQueueGetBuildIdReachabilityCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalTaskQueueGetBuildIdsCommand(cctx, &s).Command) @@ -2174,6 +2175,101 @@ func NewTemporalTaskQueueCommand(cctx *CommandContext, parent *TemporalCommand) return &s } +type TemporalTaskQueueConfigCommand struct { + Parent *TemporalTaskQueueCommand + Command cobra.Command +} + +func NewTemporalTaskQueueConfigCommand(cctx *CommandContext, parent *TemporalTaskQueueCommand) *TemporalTaskQueueConfigCommand { + var s TemporalTaskQueueConfigCommand + s.Parent = parent + s.Command.Use = "config" + s.Command.Short = "Get and set Task Queue configuration" + if hasHighlighting { + s.Command.Long = "Manage Task Queue configuration:\n\n\x1b[1mtemporal task-queue config [command] [options]\x1b[0m\n\nAvailable commands:\n- \x1b[1mget\x1b[0m: Retrieve the current configuration for a task queue\n- \x1b[1mset\x1b[0m: Update the configuration for a task queue" + } else { + s.Command.Long = "Manage Task Queue configuration:\n\n```\ntemporal task-queue config [command] [options]\n```\n\nAvailable commands:\n- `get`: Retrieve the current configuration for a task queue\n- `set`: Update the configuration for a task queue" + } + s.Command.Args = cobra.NoArgs + s.Command.AddCommand(&NewTemporalTaskQueueConfigGetCommand(cctx, &s).Command) + s.Command.AddCommand(&NewTemporalTaskQueueConfigSetCommand(cctx, &s).Command) + return &s +} + +type TemporalTaskQueueConfigGetCommand struct { + Parent *TemporalTaskQueueConfigCommand + Command cobra.Command + TaskQueue string + TaskQueueType StringEnum +} + +func NewTemporalTaskQueueConfigGetCommand(cctx *CommandContext, parent *TemporalTaskQueueConfigCommand) *TemporalTaskQueueConfigGetCommand { + var s TemporalTaskQueueConfigGetCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "get [flags]" + s.Command.Short = "Get Task Queue configuration" + if hasHighlighting { + s.Command.Long = "Retrieve the current configuration for a Task Queue:\n\n\x1b[1mtemporal task-queue config get \\\n --task-queue YourTaskQueue \\\n --task-queue-type activity\x1b[0m\n\nThis command returns the current configuration including:\n- Queue rate limit: The overall rate limit of the task queue.\n This setting overrides the worker rate limit if set.\n Unless modified, this is the system-defined rate limit.\n- Fairness key rate limit defaults: Default rate limits for fairness keys.\n If set, each individual fairness key will be limited to this rate,\n scaled by the weight of the fairness key." + } else { + s.Command.Long = "Retrieve the current configuration for a Task Queue:\n\n```\ntemporal task-queue config get \\\n --task-queue YourTaskQueue \\\n --task-queue-type activity\n```\n\nThis command returns the current configuration including:\n- Queue rate limit: The overall rate limit of the task queue.\n This setting overrides the worker rate limit if set.\n Unless modified, this is the system-defined rate limit.\n- Fairness key rate limit defaults: Default rate limits for fairness keys.\n If set, each individual fairness key will be limited to this rate,\n scaled by the weight of the fairness key." + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVarP(&s.TaskQueue, "task-queue", "t", "", "Task Queue name. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "task-queue") + s.TaskQueueType = NewStringEnum([]string{"workflow", "activity", "nexus"}, "") + s.Command.Flags().Var(&s.TaskQueueType, "task-queue-type", "Task Queue type. Accepted values: workflow, activity, nexus. Accepted values: workflow, activity, nexus. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "task-queue-type") + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + +type TemporalTaskQueueConfigSetCommand struct { + Parent *TemporalTaskQueueConfigCommand + Command cobra.Command + TaskQueue string + TaskQueueType StringEnum + QueueRpsLimit string + QueueRpsLimitReason string + FairnessKeyRpsLimitDefault string + FairnessKeyRpsLimitReason string +} + +func NewTemporalTaskQueueConfigSetCommand(cctx *CommandContext, parent *TemporalTaskQueueConfigCommand) *TemporalTaskQueueConfigSetCommand { + var s TemporalTaskQueueConfigSetCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "set [flags]" + s.Command.Short = "Set Task Queue configuration" + if hasHighlighting { + s.Command.Long = "Update configuration settings for a Task Queue.\n\n\x1b[1mtemporal task-queue config set \\\n --task-queue YourTaskQueue \\\n --task-queue-type activity \\\n --namespace YourNamespace \\\n --queue-rps-limit \\\n --queue-rps-limit-reason \\\n --fairness-key-rps-limit-default \\\n --fairness-key-rps-limit-reason \x1b[0m\n\nThis command supports updating:\n- Queue rate limits: Controls the overall rate limit of the task queue.\n This setting overrides the worker rate limit if set.\n Unless modified, this is the system-defined rate limit.\n- Fairness key rate limit defaults: Sets default rate limits for fairness keys.\n If set, each individual fairness key will be limited to this rate,\n scaled by the weight of the fairness key.\n\nTo unset a rate limit, pass in 'default', for example: --queue-rps-limit default" + } else { + s.Command.Long = "Update configuration settings for a Task Queue.\n\n```\ntemporal task-queue config set \\\n --task-queue YourTaskQueue \\\n --task-queue-type activity \\\n --namespace YourNamespace \\\n --queue-rps-limit \\\n --queue-rps-limit-reason \\\n --fairness-key-rps-limit-default \\\n --fairness-key-rps-limit-reason \n```\n\nThis command supports updating:\n- Queue rate limits: Controls the overall rate limit of the task queue.\n This setting overrides the worker rate limit if set.\n Unless modified, this is the system-defined rate limit.\n- Fairness key rate limit defaults: Sets default rate limits for fairness keys.\n If set, each individual fairness key will be limited to this rate,\n scaled by the weight of the fairness key.\n\nTo unset a rate limit, pass in 'default', for example: --queue-rps-limit default" + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVarP(&s.TaskQueue, "task-queue", "t", "", "Task Queue name. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "task-queue") + s.TaskQueueType = NewStringEnum([]string{"workflow", "activity", "nexus"}, "") + s.Command.Flags().Var(&s.TaskQueueType, "task-queue-type", "Task Queue type. Accepted values: workflow, activity, nexus. Accepted values: workflow, activity, nexus. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "task-queue-type") + s.Command.Flags().StringVar(&s.QueueRpsLimit, "queue-rps-limit", "", "Queue rate limit in requests per second. Accepts a float; or 'default' to unset.") + overrideFlagDisplayType(s.Command.Flags().Lookup("queue-rps-limit"), "float|default") + s.Command.Flags().StringVar(&s.QueueRpsLimitReason, "queue-rps-limit-reason", "", "Reason for queue rate limit update.") + s.Command.Flags().StringVar(&s.FairnessKeyRpsLimitDefault, "fairness-key-rps-limit-default", "", "Fairness key rate limit default in requests per second. Accepts a float; or 'default' to unset.") + overrideFlagDisplayType(s.Command.Flags().Lookup("fairness-key-rps-limit-default"), "float|default") + s.Command.Flags().StringVar(&s.FairnessKeyRpsLimitReason, "fairness-key-rps-limit-reason", "", "Reason for fairness key rate limit update.") + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + type TemporalTaskQueueDescribeCommand struct { Parent *TemporalTaskQueueCommand Command cobra.Command @@ -2187,6 +2283,7 @@ type TemporalTaskQueueDescribeCommand struct { TaskQueueTypeLegacy StringEnum PartitionsLegacy int DisableStats bool + ReportConfig bool } func NewTemporalTaskQueueDescribeCommand(cctx *CommandContext, parent *TemporalTaskQueueCommand) *TemporalTaskQueueDescribeCommand { @@ -2214,6 +2311,7 @@ func NewTemporalTaskQueueDescribeCommand(cctx *CommandContext, parent *TemporalT s.Command.Flags().Var(&s.TaskQueueTypeLegacy, "task-queue-type-legacy", "Task Queue type (legacy mode only). Accepted values: workflow, activity.") s.Command.Flags().IntVar(&s.PartitionsLegacy, "partitions-legacy", 1, "Query partitions 1 through `N`. Experimental/Temporary feature. Legacy mode only.") s.Command.Flags().BoolVar(&s.DisableStats, "disable-stats", false, "Disable task queue statistics.") + s.Command.Flags().BoolVar(&s.ReportConfig, "report-config", false, "Include task queue configuration in the response. When enabled, the command will return the current rate limit configuration for the task queue.") s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) diff --git a/temporalcli/commands.go b/temporalcli/commands.go index 2c280e05f..a549c80b5 100644 --- a/temporalcli/commands.go +++ b/temporalcli/commands.go @@ -580,3 +580,16 @@ func encodeMapToPayloads(in map[string]any) (map[string]*commonpb.Payload, error } 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/temporalcli/commands.taskqueue.go b/temporalcli/commands.taskqueue.go index fdfaad9cf..a769190a4 100644 --- a/temporalcli/commands.taskqueue.go +++ b/temporalcli/commands.taskqueue.go @@ -334,6 +334,7 @@ func (c *TemporalTaskQueueDescribeCommand) runLegacy(cctx *CommandContext, args var statuses []*statusWithPartition var pollers []*pollerWithPartition + var config *taskqueue.TaskQueueConfig // TODO: remove this when the server does partition fan-out for p := 0; p < partitions; p++ { @@ -345,6 +346,7 @@ func (c *TemporalTaskQueueDescribeCommand) runLegacy(cctx *CommandContext, args }, TaskQueueType: taskQueueType, IncludeTaskQueueStatus: true, + ReportConfig: c.ReportConfig, }) if err != nil { return fmt.Errorf("unable to describe task queue: %w", err) @@ -360,14 +362,25 @@ func (c *TemporalTaskQueueDescribeCommand) runLegacy(cctx *CommandContext, args Versioning: pi.WorkerVersionCapabilities, }) } + // Capture config from the first partition (they should all be the same) + if p == 0 && resp.Config != nil { + config = resp.Config + } } // For JSON, we'll just dump the proto if cctx.JSONOutput { - return cctx.Printer.PrintStructured(map[string]any{ + output := map[string]any{ "taskQueues": statuses, "pollers": pollers, - }, printer.StructuredOptions{}) + } + + // Include config if requested + if c.ReportConfig && config != nil { + output["config"] = config + } + + return cctx.Printer.PrintStructured(output, printer.StructuredOptions{}) } // For text, we will use a table for pollers @@ -382,7 +395,18 @@ func (c *TemporalTaskQueueDescribeCommand) runLegacy(cctx *CommandContext, args items[i].LastAccessTime = poller.LastAccessTime.AsTime() items[i].RatePerSecond = poller.RatePerSecond } - return cctx.Printer.PrintStructured(items, printer.StructuredOptions{Table: &printer.TableOptions{}}) + err = cctx.Printer.PrintStructured(items, printer.StructuredOptions{Table: &printer.TableOptions{}}) + if err != nil { + return err + } + + // Display config if requested + if c.ReportConfig && config != nil { + cctx.Printer.Println(color.MagentaString("\nTask Queue Configuration:")) + return printTaskQueueConfig(cctx, config) + } + + return nil } func (c *TemporalTaskQueueListPartitionCommand) run(cctx *CommandContext, args []string) error { @@ -425,3 +449,11 @@ func (c *TemporalTaskQueueListPartitionCommand) run(cctx *CommandContext, args [ return nil } + +// Helper function to truncate strings +func truncateString(s string, maxLength int) string { + if len(s) <= maxLength { + return s + } + return s[:maxLength-3] + "..." +} diff --git a/temporalcli/commands.taskqueue.helper.go b/temporalcli/commands.taskqueue.helper.go new file mode 100644 index 000000000..591531c3d --- /dev/null +++ b/temporalcli/commands.taskqueue.helper.go @@ -0,0 +1,100 @@ +package temporalcli + +import ( + "fmt" + "time" + + "github.com/fatih/color" + "github.com/temporalio/cli/temporalcli/internal/printer" + "go.temporal.io/api/enums/v1" + "go.temporal.io/api/taskqueue/v1" +) + +// Create a structured table for config display. +type configRow struct { + Setting string + Value string + Reason string + UpdatedBy string + UpdatedTime string +} + +func parseTaskQueueType(input string) (enums.TaskQueueType, error) { + switch input { + case "", "workflow": + return enums.TASK_QUEUE_TYPE_WORKFLOW, nil + case "activity": + return enums.TASK_QUEUE_TYPE_ACTIVITY, nil + case "nexus": + return enums.TASK_QUEUE_TYPE_NEXUS, nil + default: + return enums.TASK_QUEUE_TYPE_WORKFLOW, fmt.Errorf( + "invalid task queue type: %s. Must be one of: workflow, activity, nexus", input) + } +} + +func buildRateLimitConfigRow(setting string, rl *taskqueue.RateLimitConfig, format string) configRow { + value := "Not Set" + reason := "" + updatedBy := "" + updatedTime := "" + + if rl.RateLimit != nil && rl.RateLimit.RequestsPerSecond > 0 { + value = fmt.Sprintf(format, rl.RateLimit.RequestsPerSecond) + } + + if rl.Metadata != nil { + if rl.Metadata.Reason != "" { + reason = truncateString(rl.Metadata.Reason, 50) + } + if rl.Metadata.UpdateIdentity != "" { + updatedBy = truncateString(rl.Metadata.UpdateIdentity, 50) + } + if rl.Metadata.UpdateTime != nil { + updateTime := rl.Metadata.UpdateTime.AsTime() + updatedTime = updateTime.Format(time.RFC3339) + } + } + + return configRow{ + Setting: setting, + Value: value, + Reason: reason, + UpdatedBy: updatedBy, + UpdatedTime: updatedTime, + } +} + +// printTaskQueueConfig is a shared function to print task queue configuration +// This can be used by both the config get command and the describe command +func printTaskQueueConfig(cctx *CommandContext, config *taskqueue.TaskQueueConfig) error { + // For JSON, we'll just dump the proto + if cctx.JSONOutput { + return cctx.Printer.PrintStructured(config, printer.StructuredOptions{}) + } + + // For text, we will use a table + var configRows []configRow + + // Queue Rate Limit + if config.QueueRateLimit != nil { + configRows = append(configRows, buildRateLimitConfigRow("Queue Rate Limit", config.QueueRateLimit, "%.2f rps")) + } + + // Fairness Key Rate Limit Default + if config.FairnessKeysRateLimitDefault != nil { + configRows = append(configRows, buildRateLimitConfigRow("Fairness Key Rate Limit Default", config.FairnessKeysRateLimitDefault, "%.2f rps")) + } + + // Print the config table + if len(configRows) > 0 { + // Always show truncation note, regardless of actual truncation + cctx.Printer.Println(color.YellowString("Note: Long content may be truncated. Use --output json for full details.")) + + return cctx.Printer.PrintStructured(configRows, printer.StructuredOptions{ + Table: &printer.TableOptions{}, + }) + } + + return nil +} diff --git a/temporalcli/commands.taskqueue_config.go b/temporalcli/commands.taskqueue_config.go new file mode 100644 index 000000000..a9de915ef --- /dev/null +++ b/temporalcli/commands.taskqueue_config.go @@ -0,0 +1,163 @@ +package temporalcli + +import ( + "fmt" + "strconv" + "strings" + + "github.com/temporalio/cli/temporalcli/internal/printer" + enums "go.temporal.io/api/enums/v1" + "go.temporal.io/api/taskqueue/v1" + "go.temporal.io/api/workflowservice/v1" +) + +// TaskQueueConfigGetCommand handles getting task queue configuration +func (c *TemporalTaskQueueConfigGetCommand) run(cctx *CommandContext, args []string) error { + // Validate inputs before dialing client + taskQueue := c.TaskQueue + if taskQueue == "" { + return fmt.Errorf("taskQueue name is required") + } + + taskQueueType, err := parseTaskQueueType(c.TaskQueueType.Value) + if err != nil { + return err + } + + namespace := c.Parent.Parent.Namespace + if namespace == "" { + return fmt.Errorf("namespace is required") + } + + cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) + if err != nil { + return err + } + defer cl.Close() + + // Get the task queue configuration + resp, err := cl.WorkflowService().DescribeTaskQueue(cctx, &workflowservice.DescribeTaskQueueRequest{ + Namespace: namespace, + TaskQueue: &taskqueue.TaskQueue{ + Name: taskQueue, + Kind: enums.TASK_QUEUE_KIND_NORMAL, + }, + TaskQueueType: taskQueueType, + ReportConfig: true, + }) + if err != nil { + return fmt.Errorf("error getting task queue config: %w", err) + } + if resp.Config == nil { + cctx.Printer.Println("No configuration found for task queue") + return nil + } + // Print the configuration using the shared function + return printTaskQueueConfig(cctx, resp.Config) +} + +// TaskQueueConfigSetCommand handles setting task queue configuration +func (c *TemporalTaskQueueConfigSetCommand) run(cctx *CommandContext, args []string) error { + // Validate inputs before dialing client + taskQueue := c.TaskQueue + if taskQueue == "" { + return fmt.Errorf("taskQueue name is required") + } + + taskQueueType, err := parseTaskQueueType(c.TaskQueueType.Value) + if err != nil { + return err + } + + namespace := c.Parent.Parent.Namespace + if namespace == "" { + return fmt.Errorf("namespace is required") + } + + // Check workflow task queue restrictions + if taskQueueType == enums.TASK_QUEUE_TYPE_WORKFLOW { + if c.Command.Flags().Changed("queue-rps-limit") || + c.Command.Flags().Changed("queue-rps-limit-reason") { + return fmt.Errorf("setting rate limit on workflow task queues is not allowed") + } + } + + // Helper to parse RPS values for a given flag name. + // Accepts "default" or a non-negative float string. + parseRPS := func(flagName string) (*taskqueue.RateLimit, error) { + raw := strings.TrimSpace(c.Command.Flags().Lookup(flagName).Value.String()) + if raw == "" { + return nil, fmt.Errorf("invalid value for --%s: must be a non-negative number or 'default'", flagName) + } + if strings.EqualFold(raw, "default") { + // Unset: returning nil RateLimit removes the existing rate limit. + return nil, nil + } + v, err := strconv.ParseFloat(raw, 32) + if err != nil { + return nil, fmt.Errorf("invalid value for --%s: must be a non-negative number or 'default'", flagName) + } + if v < 0 { + return nil, fmt.Errorf("invalid value for --%s: must be >= 0 or 'default'", flagName) + } + return &taskqueue.RateLimit{RequestsPerSecond: float32(v)}, nil + } + + var queueRpsLimitParsed *taskqueue.RateLimit + if c.Command.Flags().Changed("queue-rps-limit") { + var err error + if queueRpsLimitParsed, err = parseRPS("queue-rps-limit"); err != nil { + return err + } + } else if c.Command.Flags().Changed("queue-rps-limit-reason") { + return fmt.Errorf("queue-rps-limit-reason can only be set if queue-rps-limit is updated") + } + + var fairnessKeyRpsLimitDefaultParsed *taskqueue.RateLimit + if c.Command.Flags().Changed("fairness-key-rps-limit-default") { + var err error + if fairnessKeyRpsLimitDefaultParsed, err = parseRPS("fairness-key-rps-limit-default"); err != nil { + return err + } + } else if c.Command.Flags().Changed("fairness-key-rps-limit-default-reason") { + return fmt.Errorf("fairness-key-rps-limit-default-reason can only be set if fairness-key-rps-limit-default is updated") + } + + cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) + if err != nil { + return err + } + defer cl.Close() + + request := &workflowservice.UpdateTaskQueueConfigRequest{ + Namespace: namespace, + Identity: c.Parent.Parent.Identity, + TaskQueue: taskQueue, + TaskQueueType: taskQueueType, + } + + // Add queue rate limit if specified (including unset) + if c.Command.Flags().Changed("queue-rps-limit") { + request.UpdateQueueRateLimit = &workflowservice.UpdateTaskQueueConfigRequest_RateLimitUpdate{ + RateLimit: queueRpsLimitParsed, + Reason: c.QueueRpsLimitReason, + } + } + + // Add fairness key rate limit default if specified (including unset) + if c.Command.Flags().Changed("fairness-key-rps-limit-default") { + request.UpdateFairnessKeyRateLimitDefault = &workflowservice.UpdateTaskQueueConfigRequest_RateLimitUpdate{ + RateLimit: fairnessKeyRpsLimitDefaultParsed, + Reason: c.FairnessKeyRpsLimitReason, + } + } + + // Call the API + resp, err := cl.WorkflowService().UpdateTaskQueueConfig(cctx, request) + if err != nil { + return fmt.Errorf("error updating task queue config: %w", err) + } + + cctx.Printer.Println("Successfully updated task queue configuration") + return cctx.Printer.PrintStructured(resp, printer.StructuredOptions{}) +} diff --git a/temporalcli/commands.taskqueue_config_test.go b/temporalcli/commands.taskqueue_config_test.go new file mode 100644 index 000000000..d6fac4b71 --- /dev/null +++ b/temporalcli/commands.taskqueue_config_test.go @@ -0,0 +1,256 @@ +package temporalcli_test + +import ( + "encoding/json" +) + +type taskQueueConfigType struct { + QueueRateLimit *rateLimitConfigType `json:"queueRateLimit,omitempty"` + FairnessKeysRateLimitDefault *rateLimitConfigType `json:"fairnessKeysRateLimitDefault,omitempty"` +} + +type rateLimitConfigType struct { + RateLimit *rateLimitType `json:"rateLimit,omitempty"` + Metadata *metadataType `json:"metadata,omitempty"` +} + +type rateLimitType struct { + RequestsPerSecond float32 `json:"requestsPerSecond"` +} + +type metadataType struct { + Reason string `json:"reason,omitempty"` + UpdateIdentity string `json:"updateIdentity,omitempty"` + UpdateTime string `json:"updateTime,omitempty"` +} + +func (s *SharedServerSuite) TestTaskQueue_Config_Get_Empty() { + // Test getting config for a task queue with no configuration + res := s.Execute( + "task-queue", "config", "get", + "--address", s.Address(), + "--task-queue", s.Worker().Options.TaskQueue, + "--task-queue-type", "activity", + ) + s.NoError(res.Err) + s.Contains(res.Stdout.String(), "No configuration found for task queue") +} + +func (s *SharedServerSuite) TestTaskQueue_Config_Set_And_Get_Both_Limits() { + taskQueue := "test-config-queue-" + s.T().Name() + testIdentity := "test-identity-" + s.T().Name() + + // Set both queue rate limit and fairness key rate limit with explicit identity + res := s.Execute( + "task-queue", "config", "set", + "--address", s.Address(), + "--task-queue", taskQueue, + "--task-queue-type", "activity", + "--identity", testIdentity, + "--queue-rps-limit", "20.0", + "--queue-rps-limit-reason", "queue limit reason", + "--fairness-key-rps-limit-default", "10.0", + "--fairness-key-rps-limit-reason", "fairness limit reason", + ) + s.NoError(res.Err) + s.Contains(res.Stdout.String(), "Successfully updated task queue configuration") + + // Get the configuration and verify both were set using JSON output + res = s.Execute( + "task-queue", "config", "get", + "--address", s.Address(), + "--task-queue", taskQueue, + "--task-queue-type", "activity", + "-o", "json", + ) + s.NoError(res.Err) + + var config taskQueueConfigType + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &config)) + + // Verify queue rate limit + s.NotNil(config.QueueRateLimit) + s.NotNil(config.QueueRateLimit.RateLimit) + s.Equal(float32(20.0), config.QueueRateLimit.RateLimit.RequestsPerSecond) + s.NotNil(config.QueueRateLimit.Metadata) + s.Equal("queue limit reason", config.QueueRateLimit.Metadata.Reason) + s.Equal(testIdentity, config.QueueRateLimit.Metadata.UpdateIdentity) + s.NotEmpty(config.QueueRateLimit.Metadata.UpdateTime) + + // Verify fairness key rate limit + s.NotNil(config.FairnessKeysRateLimitDefault) + s.NotNil(config.FairnessKeysRateLimitDefault.RateLimit) + s.Equal(float32(10.0), config.FairnessKeysRateLimitDefault.RateLimit.RequestsPerSecond) + s.NotNil(config.FairnessKeysRateLimitDefault.Metadata) + s.Equal("fairness limit reason", config.FairnessKeysRateLimitDefault.Metadata.Reason) + s.Equal(testIdentity, config.FairnessKeysRateLimitDefault.Metadata.UpdateIdentity) + s.NotEmpty(config.FairnessKeysRateLimitDefault.Metadata.UpdateTime) +} + +func (s *SharedServerSuite) TestTaskQueue_Config_Unset_Rate_Limits() { + taskQueue := "test-config-queue-" + s.T().Name() + testIdentity := "test-identity-" + s.T().Name() + var config taskQueueConfigType + // Set initial configuration + res := s.Execute( + "task-queue", "config", "set", + "--address", s.Address(), + "--task-queue", taskQueue, + "--task-queue-type", "activity", + "--identity", testIdentity, + "--queue-rps-limit", "10.0", + "--fairness-key-rps-limit-default", "5.0", + ) + s.NoError(res.Err) + + res = s.Execute( + "task-queue", "config", "get", + "--address", s.Address(), + "--task-queue", taskQueue, + "--task-queue-type", "activity", + "-o", "json", + ) + s.NoError(res.Err) + + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &config)) + s.NotNil(config.QueueRateLimit) + s.NotNil(config.QueueRateLimit.RateLimit) + s.Equal(float32(10.0), config.QueueRateLimit.RateLimit.RequestsPerSecond) + s.NotNil(config.FairnessKeysRateLimitDefault) + s.NotNil(config.FairnessKeysRateLimitDefault.RateLimit) + s.Equal(float32(5.0), config.FairnessKeysRateLimitDefault.RateLimit.RequestsPerSecond) + + // Unset queue rate limit (set to default) + res = s.Execute( + "task-queue", "config", "set", + "--address", s.Address(), + "--task-queue", taskQueue, + "--task-queue-type", "activity", + "--identity", testIdentity, + "--queue-rps-limit", "default", + ) + s.NoError(res.Err) + + // Get configuration and verify queue rate limit is unset using JSON output + res = s.Execute( + "task-queue", "config", "get", + "--address", s.Address(), + "--task-queue", taskQueue, + "--task-queue-type", "activity", + "-o", "json", + ) + s.NoError(res.Err) + + var unsetQrlConfig taskQueueConfigType + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &unsetQrlConfig)) + s.NotNil(unsetQrlConfig.QueueRateLimit) + s.Nil(unsetQrlConfig.QueueRateLimit.RateLimit) + s.NotNil(unsetQrlConfig.FairnessKeysRateLimitDefault) + s.Equal(float32(5.0), unsetQrlConfig.FairnessKeysRateLimitDefault.RateLimit.RequestsPerSecond) + + // Unset fairness key rate limit + res = s.Execute( + "task-queue", "config", "set", + "--address", s.Address(), + "--task-queue", taskQueue, + "--task-queue-type", "activity", + "--identity", testIdentity, + "--fairness-key-rps-limit-default", "default", + ) + s.NoError(res.Err) + + // Get configuration and verify both are unset using JSON output + res = s.Execute( + "task-queue", "config", "get", + "--address", s.Address(), + "--task-queue", taskQueue, + "--task-queue-type", "activity", + "-o", "json", + ) + s.NoError(res.Err) + + var unsetFkrlConfig taskQueueConfigType + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &unsetFkrlConfig)) + s.NotNil(unsetFkrlConfig.FairnessKeysRateLimitDefault) + s.Nil(unsetFkrlConfig.FairnessKeysRateLimitDefault.RateLimit) +} + +func (s *SharedServerSuite) TestTaskQueue_Config_Workflow_Task_Queue_Restrictions() { + taskQueue := "test-config-queue-" + s.T().Name() + + // Try to set queue rate limit on workflow task queue (should fail) + res := s.Execute( + "task-queue", "config", "set", + "--address", s.Address(), + "--task-queue", taskQueue, + "--task-queue-type", "workflow", + "--queue-rps-limit", "10.0", + ) + s.Error(res.Err) + + // TODO : add test to check if setting fairness key rate limit on workflow task queue is allowed + // Will be done after the server PR (pending) to allow setting fairness key rate limit on workflow task queues is merged. +} + +func (s *SharedServerSuite) TestTaskQueue_Config_Describe_With_Report_Config() { + taskQueue := "test-config-queue-" + s.T().Name() + testIdentity := "test-identity-" + s.T().Name() + + // Set configuration + res := s.Execute( + "task-queue", "config", "set", + "--address", s.Address(), + "--task-queue", taskQueue, + "--task-queue-type", "activity", + "--identity", testIdentity, + "--queue-rps-limit", "12.5", + "--queue-rps-limit-reason", "describe test", + ) + s.NoError(res.Err) + + // Test JSON output with describe + res = s.Execute( + "task-queue", "describe", + "--address", s.Address(), + "--task-queue", taskQueue, + "--task-queue-type-legacy", "activity", + "--report-config", + "--legacy-mode", + "-o", "json", + ) + s.NoError(res.Err) + + // The JSON output should contain the config section + var result map[string]any + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &result)) + + cfg, ok := result["config"].(map[string]any) + s.True(ok, "config should be an object") + s.NotEmpty(cfg) + + qrl, ok := cfg["queue_rate_limit"].(map[string]any) + s.True(ok, "config.queueRateLimit should be an object") + s.NotEmpty(qrl) + + rl, ok := qrl["rate_limit"].(map[string]any) + s.True(ok, "config.queueRateLimit.RateLimit should be an object") + + rps, ok := rl["requests_per_second"].(float64) + s.True(ok, "requests_per_second should be a number") + s.Equal(12.5, rps) + + md, ok := qrl["metadata"].(map[string]any) + s.True(ok, "metadata should be an object") + s.NotEmpty(md) + + reason, ok := md["reason"].(string) + s.True(ok) + s.Equal("describe test", reason) + + updID, ok := md["update_identity"].(string) + s.True(ok) + s.Equal(testIdentity, updID) + + updTime, _ := md["update_time"].(map[string]any) + s.NotEmpty(updTime) +} diff --git a/temporalcli/commandsgen/code.go b/temporalcli/commandsgen/code.go index e2784f90f..6c67082ba 100644 --- a/temporalcli/commandsgen/code.go +++ b/temporalcli/commandsgen/code.go @@ -421,11 +421,12 @@ func (o *Option) writeFlagBuilding(selfVar, flagVar string, w *codeWriter) error w.writeLinef("%v.%v = %v", selfVar, o.fieldName(), setDefault) } if o.Short != "" { - w.writeLinef("%v.%vP(&%v.%v, %q, %q%v, %q)", - flagVar, flagMeth, selfVar, o.fieldName(), o.Name, o.Short, defaultLit, desc) + w.writeLinef("%v.%vP(&%v.%v, %q, %q%v, %q)", flagVar, flagMeth, selfVar, o.fieldName(), o.Name, o.Short, defaultLit, desc) } else { - w.writeLinef("%v.%v(&%v.%v, %q%v, %q)", - flagVar, flagMeth, selfVar, o.fieldName(), o.Name, defaultLit, desc) + w.writeLinef("%v.%v(&%v.%v, %q%v, %q)", flagVar, flagMeth, selfVar, o.fieldName(), o.Name, defaultLit, desc) + } + if o.DisplayType != "" { + w.writeLinef("overrideFlagDisplayType(%v.Lookup(%q), %q)", flagVar, o.Name, o.DisplayType) } if o.Required { w.writeLinef("_ = %v.MarkFlagRequired(%v, %q)", w.importCobra(), flagVar, o.Name) diff --git a/temporalcli/commandsgen/commands.yml b/temporalcli/commandsgen/commands.yml index 9cbf02389..7f7bcc03a 100644 --- a/temporalcli/commandsgen/commands.yml +++ b/temporalcli/commandsgen/commands.yml @@ -99,6 +99,7 @@ # options: A list of options. (Option[]) # - name: The option name. (string) # type: The option type. (string) +# display-type: Optional custom description for displaying the type in the help (string) # description: The option description. (string) # required: Whether the option is required. (bool) # short: The single letter short version of name (i.e. a for address). (string) @@ -2592,6 +2593,12 @@ commands: - name: disable-stats type: bool description: Disable task queue statistics. + - name: report-config + type: bool + description: | + Include task queue configuration in the response. + When enabled, the command will return the current rate limit + configuration for the task queue. - name: temporal task-queue get-build-id-reachability summary: Show Build ID availability (Deprecated) @@ -3183,6 +3190,119 @@ commands: type: bool description: Don't prompt to confirm. + - name: temporal task-queue config + summary: Get and set Task Queue configuration + description: | + Manage Task Queue configuration: + + ``` + temporal task-queue config [command] [options] + ``` + + Available commands: + - `get`: Retrieve the current configuration for a task queue + - `set`: Update the configuration for a task queue + + - name: temporal task-queue config get + summary: Get Task Queue configuration + description: | + Retrieve the current configuration for a Task Queue: + + ``` + temporal task-queue config get \ + --task-queue YourTaskQueue \ + --task-queue-type activity + ``` + + This command returns the current configuration including: + - Queue rate limit: The overall rate limit of the task queue. + This setting overrides the worker rate limit if set. + Unless modified, this is the system-defined rate limit. + - Fairness key rate limit defaults: Default rate limits for fairness keys. + If set, each individual fairness key will be limited to this rate, + scaled by the weight of the fairness key. + + options: + - name: task-queue + type: string + description: | + Task Queue name. + required: true + short: t + - name: task-queue-type + type: string-enum + description: | + Task Queue type. + Accepted values: workflow, activity, nexus. + required: true + enum-values: + - workflow + - activity + - nexus + + - name: temporal task-queue config set + summary: Set Task Queue configuration + description: | + Update configuration settings for a Task Queue. + + ``` + temporal task-queue config set \ + --task-queue YourTaskQueue \ + --task-queue-type activity \ + --namespace YourNamespace \ + --queue-rps-limit \ + --queue-rps-limit-reason \ + --fairness-key-rps-limit-default \ + --fairness-key-rps-limit-reason + ``` + + This command supports updating: + - Queue rate limits: Controls the overall rate limit of the task queue. + This setting overrides the worker rate limit if set. + Unless modified, this is the system-defined rate limit. + - Fairness key rate limit defaults: Sets default rate limits for fairness keys. + If set, each individual fairness key will be limited to this rate, + scaled by the weight of the fairness key. + + To unset a rate limit, pass in 'default', for example: --queue-rps-limit default + options: + - name: task-queue + type: string + description: | + Task Queue name. + required: true + short: t + - name: task-queue-type + type: string-enum + description: | + Task Queue type. + Accepted values: workflow, activity, nexus. + required: true + enum-values: + - workflow + - activity + - nexus + - name: queue-rps-limit + type: string + display-type: float|default + description: | + Queue rate limit in requests per second. + Accepts a float; or 'default' to unset. + - name: queue-rps-limit-reason + type: string + description: Reason for queue rate limit update. + hidden: true + - name: fairness-key-rps-limit-default + type: string + display-type: float|default + description: | + Fairness key rate limit default in requests per second. + Accepts a float; or 'default' to unset. + - name: fairness-key-rps-limit-reason + type: string + description: Reason for fairness key rate limit update. + hidden: true + - name: temporal workflow summary: Start, list, and operate on Workflows description: | diff --git a/temporalcli/commandsgen/parse.go b/temporalcli/commandsgen/parse.go index 568e38fd7..7945e1c7e 100644 --- a/temporalcli/commandsgen/parse.go +++ b/temporalcli/commandsgen/parse.go @@ -22,6 +22,7 @@ type ( Option struct { Name string `yaml:"name"` Type string `yaml:"type"` + DisplayType string `yaml:"display-type"` Description string `yaml:"description"` Deprecated string `yaml:"deprecated"` Short string `yaml:"short,omitempty"` @@ -215,6 +216,9 @@ func (o *Option) processSection() error { if o.Type == "" { return fmt.Errorf("missing option type") } + if o.Type != "string" && o.DisplayType != "" { + return fmt.Errorf("display-type is only allowed for string options") + } if o.Description == "" { return fmt.Errorf("missing description for option: %s", o.Name) From 4020fc4e42921d267428453fa29139aadd5c8989 Mon Sep 17 00:00:00 2001 From: Roey Berman Date: Fri, 3 Oct 2025 11:11:44 -0700 Subject: [PATCH 06/28] Formatting and merge conflicts --- temporalcli/commands.activity.go | 2 ++ temporalcli/commandsgen/commands.yml | 7 ++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/temporalcli/commands.activity.go b/temporalcli/commands.activity.go index d9550b319..0d297a4a2 100644 --- a/temporalcli/commands.activity.go +++ b/temporalcli/commands.activity.go @@ -376,6 +376,8 @@ func (c *TemporalActivityResetCommand) run(cctx *CommandContext, args []string) request.Activity = &workflowservice.ResetActivityRequest_Type{Type: c.ActivityType} } else if c.ActivityId != "" { request.Activity = &workflowservice.ResetActivityRequest_Id{Id: c.ActivityId} + } else { + return fmt.Errorf("either Activity Type or Activity Id must be provided") } resp, err := cl.WorkflowService().ResetActivity(cctx, request) diff --git a/temporalcli/commandsgen/commands.yml b/temporalcli/commandsgen/commands.yml index 7f7bcc03a..9405fb761 100644 --- a/temporalcli/commandsgen/commands.yml +++ b/temporalcli/commandsgen/commands.yml @@ -337,8 +337,7 @@ commands: summary: Update Activity options description: | Update the options of a running Activity that were passed into it from - a Workflow. Updates are incremental, only changing the specified - options. + a Workflow. Updates are incremental, only changing the specified options. For example: @@ -370,7 +369,6 @@ commands: ... ``` options: - - name: activity-id short: a type: string @@ -541,8 +539,7 @@ commands: - name: match-all type: bool description: | - Every paused activity should be unpaused. This flag is ignored if - activity-type is provided. + Every paused activity should be unpaused. This flag is ignored if activity-type is provided. - name: jitter type: duration description: | From 4113c11255557f050ceee1bb217de916be9147b7 Mon Sep 17 00:00:00 2001 From: Roey Berman Date: Fri, 3 Oct 2025 12:50:38 -0600 Subject: [PATCH 07/28] Bump server to 1.29.0 (#857) --- go.mod | 6 +++--- go.sum | 8 ++++---- temporalcli/commands.gen.go | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 962d5464a..ffa9f813f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/temporalio/cli -go 1.24.5 +go 1.25.0 require ( github.com/BurntSushi/toml v1.4.0 @@ -16,10 +16,10 @@ require ( github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 github.com/temporalio/ui-server/v2 v2.39.0 - go.temporal.io/api v1.52.0 + go.temporal.io/api v1.53.0 go.temporal.io/sdk v1.36.0 go.temporal.io/sdk/contrib/envconfig v0.1.0 - go.temporal.io/server v1.29.0-139.3 + go.temporal.io/server v1.29.0 google.golang.org/grpc v1.72.2 google.golang.org/protobuf v1.36.6 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index c6e37dc53..8e4d4c508 100644 --- a/go.sum +++ b/go.sum @@ -383,14 +383,14 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= -go.temporal.io/api v1.52.0 h1:Tn69z2nhQeXtofa1/j/MbwPHnFRM9+13xqYmFl/KFjM= -go.temporal.io/api v1.52.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/api v1.53.0 h1:6vAFpXaC584AIELa6pONV56MTpkm4Ha7gPWL2acNAjo= +go.temporal.io/api v1.53.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/sdk v1.36.0 h1:WO9zetpybBNK7xsQth4Z+3Zzw1zSaM9MOUGrnnUjZMo= go.temporal.io/sdk v1.36.0/go.mod h1:8BxGRF0LcQlfQrLLGkgVajbsKUp/PY7280XTdcKc18Y= go.temporal.io/sdk/contrib/envconfig v0.1.0 h1:s+G/Ujph+Xl2jzLiiIm2T1vuijDkUL4Kse49dgDVGBE= go.temporal.io/sdk/contrib/envconfig v0.1.0/go.mod h1:FQEO3C56h9C7M6sDgSanB8HnBTmopw9qgVx4F1S6pJk= -go.temporal.io/server v1.29.0-139.3 h1:j4u7esLvLqzE67Q6s/ExvaFKOensFhskCWXe1VHyBdU= -go.temporal.io/server v1.29.0-139.3/go.mod h1:OKFCSB0xxCX1Bmi2rYutWJEKppIrwuqx+c3e5GeY54w= +go.temporal.io/server v1.29.0 h1:BGBCvI7vcPokCjuDsfitLx2eS+8ow+yQ4frLJZcn2nQ= +go.temporal.io/server v1.29.0/go.mod h1:pc0n6DRcN06V4WNhaxdxE3KaZIS3KSDNKdca6uu6RuU= 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= diff --git a/temporalcli/commands.gen.go b/temporalcli/commands.gen.go index 35acc6370..a7a2d44ba 100644 --- a/temporalcli/commands.gen.go +++ b/temporalcli/commands.gen.go @@ -639,9 +639,9 @@ func NewTemporalActivityUpdateOptionsCommand(cctx *CommandContext, parent *Tempo s.Command.Use = "update-options [flags]" s.Command.Short = "Update Activity options" if hasHighlighting { - s.Command.Long = "Update the options of a running Activity that were passed into it from\na Workflow. Updates are incremental, only changing the specified \noptions.\n\nFor example:\n\n\x1b[1mtemporal activity update-options \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId \\\n --task-queue NewTaskQueueName \\\n --schedule-to-close-timeout DURATION \\\n --schedule-to-start-timeout DURATION \\\n --start-to-close-timeout DURATION \\\n --heartbeat-timeout DURATION \\\n --retry-initial-interval DURATION \\\n --retry-maximum-interval DURATION \\\n --retry-backoff-coefficient NewBackoffCoefficient \\\n --retry-maximum-attempts NewMaximumAttempts\x1b[0m\n\nYou may follow this command with \x1b[1mtemporal activity reset\x1b[0m, and the new values will apply after the reset.\n\nEither \x1b[1mactivity-id\x1b[0m, \x1b[1mactivity-type\x1b[0m, or \x1b[1m--match-all\x1b[0m must be specified.\n\nActivity options can be updated in bulk with a visibility query list filter. \nFor example, if you want to reset for activities of type Foo, do:\n\n\x1b[1mtemporal activity update-options \\\n --query 'TemporalPauseInfo=\"property:activityType=Foo\"'\n ...\x1b[0m" + s.Command.Long = "Update the options of a running Activity that were passed into it from\na Workflow. Updates are incremental, only changing the specified options.\n\nFor example:\n\n\x1b[1mtemporal activity update-options \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId \\\n --task-queue NewTaskQueueName \\\n --schedule-to-close-timeout DURATION \\\n --schedule-to-start-timeout DURATION \\\n --start-to-close-timeout DURATION \\\n --heartbeat-timeout DURATION \\\n --retry-initial-interval DURATION \\\n --retry-maximum-interval DURATION \\\n --retry-backoff-coefficient NewBackoffCoefficient \\\n --retry-maximum-attempts NewMaximumAttempts\x1b[0m\n\nYou may follow this command with \x1b[1mtemporal activity reset\x1b[0m, and the new values will apply after the reset.\n\nEither \x1b[1mactivity-id\x1b[0m, \x1b[1mactivity-type\x1b[0m, or \x1b[1m--match-all\x1b[0m must be specified.\n\nActivity options can be updated in bulk with a visibility query list filter. \nFor example, if you want to reset for activities of type Foo, do:\n\n\x1b[1mtemporal activity update-options \\\n --query 'TemporalPauseInfo=\"property:activityType=Foo\"'\n ...\x1b[0m" } else { - s.Command.Long = "Update the options of a running Activity that were passed into it from\na Workflow. Updates are incremental, only changing the specified \noptions.\n\nFor example:\n\n```\ntemporal activity update-options \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId \\\n --task-queue NewTaskQueueName \\\n --schedule-to-close-timeout DURATION \\\n --schedule-to-start-timeout DURATION \\\n --start-to-close-timeout DURATION \\\n --heartbeat-timeout DURATION \\\n --retry-initial-interval DURATION \\\n --retry-maximum-interval DURATION \\\n --retry-backoff-coefficient NewBackoffCoefficient \\\n --retry-maximum-attempts NewMaximumAttempts\n```\n\nYou may follow this command with `temporal activity reset`, and the new values will apply after the reset.\n\nEither `activity-id`, `activity-type`, or `--match-all` must be specified.\n\nActivity options can be updated in bulk with a visibility query list filter. \nFor example, if you want to reset for activities of type Foo, do:\n\n```\ntemporal activity update-options \\\n --query 'TemporalPauseInfo=\"property:activityType=Foo\"'\n ...\n```" + s.Command.Long = "Update the options of a running Activity that were passed into it from\na Workflow. Updates are incremental, only changing the specified options.\n\nFor example:\n\n```\ntemporal activity update-options \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId \\\n --task-queue NewTaskQueueName \\\n --schedule-to-close-timeout DURATION \\\n --schedule-to-start-timeout DURATION \\\n --start-to-close-timeout DURATION \\\n --heartbeat-timeout DURATION \\\n --retry-initial-interval DURATION \\\n --retry-maximum-interval DURATION \\\n --retry-backoff-coefficient NewBackoffCoefficient \\\n --retry-maximum-attempts NewMaximumAttempts\n```\n\nYou may follow this command with `temporal activity reset`, and the new values will apply after the reset.\n\nEither `activity-id`, `activity-type`, or `--match-all` must be specified.\n\nActivity options can be updated in bulk with a visibility query list filter. \nFor example, if you want to reset for activities of type Foo, do:\n\n```\ntemporal activity update-options \\\n --query 'TemporalPauseInfo=\"property:activityType=Foo\"'\n ...\n```" } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVarP(&s.ActivityId, "activity-id", "a", "", "The Activity ID to update options. Mutually exclusive with `--query`, `--match-all`, and `--activity-type`. Requires `--workflow-id` to be specified.") From 7e319a6ba4bdf822e4943e0656254ebef029c6df Mon Sep 17 00:00:00 2001 From: Carly de Frondeville Date: Wed, 8 Oct 2025 15:33:57 -0700 Subject: [PATCH 08/28] Update Go SDK to v1.37 (#859) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What was changed Update Go SDK to v1.37 ## Why? To use new APIs and flags ## Checklist 1. Closes 2. How was this tested: 3. Any docs updates needed? --- go.mod | 2 +- go.sum | 4 +- temporalcli/commands.worker.deployment.go | 16 ++-- .../commands.worker.deployment_test.go | 86 +++++++++---------- temporalcli/commands.workflow.go | 8 +- temporalcli/commands.workflow_test.go | 28 +++--- temporalcli/commands.workflow_view_test.go | 12 +-- 7 files changed, 78 insertions(+), 78 deletions(-) diff --git a/go.mod b/go.mod index ffa9f813f..c0cd08d04 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/stretchr/testify v1.10.0 github.com/temporalio/ui-server/v2 v2.39.0 go.temporal.io/api v1.53.0 - go.temporal.io/sdk v1.36.0 + go.temporal.io/sdk v1.37.0 go.temporal.io/sdk/contrib/envconfig v0.1.0 go.temporal.io/server v1.29.0 google.golang.org/grpc v1.72.2 diff --git a/go.sum b/go.sum index 8e4d4c508..202655411 100644 --- a/go.sum +++ b/go.sum @@ -385,8 +385,8 @@ go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.temporal.io/api v1.53.0 h1:6vAFpXaC584AIELa6pONV56MTpkm4Ha7gPWL2acNAjo= go.temporal.io/api v1.53.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= -go.temporal.io/sdk v1.36.0 h1:WO9zetpybBNK7xsQth4Z+3Zzw1zSaM9MOUGrnnUjZMo= -go.temporal.io/sdk v1.36.0/go.mod h1:8BxGRF0LcQlfQrLLGkgVajbsKUp/PY7280XTdcKc18Y= +go.temporal.io/sdk v1.37.0 h1:RbwCkUQuqY4rfCzdrDZF9lgT7QWG/pHlxfZFq0NPpDQ= +go.temporal.io/sdk v1.37.0/go.mod h1:tOy6vGonfAjrpCl6Bbw/8slTgQMiqvoyegRv2ZHPm5M= go.temporal.io/sdk/contrib/envconfig v0.1.0 h1:s+G/Ujph+Xl2jzLiiIm2T1vuijDkUL4Kse49dgDVGBE= go.temporal.io/sdk/contrib/envconfig v0.1.0/go.mod h1:FQEO3C56h9C7M6sDgSanB8HnBTmopw9qgVx4F1S6pJk= go.temporal.io/server v1.29.0 h1:BGBCvI7vcPokCjuDsfitLx2eS+8ow+yQ4frLJZcn2nQ= diff --git a/temporalcli/commands.worker.deployment.go b/temporalcli/commands.worker.deployment.go index 072fd838f..3d184846f 100644 --- a/temporalcli/commands.worker.deployment.go +++ b/temporalcli/commands.worker.deployment.go @@ -93,7 +93,7 @@ func formatVersionSummaries(vss []client.WorkerDeploymentVersionSummary) ([]vers } vsRows = append(vsRows, versionSummariesRowType{ DeploymentName: vs.Version.DeploymentName, - BuildId: vs.Version.BuildId, + BuildId: vs.Version.BuildID, CreateTime: vs.CreateTime, DrainageStatus: drainageStr, }) @@ -108,11 +108,11 @@ func formatRoutingConfig(rc client.WorkerDeploymentRoutingConfig) (formattedRout rvbid := "" if rc.CurrentVersion != nil { cvdn = rc.CurrentVersion.DeploymentName - cvbid = rc.CurrentVersion.BuildId + cvbid = rc.CurrentVersion.BuildID } if rc.RampingVersion != nil { rvdn = rc.RampingVersion.DeploymentName - rvbid = rc.RampingVersion.BuildId + rvbid = rc.RampingVersion.BuildID } return formattedRoutingConfigType{ CurrentVersionDeploymentName: cvdn, @@ -161,11 +161,11 @@ func printWorkerDeploymentInfo(cctx *CommandContext, deploymentInfo client.Worke rampVerBuildId := "" if deploymentInfo.RoutingConfig.CurrentVersion != nil { curVerDepName = deploymentInfo.RoutingConfig.CurrentVersion.DeploymentName - curVerBuildId = deploymentInfo.RoutingConfig.CurrentVersion.BuildId + curVerBuildId = deploymentInfo.RoutingConfig.CurrentVersion.BuildID } if deploymentInfo.RoutingConfig.RampingVersion != nil { rampVerDepName = deploymentInfo.RoutingConfig.RampingVersion.DeploymentName - rampVerBuildId = deploymentInfo.RoutingConfig.RampingVersion.BuildId + rampVerBuildId = deploymentInfo.RoutingConfig.RampingVersion.BuildID } printMe := struct { Name string @@ -261,7 +261,7 @@ func workerDeploymentVersionInfoToRows(deploymentInfo client.WorkerDeploymentVer return formattedWorkerDeploymentVersionInfoType{ DeploymentName: deploymentInfo.Version.DeploymentName, - BuildId: deploymentInfo.Version.BuildId, + BuildId: deploymentInfo.Version.BuildID, CreateTime: deploymentInfo.CreateTime, RoutingChangedTime: deploymentInfo.RoutingChangedTime, CurrentSinceTime: deploymentInfo.CurrentSinceTime, @@ -307,7 +307,7 @@ func printWorkerDeploymentVersionInfo(cctx *CommandContext, deploymentInfo clien Metadata map[string]*common.Payload `cli:",cardOmitEmpty"` }{ DeploymentName: deploymentInfo.Version.DeploymentName, - BuildId: deploymentInfo.Version.BuildId, + BuildId: deploymentInfo.Version.BuildID, CreateTime: deploymentInfo.CreateTime, RoutingChangedTime: deploymentInfo.RoutingChangedTime, CurrentSinceTime: deploymentInfo.CurrentSinceTime, @@ -620,7 +620,7 @@ func (c *TemporalWorkerDeploymentUpdateMetadataVersionCommand) run(cctx *Command dHandle := cl.WorkerDeploymentClient().GetHandle(c.DeploymentName) response, err := dHandle.UpdateVersionMetadata(cctx, client.WorkerDeploymentUpdateVersionMetadataOptions{ Version: worker.WorkerDeploymentVersion{ - BuildId: c.BuildId, + BuildID: c.BuildId, DeploymentName: c.DeploymentName, }, MetadataUpdate: client.WorkerDeploymentMetadataUpdate{ diff --git a/temporalcli/commands.worker.deployment_test.go b/temporalcli/commands.worker.deployment_test.go index f72b8b40a..0b5b7a5e4 100644 --- a/temporalcli/commands.worker.deployment_test.go +++ b/temporalcli/commands.worker.deployment_test.go @@ -69,7 +69,7 @@ func (s *SharedServerSuite) TestDeployment_Set_Current_Version() { buildId := uuid.NewString() version := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId, + BuildID: buildId, } w := s.DevServer.StartDevWorker(s.Suite.T(), DevWorkerOptions{ Worker: worker.Options{ @@ -95,7 +95,7 @@ func (s *SharedServerSuite) TestDeployment_Set_Current_Version() { res := s.Execute( "worker", "deployment", "describe-version", "--address", s.Address(), - "--deployment-name", version.DeploymentName, "--build-id", version.BuildId, + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, ) assert.NoError(t, res.Err) }, 30*time.Second, 100*time.Millisecond) @@ -103,7 +103,7 @@ func (s *SharedServerSuite) TestDeployment_Set_Current_Version() { res := s.Execute( "worker", "deployment", "set-current-version", "--address", s.Address(), - "--deployment-name", version.DeploymentName, "--build-id", version.BuildId, + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, "--yes", ) s.NoError(res.Err) @@ -117,7 +117,7 @@ func (s *SharedServerSuite) TestDeployment_Set_Current_Version() { s.ContainsOnSameLine(res.Stdout.String(), "Name", deploymentName) s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionDeploymentName", version.DeploymentName) - s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version.BuildID) // json res = s.Execute( @@ -132,13 +132,13 @@ func (s *SharedServerSuite) TestDeployment_Set_Current_Version() { s.NoError(json.Unmarshal(res.Stdout.Bytes(), &jsonOut)) s.Equal(deploymentName, jsonOut.Name) s.Equal(version.DeploymentName, jsonOut.RoutingConfig.CurrentVersionDeploymentName) - s.Equal(version.BuildId, jsonOut.RoutingConfig.CurrentVersionBuildID) + s.Equal(version.BuildID, jsonOut.RoutingConfig.CurrentVersionBuildID) // set metadata res = s.Execute( "worker", "deployment", "update-metadata-version", "--address", s.Address(), - "--deployment-name", version.DeploymentName, "--build-id", version.BuildId, + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, "--metadata", "bar=1", "--output", "json", ) @@ -155,7 +155,7 @@ func (s *SharedServerSuite) TestDeployment_Set_Current_Version() { res = s.Execute( "worker", "deployment", "update-metadata-version", "--address", s.Address(), - "--deployment-name", version.DeploymentName, "--build-id", version.BuildId, + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, "--remove-entries", "bar", "--output", "json", ) @@ -164,7 +164,7 @@ func (s *SharedServerSuite) TestDeployment_Set_Current_Version() { res = s.Execute( "worker", "deployment", "describe-version", "--address", s.Address(), - "--deployment-name", version.DeploymentName, "--build-id", version.BuildId, + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, "--output", "json", ) s.NoError(res.Err) @@ -190,11 +190,11 @@ func (s *SharedServerSuite) TestDeployment_List() { buildId2 := uuid.NewString() version1 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName1, - BuildId: buildId1, + BuildID: buildId1, } version2 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName2, - BuildId: buildId2, + BuildID: buildId2, } w1 := s.DevServer.StartDevWorker(s.Suite.T(), DevWorkerOptions{ @@ -233,13 +233,13 @@ func (s *SharedServerSuite) TestDeployment_List() { res := s.Execute( "worker", "deployment", "describe-version", "--address", s.Address(), - "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildId, + "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildID, ) assert.NoError(t, res.Err) res = s.Execute( "worker", "deployment", "describe-version", "--address", s.Address(), - "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildId, + "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildID, ) assert.NoError(t, res.Err) }, 30*time.Second, 100*time.Millisecond) @@ -247,7 +247,7 @@ func (s *SharedServerSuite) TestDeployment_List() { res := s.Execute( "worker", "deployment", "set-current-version", "--address", s.Address(), - "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildId, + "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildID, "--yes", ) s.NoError(res.Err) @@ -255,7 +255,7 @@ func (s *SharedServerSuite) TestDeployment_List() { res = s.Execute( "worker", "deployment", "set-current-version", "--address", s.Address(), - "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildId, + "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildID, "--yes", ) s.NoError(res.Err) @@ -268,8 +268,8 @@ func (s *SharedServerSuite) TestDeployment_List() { s.NoError(res.Err) }, 10*time.Second, 100*time.Millisecond) - s.ContainsOnSameLine(res.Stdout.String(), deploymentName1, version1.BuildId) - s.ContainsOnSameLine(res.Stdout.String(), deploymentName2, version2.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), deploymentName1, version1.BuildID) + s.ContainsOnSameLine(res.Stdout.String(), deploymentName2, version2.BuildID) // json res = s.Execute( @@ -288,10 +288,10 @@ func (s *SharedServerSuite) TestDeployment_List() { s.Equal(2, len(jsonOut)) s.Equal(deploymentName1, jsonOut[0].Name) s.Equal(version1.DeploymentName, jsonOut[0].RoutingConfig.CurrentVersionDeploymentName) - s.Equal(version1.BuildId, jsonOut[0].RoutingConfig.CurrentVersionBuildID) + s.Equal(version1.BuildID, jsonOut[0].RoutingConfig.CurrentVersionBuildID) s.Equal(deploymentName2, jsonOut[1].Name) s.Equal(version2.DeploymentName, jsonOut[1].RoutingConfig.CurrentVersionDeploymentName) - s.Equal(version2.BuildId, jsonOut[1].RoutingConfig.CurrentVersionBuildID) + s.Equal(version2.BuildID, jsonOut[1].RoutingConfig.CurrentVersionBuildID) } func (s *SharedServerSuite) TestDeployment_Describe_Drainage() { @@ -300,11 +300,11 @@ func (s *SharedServerSuite) TestDeployment_Describe_Drainage() { buildId2 := "b" + uuid.NewString() version1 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId1, + BuildID: buildId1, } version2 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId2, + BuildID: buildId2, } w1 := s.DevServer.StartDevWorker(s.Suite.T(), DevWorkerOptions{ @@ -342,13 +342,13 @@ func (s *SharedServerSuite) TestDeployment_Describe_Drainage() { res := s.Execute( "worker", "deployment", "describe-version", "--address", s.Address(), - "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildId, + "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildID, ) assert.NoError(t, res.Err) res = s.Execute( "worker", "deployment", "describe-version", "--address", s.Address(), - "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildId, + "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildID, ) assert.NoError(t, res.Err) }, 30*time.Second, 100*time.Millisecond) @@ -356,7 +356,7 @@ func (s *SharedServerSuite) TestDeployment_Describe_Drainage() { res := s.Execute( "worker", "deployment", "set-current-version", "--address", s.Address(), - "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildId, + "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildID, "--yes", ) s.NoError(res.Err) @@ -368,13 +368,13 @@ func (s *SharedServerSuite) TestDeployment_Describe_Drainage() { ) s.NoError(res.Err) s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionDeploymentName", version1.DeploymentName) - s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version1.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version1.BuildID) fmt.Print("hello") res = s.Execute( "worker", "deployment", "set-current-version", "--address", s.Address(), - "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildId, + "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildID, "--yes", ) s.NoError(res.Err) @@ -387,7 +387,7 @@ func (s *SharedServerSuite) TestDeployment_Describe_Drainage() { s.NoError(res.Err) s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionDeploymentName", version2.DeploymentName) - s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version2.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version2.BuildID) s.ContainsOnSameLine(res.Stdout.String(), version1.DeploymentName, "draining") s.ContainsOnSameLine(res.Stdout.String(), version2.DeploymentName, "unspecified") @@ -409,9 +409,9 @@ func (s *SharedServerSuite) TestDeployment_Describe_Drainage() { s.Equal(2, len(jsonOut.VersionSummaries)) s.Equal("draining", jsonOut.VersionSummaries[0].DrainageStatus) - s.Equal(version1.BuildId, jsonOut.VersionSummaries[0].BuildID) + s.Equal(version1.BuildID, jsonOut.VersionSummaries[0].BuildID) s.Equal("unspecified", jsonOut.VersionSummaries[1].DrainageStatus) - s.Equal(version2.BuildId, jsonOut.VersionSummaries[1].BuildID) + s.Equal(version2.BuildID, jsonOut.VersionSummaries[1].BuildID) } func (s *SharedServerSuite) TestDeployment_Ramping() { @@ -420,11 +420,11 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { buildId2 := "b" + uuid.NewString() version1 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId1, + BuildID: buildId1, } version2 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId2, + BuildID: buildId2, } w1 := s.DevServer.StartDevWorker(s.Suite.T(), DevWorkerOptions{ @@ -462,13 +462,13 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { res := s.Execute( "worker", "deployment", "describe-version", "--address", s.Address(), - "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildId, + "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildID, ) assert.NoError(t, res.Err) res = s.Execute( "worker", "deployment", "describe-version", "--address", s.Address(), - "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildId, + "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildID, ) assert.NoError(t, res.Err) }, 30*time.Second, 100*time.Millisecond) @@ -476,7 +476,7 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { res := s.Execute( "worker", "deployment", "set-current-version", "--address", s.Address(), - "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildId, + "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildID, "--yes", ) s.NoError(res.Err) @@ -484,7 +484,7 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { res = s.Execute( "worker", "deployment", "set-ramping-version", "--address", s.Address(), - "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildId, + "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildID, "--percentage", "12.5", "--yes", ) @@ -497,16 +497,16 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { ) s.NoError(res.Err) s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionDeploymentName", version1.DeploymentName) - s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version1.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version1.BuildID) s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionDeploymentName", version2.DeploymentName) - s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionBuildID", version2.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionBuildID", version2.BuildID) s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionPercentage", "12.5") // setting version2 as current also removes the ramp res = s.Execute( "worker", "deployment", "set-current-version", "--address", s.Address(), - "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildId, + "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildID, "--yes", ) s.NoError(res.Err) @@ -523,13 +523,13 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { s.NoError(json.Unmarshal(res.Stdout.Bytes(), &jsonOut)) s.Equal(deploymentName, jsonOut.Name) s.Empty(jsonOut.RoutingConfig.RampingVersionBuildID) - s.Equal(version2.BuildId, jsonOut.RoutingConfig.CurrentVersionBuildID) + s.Equal(version2.BuildID, jsonOut.RoutingConfig.CurrentVersionBuildID) //same with explicit delete res = s.Execute( "worker", "deployment", "set-ramping-version", "--address", s.Address(), - "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildId, + "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildID, "--percentage", "10.1", "--yes", ) @@ -542,15 +542,15 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { ) s.NoError(res.Err) s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionDeploymentName", version2.DeploymentName) - s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version2.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version2.BuildID) s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionDeploymentName", version1.DeploymentName) - s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionBuildID", version1.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionBuildID", version1.BuildID) s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionPercentage", "10.1") res = s.Execute( "worker", "deployment", "set-ramping-version", "--address", s.Address(), - "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildId, + "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildID, "--delete", "--yes", ) @@ -567,5 +567,5 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { s.NoError(json.Unmarshal(res.Stdout.Bytes(), &jsonOut)) s.Equal(deploymentName, jsonOut.Name) s.Equal(float32(0), jsonOut.RoutingConfig.RampingVersionPercentage) - s.Equal(version2.BuildId, jsonOut.RoutingConfig.CurrentVersionBuildID) + s.Equal(version2.BuildID, jsonOut.RoutingConfig.CurrentVersionBuildID) } diff --git a/temporalcli/commands.workflow.go b/temporalcli/commands.workflow.go index 6b9b8714b..4daf81b91 100644 --- a/temporalcli/commands.workflow.go +++ b/temporalcli/commands.workflow.go @@ -129,7 +129,7 @@ func (c *TemporalWorkflowUpdateOptionsCommand) run(cctx *CommandContext, args [] Value: &client.PinnedVersioningOverride{ Version: worker.WorkerDeploymentVersion{ DeploymentName: c.VersioningOverrideDeploymentName, - BuildId: c.VersioningOverrideBuildId, + BuildID: c.VersioningOverrideBuildId, }, }, } @@ -698,17 +698,17 @@ func versioningOverrideToProto(versioningOverride client.VersioningOverride) *wo case *client.PinnedVersioningOverride: return &workflowpb.VersioningOverride{ Behavior: enums.VERSIONING_BEHAVIOR_PINNED, - PinnedVersion: fmt.Sprintf("%s.%s", v.Version.DeploymentName, v.Version.BuildId), + PinnedVersion: fmt.Sprintf("%s.%s", v.Version.DeploymentName, v.Version.BuildID), Deployment: &deploymentpb.Deployment{ SeriesName: v.Version.DeploymentName, - BuildId: v.Version.BuildId, + BuildId: v.Version.BuildID, }, Override: &workflowpb.VersioningOverride_Pinned{ Pinned: &workflowpb.VersioningOverride_PinnedOverride{ Behavior: workflowpb.VersioningOverride_PINNED_OVERRIDE_BEHAVIOR_PINNED, Version: &deploymentpb.WorkerDeploymentVersion{ DeploymentName: v.Version.DeploymentName, - BuildId: v.Version.BuildId, + BuildId: v.Version.BuildID, }, }, }, diff --git a/temporalcli/commands.workflow_test.go b/temporalcli/commands.workflow_test.go index 775b7000e..91024ed66 100644 --- a/temporalcli/commands.workflow_test.go +++ b/temporalcli/commands.workflow_test.go @@ -474,11 +474,11 @@ func (s *SharedServerSuite) TestWorkflow_Batch_Update_Options_Versioning_Overrid deploymentName := uuid.NewString() version1 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId1, + BuildID: buildId1, } version2 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId2, + BuildID: buildId2, } // Workflow that waits to be canceled. waitingWorkflow := func(ctx workflow.Context) error { @@ -511,7 +511,7 @@ func (s *SharedServerSuite) TestWorkflow_Batch_Update_Options_Versioning_Overrid "worker", "deployment", "describe-version", "--address", s.Address(), "--deployment-name", version1.DeploymentName, - "--build-id", version1.BuildId, + "--build-id", version1.BuildID, ) assert.NoError(t, res.Err) }, 30*time.Second, 100*time.Millisecond) @@ -520,7 +520,7 @@ func (s *SharedServerSuite) TestWorkflow_Batch_Update_Options_Versioning_Overrid "worker", "deployment", "set-current-version", "--address", s.Address(), "--deployment-name", version1.DeploymentName, - "--build-id", version1.BuildId, + "--build-id", version1.BuildID, "--yes", ) s.NoError(res.Err) @@ -551,7 +551,7 @@ func (s *SharedServerSuite) TestWorkflow_Batch_Update_Options_Versioning_Overrid ) assert.NoError(t, res.Err) assert.Contains(t, res.Stdout.String(), version1.DeploymentName) - assert.Contains(t, res.Stdout.String(), version1.BuildId) + assert.Contains(t, res.Stdout.String(), version1.BuildID) assert.Contains(t, res.Stdout.String(), "Pinned") } }, 30*time.Second, 100*time.Millisecond) @@ -572,7 +572,7 @@ func (s *SharedServerSuite) TestWorkflow_Batch_Update_Options_Versioning_Overrid "--query", "CustomKeywordField = '"+searchAttr+"'", "--versioning-override-behavior", "pinned", "--versioning-override-deployment-name", version2.DeploymentName, - "--versioning-override-build-id", version2.BuildId, + "--versioning-override-build-id", version2.BuildID, ) s.NoError(res.Err) time.Sleep(10 * time.Second) @@ -602,7 +602,7 @@ func (s *SharedServerSuite) TestWorkflow_Batch_Update_Options_Versioning_Overrid require.NotNil(t, versioningInfo.VersioningOverride) asPinned := versioningInfo.VersioningOverride.Override.(*workflowpb.VersioningOverride_Pinned) require.Equal(t, version2.DeploymentName, asPinned.Pinned.Version.DeploymentName) - require.Equal(t, version2.BuildId, asPinned.Pinned.Version.BuildId) + require.Equal(t, version2.BuildID, asPinned.Pinned.Version.BuildId) require.Equal(t, enums.VERSIONING_BEHAVIOR_PINNED, versioningInfo.Behavior) } }, 10*time.Second, 100*time.Millisecond) @@ -615,11 +615,11 @@ func (s *SharedServerSuite) TestWorkflow_Update_Options_Versioning_Override() { deploymentName := uuid.NewString() version1 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId1, + BuildID: buildId1, } version2 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId2, + BuildID: buildId2, } // Workflow that waits to be canceled. @@ -653,7 +653,7 @@ func (s *SharedServerSuite) TestWorkflow_Update_Options_Versioning_Override() { "worker", "deployment", "describe-version", "--address", s.Address(), "--deployment-name", version1.DeploymentName, - "--build-id", version1.BuildId, + "--build-id", version1.BuildID, ) assert.NoError(t, res.Err) }, 30*time.Second, 100*time.Millisecond) @@ -662,7 +662,7 @@ func (s *SharedServerSuite) TestWorkflow_Update_Options_Versioning_Override() { "worker", "deployment", "set-current-version", "--address", s.Address(), "--deployment-name", version1.DeploymentName, - "--build-id", version1.BuildId, + "--build-id", version1.BuildID, "--yes", ) s.NoError(res.Err) @@ -683,7 +683,7 @@ func (s *SharedServerSuite) TestWorkflow_Update_Options_Versioning_Override() { ) assert.NoError(t, res.Err) assert.Contains(t, res.Stdout.String(), version1.DeploymentName) - assert.Contains(t, res.Stdout.String(), version1.BuildId) + assert.Contains(t, res.Stdout.String(), version1.BuildID) assert.Contains(t, res.Stdout.String(), "Pinned") }, 30*time.Second, 100*time.Millisecond) @@ -693,7 +693,7 @@ func (s *SharedServerSuite) TestWorkflow_Update_Options_Versioning_Override() { "-w", run.GetID(), "--versioning-override-behavior", "pinned", "--versioning-override-deployment-name", version2.DeploymentName, - "--versioning-override-build-id", version2.BuildId, + "--versioning-override-build-id", version2.BuildID, ) s.NoError(res.Err) @@ -706,7 +706,7 @@ func (s *SharedServerSuite) TestWorkflow_Update_Options_Versioning_Override() { s.ContainsOnSameLine(res.Stdout.String(), "OverrideBehavior", "Pinned") s.ContainsOnSameLine(res.Stdout.String(), "OverridePinnedVersionDeploymentName", version2.DeploymentName) - s.ContainsOnSameLine(res.Stdout.String(), "OverridePinnedVersionBuildId", version2.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), "OverridePinnedVersionBuildId", version2.BuildID) // Using only build-id res = s.Execute( diff --git a/temporalcli/commands.workflow_view_test.go b/temporalcli/commands.workflow_view_test.go index 1e9150755..3ac697209 100644 --- a/temporalcli/commands.workflow_view_test.go +++ b/temporalcli/commands.workflow_view_test.go @@ -560,7 +560,7 @@ func (s *SharedServerSuite) TestWorkflow_Describe_Deployment() { } version := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId, + BuildID: buildId, } w := s.DevServer.StartDevWorker(s.Suite.T(), DevWorkerOptions{ Worker: worker.Options{ @@ -588,7 +588,7 @@ func (s *SharedServerSuite) TestWorkflow_Describe_Deployment() { "worker", "deployment", "describe-version", "--address", s.Address(), "--deployment-name", version.DeploymentName, - "--build-id", version.BuildId, + "--build-id", version.BuildID, ) assert.NoError(t, res.Err) }, 30*time.Second, 100*time.Millisecond) @@ -597,7 +597,7 @@ func (s *SharedServerSuite) TestWorkflow_Describe_Deployment() { "worker", "deployment", "set-current-version", "--address", s.Address(), "--deployment-name", version.DeploymentName, - "--build-id", version.BuildId, + "--build-id", version.BuildID, "--yes", ) s.NoError(res.Err) @@ -618,14 +618,14 @@ func (s *SharedServerSuite) TestWorkflow_Describe_Deployment() { ) assert.NoError(t, res.Err) assert.Contains(t, res.Stdout.String(), version.DeploymentName) - assert.Contains(t, res.Stdout.String(), version.BuildId) + assert.Contains(t, res.Stdout.String(), version.BuildID) assert.Contains(t, res.Stdout.String(), "Pinned") }, 30*time.Second, 100*time.Millisecond) out := res.Stdout.String() s.ContainsOnSameLine(out, "Behavior", "Pinned") s.ContainsOnSameLine(out, "DeploymentName", version.DeploymentName) - s.ContainsOnSameLine(out, "BuildId", version.BuildId) + s.ContainsOnSameLine(out, "BuildId", version.BuildID) // json res = s.Execute( @@ -640,7 +640,7 @@ func (s *SharedServerSuite) TestWorkflow_Describe_Deployment() { s.NoError(temporalcli.UnmarshalProtoJSONWithOptions(res.Stdout.Bytes(), &jsonResp, true)) versioningInfo := jsonResp.WorkflowExecutionInfo.VersioningInfo s.Equal("Pinned", versioningInfo.Behavior.String()) - s.Equal(version.BuildId, versioningInfo.DeploymentVersion.BuildId) + s.Equal(version.BuildID, versioningInfo.DeploymentVersion.BuildId) s.Equal(version.DeploymentName, versioningInfo.DeploymentVersion.DeploymentName) s.Nil(versioningInfo.VersioningOverride) } From 9999fb4456ea4b8010baf8b988c55a72836b58cf Mon Sep 17 00:00:00 2001 From: Carly de Frondeville Date: Thu, 9 Oct 2025 09:28:06 -0700 Subject: [PATCH 09/28] Add `temporal worker deployment manager-identity` commands (#858) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What was changed Add `temporal worker deployment manager-identity` commands (`set` and `unset`) ## Why? So that CLI clients of Worker Deployments that are managed by an automated system (such as the Kubernetes Worker Controller) can safely coordinate with that client to make intended changes and ensure they are not overwritten. ## Checklist 1. Closes 2. How was this tested: Added a new test. Also tested in SDK and server. 3. Any docs updates needed? --- temporalcli/commands.gen.go | 84 ++++++++++ temporalcli/commands.worker.deployment.go | 98 ++++++++++-- .../commands.worker.deployment_test.go | 77 ++++++++- temporalcli/commandsgen/commands.yml | 148 ++++++++++++++++++ 4 files changed, 390 insertions(+), 17 deletions(-) diff --git a/temporalcli/commands.gen.go b/temporalcli/commands.gen.go index a7a2d44ba..eee204100 100644 --- a/temporalcli/commands.gen.go +++ b/temporalcli/commands.gen.go @@ -2891,6 +2891,7 @@ func NewTemporalWorkerDeploymentCommand(cctx *CommandContext, parent *TemporalWo s.Command.AddCommand(&NewTemporalWorkerDeploymentDescribeCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkerDeploymentDescribeVersionCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkerDeploymentListCommand(cctx, &s).Command) + s.Command.AddCommand(&NewTemporalWorkerDeploymentManagerIdentityCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkerDeploymentSetCurrentVersionCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkerDeploymentSetRampingVersionCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkerDeploymentUpdateMetadataVersionCommand(cctx, &s).Command) @@ -3032,6 +3033,89 @@ func NewTemporalWorkerDeploymentListCommand(cctx *CommandContext, parent *Tempor return &s } +type TemporalWorkerDeploymentManagerIdentityCommand struct { + Parent *TemporalWorkerDeploymentCommand + Command cobra.Command +} + +func NewTemporalWorkerDeploymentManagerIdentityCommand(cctx *CommandContext, parent *TemporalWorkerDeploymentCommand) *TemporalWorkerDeploymentManagerIdentityCommand { + var s TemporalWorkerDeploymentManagerIdentityCommand + s.Parent = parent + s.Command.Use = "manager-identity" + s.Command.Short = "Manager Identity commands change the `ManagerIdentity` of a Worker Deployment" + if hasHighlighting { + s.Command.Long = "\x1b[1m+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\x1b[0m\n\nManager Identity commands change the \x1b[1mManagerIdentity\x1b[0m of a Worker Deployment:\n\n\x1b[1mtemporal worker deployment manager-identity [command] [options]\x1b[0m\n\nWhen present, \x1b[1mManagerIdentity\x1b[0m is the identity of the user that has the \nexclusive right to make changes to this Worker Deployment. Empty by default.\nWhen set, users whose identity does not match the \x1b[1mManagerIdentity\x1b[0m will not\nbe able to change the Worker Deployment.\n\nThis is especially useful in environments where multiple users (such as CLI\nusers and automated controllers) may interact with the same Worker Deployment.\n\x1b[1mManagerIdentity\x1b[0m allows different users to communicate with one another about\nwho is expected to make changes to the Worker Deployment.\n\nThe current Manager Identity is returned with \x1b[1mdescribe\x1b[0m:\n\x1b[1m temporal worker deployment describe \\\n --deployment-name YourDeploymentName\x1b[0m" + } else { + s.Command.Long = "```\n+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\n```\n\nManager Identity commands change the `ManagerIdentity` of a Worker Deployment:\n\n```\ntemporal worker deployment manager-identity [command] [options]\n```\n\nWhen present, `ManagerIdentity` is the identity of the user that has the \nexclusive right to make changes to this Worker Deployment. Empty by default.\nWhen set, users whose identity does not match the `ManagerIdentity` will not\nbe able to change the Worker Deployment.\n\nThis is especially useful in environments where multiple users (such as CLI\nusers and automated controllers) may interact with the same Worker Deployment.\n`ManagerIdentity` allows different users to communicate with one another about\nwho is expected to make changes to the Worker Deployment.\n\nThe current Manager Identity is returned with `describe`:\n```\n temporal worker deployment describe \\\n --deployment-name YourDeploymentName\n```" + } + s.Command.Args = cobra.NoArgs + s.Command.AddCommand(&NewTemporalWorkerDeploymentManagerIdentitySetCommand(cctx, &s).Command) + s.Command.AddCommand(&NewTemporalWorkerDeploymentManagerIdentityUnsetCommand(cctx, &s).Command) + return &s +} + +type TemporalWorkerDeploymentManagerIdentitySetCommand struct { + Parent *TemporalWorkerDeploymentManagerIdentityCommand + Command cobra.Command + ManagerIdentity string + Self bool + DeploymentName string + Yes bool +} + +func NewTemporalWorkerDeploymentManagerIdentitySetCommand(cctx *CommandContext, parent *TemporalWorkerDeploymentManagerIdentityCommand) *TemporalWorkerDeploymentManagerIdentitySetCommand { + var s TemporalWorkerDeploymentManagerIdentitySetCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "set [flags]" + s.Command.Short = "Set the Manager Identity of a Worker Deployment" + if hasHighlighting { + s.Command.Long = "\x1b[1m+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\x1b[0m\n\nSet the \x1b[1mManagerIdentity\x1b[0m of a Worker Deployment given its Deployment Name.\n\nWhen present, \x1b[1mManagerIdentity\x1b[0m is the identity of the user that has the \nexclusive right to make changes to this Worker Deployment. Empty by default.\nWhen set, users whose identity does not match the \x1b[1mManagerIdentity\x1b[0m will not\nbe able to change the Worker Deployment.\n\nThis is especially useful in environments where multiple users (such as CLI\nusers and automated controllers) may interact with the same Worker Deployment.\n\x1b[1mManagerIdentity\x1b[0m allows different users to communicate with one another about\nwho is expected to make changes to the Worker Deployment.\n\n\x1b[1mtemporal worker deployment manager-identity set [options]\x1b[0m\n\nFor example:\n\n\x1b[1mtemporal worker deployment manager-identity set \\\n --deployment-name DeploymentName \\\n --self \\\n --identity YourUserIdentity # optional, populated by CLI if not provided\x1b[0m\n\nSets the Manager Identity of the Deployment to the identity of the user making \nthis request. If you don't specifically pass an identity field, the CLI will \ngenerate your identity for you.\n\nFor example:\n\x1b[1mtemporal worker deployment manager-identity set \\\n --deployment-name DeploymentName \\\n --manager-identity NewManagerIdentity\x1b[0m\n\nSets the Manager Identity of the Deployment to any string." + } else { + s.Command.Long = "```\n+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\n```\n\nSet the `ManagerIdentity` of a Worker Deployment given its Deployment Name.\n\nWhen present, `ManagerIdentity` is the identity of the user that has the \nexclusive right to make changes to this Worker Deployment. Empty by default.\nWhen set, users whose identity does not match the `ManagerIdentity` will not\nbe able to change the Worker Deployment.\n\nThis is especially useful in environments where multiple users (such as CLI\nusers and automated controllers) may interact with the same Worker Deployment.\n`ManagerIdentity` allows different users to communicate with one another about\nwho is expected to make changes to the Worker Deployment.\n\n```\ntemporal worker deployment manager-identity set [options]\n```\n\nFor example:\n\n```\ntemporal worker deployment manager-identity set \\\n --deployment-name DeploymentName \\\n --self \\\n --identity YourUserIdentity # optional, populated by CLI if not provided\n```\n\nSets the Manager Identity of the Deployment to the identity of the user making \nthis request. If you don't specifically pass an identity field, the CLI will \ngenerate your identity for you.\n\nFor example:\n```\ntemporal worker deployment manager-identity set \\\n --deployment-name DeploymentName \\\n --manager-identity NewManagerIdentity\n```\n\nSets the Manager Identity of the Deployment to any string." + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVar(&s.ManagerIdentity, "manager-identity", "", "New Manager Identity. Required unless --self is specified.") + s.Command.Flags().BoolVar(&s.Self, "self", false, "Set Manager Identity to the identity of the user submitting this request. Required unless --manager-identity is specified.") + s.Command.Flags().StringVar(&s.DeploymentName, "deployment-name", "", "Name for a Worker Deployment. Required.") + s.Command.Flags().BoolVarP(&s.Yes, "yes", "y", false, "Don't prompt to confirm set Manager Identity.") + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + +type TemporalWorkerDeploymentManagerIdentityUnsetCommand struct { + Parent *TemporalWorkerDeploymentManagerIdentityCommand + Command cobra.Command + DeploymentName string + Yes bool +} + +func NewTemporalWorkerDeploymentManagerIdentityUnsetCommand(cctx *CommandContext, parent *TemporalWorkerDeploymentManagerIdentityCommand) *TemporalWorkerDeploymentManagerIdentityUnsetCommand { + var s TemporalWorkerDeploymentManagerIdentityUnsetCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "unset [flags]" + s.Command.Short = "Unset the Manager Identity of a Worker Deployment" + if hasHighlighting { + s.Command.Long = "\x1b[1m+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\x1b[0m\n\nUnset the \x1b[1mManagerIdentity\x1b[0m of a Worker Deployment given its Deployment Name.\n\nWhen present, \x1b[1mManagerIdentity\x1b[0m is the identity of the user that has the \nexclusive right to make changes to this Worker Deployment. Empty by default.\nWhen set, users whose identity does not match the \x1b[1mManagerIdentity\x1b[0m will not\nbe able to change the Worker Deployment.\n\nThis is especially useful in environments where multiple users (such as CLI\nusers and automated controllers) may interact with the same Worker Deployment.\n\x1b[1mManagerIdentity\x1b[0m allows different users to communicate with one another about\nwho is expected to make changes to the Worker Deployment.\n\n\x1b[1mtemporal worker deployment manager-identity unset [options]\x1b[0m\n\nFor example:\n\n\x1b[1mtemporal worker deployment manager-identity unset \\\n --deployment-name YourDeploymentName\x1b[0m\n\nClears the Manager Identity field for a given Deployment." + } else { + s.Command.Long = "```\n+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\n```\n\nUnset the `ManagerIdentity` of a Worker Deployment given its Deployment Name.\n\nWhen present, `ManagerIdentity` is the identity of the user that has the \nexclusive right to make changes to this Worker Deployment. Empty by default.\nWhen set, users whose identity does not match the `ManagerIdentity` will not\nbe able to change the Worker Deployment.\n\nThis is especially useful in environments where multiple users (such as CLI\nusers and automated controllers) may interact with the same Worker Deployment.\n`ManagerIdentity` allows different users to communicate with one another about\nwho is expected to make changes to the Worker Deployment.\n\n```\ntemporal worker deployment manager-identity unset [options]\n```\n\nFor example:\n\n```\ntemporal worker deployment manager-identity unset \\\n --deployment-name YourDeploymentName\n```\n\nClears the Manager Identity field for a given Deployment." + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVar(&s.DeploymentName, "deployment-name", "", "Name for a Worker Deployment. Required.") + s.Command.Flags().BoolVarP(&s.Yes, "yes", "y", false, "Don't prompt to confirm unset Manager Identity.") + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + type TemporalWorkerDeploymentSetCurrentVersionCommand struct { Parent *TemporalWorkerDeploymentCommand Command cobra.Command diff --git a/temporalcli/commands.worker.deployment.go b/temporalcli/commands.worker.deployment.go index 3d184846f..a86a4c907 100644 --- a/temporalcli/commands.worker.deployment.go +++ b/temporalcli/commands.worker.deployment.go @@ -13,16 +13,16 @@ import ( type versionSummariesRowType struct { DeploymentName string `json:"deploymentName"` - BuildId string `json:"buildId"` + BuildID string `json:"BuildID"` DrainageStatus string `json:"drainageStatus"` CreateTime time.Time `json:"createTime"` } type formattedRoutingConfigType struct { CurrentVersionDeploymentName string `json:"currentVersionDeploymentName"` - CurrentVersionBuildId string `json:"currentVersionBuildId"` + CurrentVersionBuildID string `json:"currentVersionBuildID"` RampingVersionDeploymentName string `json:"rampingVersionDeploymentName"` - RampingVersionBuildId string `json:"rampingVersionBuildId"` + RampingVersionBuildID string `json:"rampingVersionBuildID"` RampingVersionPercentage float32 `json:"rampingVersionPercentage"` CurrentVersionChangedTime time.Time `json:"currentVersionChangedTime"` RampingVersionChangedTime time.Time `json:"rampingVersionChangedTime"` @@ -35,15 +35,16 @@ type formattedWorkerDeploymentInfoType struct { LastModifierIdentity string `json:"lastModifierIdentity"` RoutingConfig formattedRoutingConfigType `json:"routingConfig"` VersionSummaries []versionSummariesRowType `json:"versionSummaries"` + ManagerIdentity string `json:"managerIdentity"` } type formattedWorkerDeploymentListEntryType struct { Name string CreateTime time.Time CurrentVersionDeploymentName string `cli:",cardOmitEmpty"` - CurrentVersionBuildId string `cli:",cardOmitEmpty"` + CurrentVersionBuildID string `cli:",cardOmitEmpty"` RampingVersionDeploymentName string `cli:",cardOmitEmpty"` - RampingVersionBuildId string `cli:",cardOmitEmpty"` + RampingVersionBuildID string `cli:",cardOmitEmpty"` RampingVersionPercentage float32 `cli:",cardOmitEmpty"` } @@ -60,7 +61,7 @@ type formattedTaskQueueInfoRowType struct { type formattedWorkerDeploymentVersionInfoType struct { DeploymentName string `json:"deploymentName"` - BuildId string `json:"buildId"` + BuildID string `json:"BuildID"` CreateTime time.Time `json:"createTime"` RoutingChangedTime time.Time `json:"routingChangedTime"` CurrentSinceTime time.Time `json:"currentSinceTime"` @@ -93,7 +94,7 @@ func formatVersionSummaries(vss []client.WorkerDeploymentVersionSummary) ([]vers } vsRows = append(vsRows, versionSummariesRowType{ DeploymentName: vs.Version.DeploymentName, - BuildId: vs.Version.BuildID, + BuildID: vs.Version.BuildID, CreateTime: vs.CreateTime, DrainageStatus: drainageStr, }) @@ -116,9 +117,9 @@ func formatRoutingConfig(rc client.WorkerDeploymentRoutingConfig) (formattedRout } return formattedRoutingConfigType{ CurrentVersionDeploymentName: cvdn, - CurrentVersionBuildId: cvbid, + CurrentVersionBuildID: cvbid, RampingVersionDeploymentName: rvdn, - RampingVersionBuildId: rvbid, + RampingVersionBuildID: rvbid, RampingVersionPercentage: rc.RampingVersionPercentage, CurrentVersionChangedTime: rc.CurrentVersionChangedTime, RampingVersionChangedTime: rc.RampingVersionChangedTime, @@ -143,6 +144,7 @@ func workerDeploymentInfoToRows(deploymentInfo client.WorkerDeploymentInfo) (for CreateTime: deploymentInfo.CreateTime, RoutingConfig: rc, VersionSummaries: vs, + ManagerIdentity: deploymentInfo.ManagerIdentity, }, nil } @@ -171,6 +173,7 @@ func printWorkerDeploymentInfo(cctx *CommandContext, deploymentInfo client.Worke Name string CreateTime time.Time LastModifierIdentity string `cli:",cardOmitEmpty"` + ManagerIdentity string `cli:",cardOmitEmpty"` CurrentVersionDeploymentName string `cli:",cardOmitEmpty"` CurrentVersionBuildID string `cli:",cardOmitEmpty"` RampingVersionDeploymentName string `cli:",cardOmitEmpty"` @@ -183,6 +186,7 @@ func printWorkerDeploymentInfo(cctx *CommandContext, deploymentInfo client.Worke Name: deploymentInfo.Name, CreateTime: deploymentInfo.CreateTime, LastModifierIdentity: deploymentInfo.LastModifierIdentity, + ManagerIdentity: deploymentInfo.ManagerIdentity, CurrentVersionDeploymentName: curVerDepName, CurrentVersionBuildID: curVerBuildId, RampingVersionDeploymentName: rampVerDepName, @@ -261,7 +265,7 @@ func workerDeploymentVersionInfoToRows(deploymentInfo client.WorkerDeploymentVer return formattedWorkerDeploymentVersionInfoType{ DeploymentName: deploymentInfo.Version.DeploymentName, - BuildId: deploymentInfo.Version.BuildID, + BuildID: deploymentInfo.Version.BuildID, CreateTime: deploymentInfo.CreateTime, RoutingChangedTime: deploymentInfo.RoutingChangedTime, CurrentSinceTime: deploymentInfo.CurrentSinceTime, @@ -295,7 +299,7 @@ func printWorkerDeploymentVersionInfo(cctx *CommandContext, deploymentInfo clien printMe := struct { DeploymentName string - BuildId string + BuildID string CreateTime time.Time RoutingChangedTime time.Time `cli:",cardOmitEmpty"` CurrentSinceTime time.Time `cli:",cardOmitEmpty"` @@ -307,7 +311,7 @@ func printWorkerDeploymentVersionInfo(cctx *CommandContext, deploymentInfo clien Metadata map[string]*common.Payload `cli:",cardOmitEmpty"` }{ DeploymentName: deploymentInfo.Version.DeploymentName, - BuildId: deploymentInfo.Version.BuildID, + BuildID: deploymentInfo.Version.BuildID, CreateTime: deploymentInfo.CreateTime, RoutingChangedTime: deploymentInfo.RoutingChangedTime, CurrentSinceTime: deploymentInfo.CurrentSinceTime, @@ -471,9 +475,9 @@ func (c *TemporalWorkerDeploymentListCommand) run(cctx *CommandContext, args []s Name: listEntry.Name, CreateTime: listEntry.CreateTime, CurrentVersionDeploymentName: listEntry.RoutingConfig.CurrentVersionDeploymentName, - CurrentVersionBuildId: listEntry.RoutingConfig.CurrentVersionBuildId, + CurrentVersionBuildID: listEntry.RoutingConfig.CurrentVersionBuildID, RampingVersionDeploymentName: listEntry.RoutingConfig.RampingVersionDeploymentName, - RampingVersionBuildId: listEntry.RoutingConfig.RampingVersionBuildId, + RampingVersionBuildID: listEntry.RoutingConfig.RampingVersionBuildID, RampingVersionPercentage: listEntry.RoutingConfig.RampingVersionPercentage, }) if len(page) == cap(page) { @@ -492,6 +496,72 @@ func (c *TemporalWorkerDeploymentListCommand) run(cctx *CommandContext, args []s return nil } +func (c *TemporalWorkerDeploymentManagerIdentitySetCommand) run(cctx *CommandContext, args []string) error { + cl, err := c.Parent.Parent.Parent.dialClient(cctx) + if err != nil { + return err + } + defer cl.Close() + + token, err := c.Parent.Parent.getConflictToken(cctx, &getDeploymentConflictTokenOptions{ + safeMode: !c.Yes, + safeModeMessage: "ManagerIdentity", + deploymentName: c.DeploymentName, + }) + if err != nil { + return err + } + + newManagerIdentity := c.ManagerIdentity + if c.Self { + newManagerIdentity = c.Parent.Parent.Parent.Identity + } + + dHandle := cl.WorkerDeploymentClient().GetHandle(c.DeploymentName) + resp, err := dHandle.SetManagerIdentity(cctx, client.WorkerDeploymentSetManagerIdentityOptions{ + Identity: c.Parent.Parent.Parent.Identity, + ConflictToken: token, + Self: c.Self, + ManagerIdentity: c.ManagerIdentity, + }) + if err != nil { + return fmt.Errorf("error setting the manager identity: %w", err) + } + + cctx.Printer.Printlnf("Successfully set manager identity to '%s', was previously '%s'", newManagerIdentity, resp.PreviousManagerIdentity) + return nil +} + +func (c *TemporalWorkerDeploymentManagerIdentityUnsetCommand) run(cctx *CommandContext, args []string) error { + cl, err := c.Parent.Parent.Parent.dialClient(cctx) + if err != nil { + return err + } + defer cl.Close() + + token, err := c.Parent.Parent.getConflictToken(cctx, &getDeploymentConflictTokenOptions{ + safeMode: !c.Yes, + safeModeMessage: "ManagerIdentity", + deploymentName: c.DeploymentName, + }) + if err != nil { + return err + } + + dHandle := cl.WorkerDeploymentClient().GetHandle(c.DeploymentName) + resp, err := dHandle.SetManagerIdentity(cctx, client.WorkerDeploymentSetManagerIdentityOptions{ + Identity: c.Parent.Parent.Parent.Identity, + ConflictToken: token, + ManagerIdentity: "", + }) + if err != nil { + return fmt.Errorf("error unsetting the manager identity: %w", err) + } + + cctx.Printer.Printlnf("Successfully unset manager identity, was previously '%s'", resp.PreviousManagerIdentity) + return nil +} + func (c *TemporalWorkerDeploymentDeleteVersionCommand) run(cctx *CommandContext, args []string) error { cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) if err != nil { diff --git a/temporalcli/commands.worker.deployment_test.go b/temporalcli/commands.worker.deployment_test.go index 0b5b7a5e4..7a526b285 100644 --- a/temporalcli/commands.worker.deployment_test.go +++ b/temporalcli/commands.worker.deployment_test.go @@ -17,16 +17,16 @@ import ( type jsonVersionSummariesRowType struct { DeploymentName string `json:"deploymentName"` - BuildID string `json:"buildId"` + BuildID string `json:"BuildID"` DrainageStatus string `json:"drainageStatus"` CreateTime time.Time `json:"createTime"` } type jsonRoutingConfigType struct { CurrentVersionDeploymentName string `json:"currentVersionDeploymentName"` - CurrentVersionBuildID string `json:"currentVersionBuildId"` + CurrentVersionBuildID string `json:"currentVersionBuildID"` RampingVersionDeploymentName string `json:"rampingVersionDeploymentName"` - RampingVersionBuildID string `json:"rampingVersionBuildId"` + RampingVersionBuildID string `json:"rampingVersionBuildID"` RampingVersionPercentage float32 `json:"rampingVersionPercentage"` CurrentVersionChangedTime time.Time `json:"currentVersionChangedTime"` RampingVersionChangedTime time.Time `json:"rampingVersionChangedTime"` @@ -39,6 +39,7 @@ type jsonDeploymentInfoType struct { LastModifierIdentity string `json:"lastModifierIdentity"` RoutingConfig jsonRoutingConfigType `json:"routingConfig"` VersionSummaries []jsonVersionSummariesRowType `json:"versionSummaries"` + ManagerIdentity string `json:"managerIdentity"` } type jsonDrainageInfo struct { @@ -569,3 +570,73 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { s.Equal(float32(0), jsonOut.RoutingConfig.RampingVersionPercentage) s.Equal(version2.BuildID, jsonOut.RoutingConfig.CurrentVersionBuildID) } + +func (s *SharedServerSuite) TestDeployment_Set_Manager_Identity() { + deploymentName := uuid.NewString() + BuildID := uuid.NewString() + testIdentity := uuid.NewString() + version := worker.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildID: BuildID, + } + w := s.DevServer.StartDevWorker(s.Suite.T(), DevWorkerOptions{ + Worker: worker.Options{ + DeploymentOptions: worker.DeploymentOptions{ + UseVersioning: true, + Version: version, + DefaultVersioningBehavior: workflow.VersioningBehaviorPinned, + }, + }, + }) + defer w.Stop() + + s.EventuallyWithT(func(t *assert.CollectT) { + res := s.Execute( + "worker", "deployment", "list", + "--address", s.Address(), + ) + assert.NoError(t, res.Err) + assert.Contains(t, res.Stdout.String(), deploymentName) + }, 30*time.Second, 100*time.Millisecond) + + s.EventuallyWithT(func(t *assert.CollectT) { + res := s.Execute( + "worker", "deployment", "describe-version", + "--address", s.Address(), + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, + ) + assert.NoError(t, res.Err) + }, 30*time.Second, 100*time.Millisecond) + + res := s.Execute( + "worker", "deployment", "manager-identity", "set", + "--address", s.Address(), + "--deployment-name", version.DeploymentName, "--manager-identity", testIdentity, + "--yes", + ) + s.NoError(res.Err) + + res = s.Execute( + "worker", "deployment", "describe", + "--address", s.Address(), + "--name", deploymentName, + ) + s.NoError(res.Err) + + s.ContainsOnSameLine(res.Stdout.String(), "Name", deploymentName) + s.ContainsOnSameLine(res.Stdout.String(), "ManagerIdentity", testIdentity) + + // json + res = s.Execute( + "worker", "deployment", "describe", + "--address", s.Address(), + "--name", deploymentName, + "--output", "json", + ) + s.NoError(res.Err) + + var jsonOut jsonDeploymentInfoType + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &jsonOut)) + s.Equal(deploymentName, jsonOut.Name) + s.Equal(testIdentity, jsonOut.ManagerIdentity) +} diff --git a/temporalcli/commandsgen/commands.yml b/temporalcli/commandsgen/commands.yml index 9405fb761..8d1a738fd 100644 --- a/temporalcli/commandsgen/commands.yml +++ b/temporalcli/commandsgen/commands.yml @@ -942,6 +942,7 @@ commands: - worker deployment set-ramping-version - worker deployment delete-version - worker deployment update-metadata-version + - worker deployment manager-identity - name: temporal worker deployment describe summary: Show properties of a Worker Deployment @@ -1250,6 +1251,153 @@ commands: Keys of entries to be deleted from metadata. Can be passed multiple times. + - name: temporal worker deployment manager-identity + summary: Manager Identity commands change the `ManagerIdentity` of a Worker Deployment + description: | + ``` + +---------------------------------------------------------------------+ + | CAUTION: Worker Deployment is experimental. Deployment commands are | + | subject to change. | + +---------------------------------------------------------------------+ + ``` + + Manager Identity commands change the `ManagerIdentity` of a Worker Deployment: + + ``` + temporal worker deployment manager-identity [command] [options] + ``` + + When present, `ManagerIdentity` is the identity of the user that has the + exclusive right to make changes to this Worker Deployment. Empty by default. + When set, users whose identity does not match the `ManagerIdentity` will not + be able to change the Worker Deployment. + + This is especially useful in environments where multiple users (such as CLI + users and automated controllers) may interact with the same Worker Deployment. + `ManagerIdentity` allows different users to communicate with one another about + who is expected to make changes to the Worker Deployment. + + The current Manager Identity is returned with `describe`: + ``` + temporal worker deployment describe \ + --deployment-name YourDeploymentName + ``` + + docs: + description-header: >- + Temporal Deployment Manager Identity commands enable set and unset + operations on the manager identity field of Worker Deployments. + keywords: + - worker deployment manager-identity set + - worker deployment manager-identity unset + + - name: temporal worker deployment manager-identity set + summary: Set the Manager Identity of a Worker Deployment + description: | + ``` + +---------------------------------------------------------------------+ + | CAUTION: Worker Deployment is experimental. Deployment commands are | + | subject to change. | + +---------------------------------------------------------------------+ + ``` + + Set the `ManagerIdentity` of a Worker Deployment given its Deployment Name. + + When present, `ManagerIdentity` is the identity of the user that has the + exclusive right to make changes to this Worker Deployment. Empty by default. + When set, users whose identity does not match the `ManagerIdentity` will not + be able to change the Worker Deployment. + + This is especially useful in environments where multiple users (such as CLI + users and automated controllers) may interact with the same Worker Deployment. + `ManagerIdentity` allows different users to communicate with one another about + who is expected to make changes to the Worker Deployment. + + ``` + temporal worker deployment manager-identity set [options] + ``` + + For example: + + ``` + temporal worker deployment manager-identity set \ + --deployment-name DeploymentName \ + --self \ + --identity YourUserIdentity # optional, populated by CLI if not provided + ``` + + Sets the Manager Identity of the Deployment to the identity of the user making + this request. If you don't specifically pass an identity field, the CLI will + generate your identity for you. + + For example: + ``` + temporal worker deployment manager-identity set \ + --deployment-name DeploymentName \ + --manager-identity NewManagerIdentity + ``` + + Sets the Manager Identity of the Deployment to any string. + + options: + - name: manager-identity + type: string + description: New Manager Identity. Required unless --self is specified. + - name: self + type: bool + description: Set Manager Identity to the identity of the user submitting this request. Required unless --manager-identity is specified. + - name: deployment-name + type: string + description: Name for a Worker Deployment. Required. + - name: yes + short: y + type: bool + description: Don't prompt to confirm set Manager Identity. + + - name: temporal worker deployment manager-identity unset + summary: Unset the Manager Identity of a Worker Deployment + description: | + ``` + +---------------------------------------------------------------------+ + | CAUTION: Worker Deployment is experimental. Deployment commands are | + | subject to change. | + +---------------------------------------------------------------------+ + ``` + + Unset the `ManagerIdentity` of a Worker Deployment given its Deployment Name. + + When present, `ManagerIdentity` is the identity of the user that has the + exclusive right to make changes to this Worker Deployment. Empty by default. + When set, users whose identity does not match the `ManagerIdentity` will not + be able to change the Worker Deployment. + + This is especially useful in environments where multiple users (such as CLI + users and automated controllers) may interact with the same Worker Deployment. + `ManagerIdentity` allows different users to communicate with one another about + who is expected to make changes to the Worker Deployment. + + ``` + temporal worker deployment manager-identity unset [options] + ``` + + For example: + + ``` + temporal worker deployment manager-identity unset \ + --deployment-name YourDeploymentName + ``` + + Clears the Manager Identity field for a given Deployment. + + options: + - name: deployment-name + type: string + description: Name for a Worker Deployment. Required. + - name: yes + short: y + type: bool + description: Don't prompt to confirm unset Manager Identity. + - name: temporal env summary: Manage environments description: | From 4e43b7a2ba3b1165e7c6add88d6ec5d793cb5565 Mon Sep 17 00:00:00 2001 From: Carly de Frondeville Date: Thu, 9 Oct 2025 14:54:15 -0700 Subject: [PATCH 10/28] Add --allow-no-pollers flag to set-current-version and set-ramping-version (#860) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What was changed Add --allow-no-pollers flag to set-current-version and set-ramping-version ## Why? So that users can make a version current / ramping before it has pollers. ## Checklist 1. Closes 2. How was this tested: Added tests 3. Any docs updates needed? --- temporalcli/commands.gen.go | 4 + temporalcli/commands.worker.deployment.go | 8 +- .../commands.worker.deployment_test.go | 127 ++++++++++++++++++ temporalcli/commandsgen/commands.yml | 6 + 4 files changed, 143 insertions(+), 2 deletions(-) diff --git a/temporalcli/commands.gen.go b/temporalcli/commands.gen.go index eee204100..714c94950 100644 --- a/temporalcli/commands.gen.go +++ b/temporalcli/commands.gen.go @@ -3121,6 +3121,7 @@ type TemporalWorkerDeploymentSetCurrentVersionCommand struct { Command cobra.Command DeploymentVersionOrUnversionedOptions IgnoreMissingTaskQueues bool + AllowNoPollers bool Yes bool } @@ -3137,6 +3138,7 @@ func NewTemporalWorkerDeploymentSetCurrentVersionCommand(cctx *CommandContext, p } s.Command.Args = cobra.NoArgs s.Command.Flags().BoolVar(&s.IgnoreMissingTaskQueues, "ignore-missing-task-queues", false, "Override protection to accidentally remove task queues.") + s.Command.Flags().BoolVar(&s.AllowNoPollers, "allow-no-pollers", false, "Override protection and set version as current even if it has no pollers.") s.Command.Flags().BoolVarP(&s.Yes, "yes", "y", false, "Don't prompt to confirm set Current Version.") s.DeploymentVersionOrUnversionedOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { @@ -3154,6 +3156,7 @@ type TemporalWorkerDeploymentSetRampingVersionCommand struct { Percentage float32 Delete bool IgnoreMissingTaskQueues bool + AllowNoPollers bool Yes bool } @@ -3172,6 +3175,7 @@ func NewTemporalWorkerDeploymentSetRampingVersionCommand(cctx *CommandContext, p s.Command.Flags().Float32Var(&s.Percentage, "percentage", 0, "Percentage of tasks redirected to the Ramping Version. Valid range [0,100].") s.Command.Flags().BoolVar(&s.Delete, "delete", false, "Delete the Ramping Version.") s.Command.Flags().BoolVar(&s.IgnoreMissingTaskQueues, "ignore-missing-task-queues", false, "Override protection to accidentally remove task queues.") + s.Command.Flags().BoolVar(&s.AllowNoPollers, "allow-no-pollers", false, "Override protection and set version as ramping even if it has no pollers.") s.Command.Flags().BoolVarP(&s.Yes, "yes", "y", false, "Don't prompt to confirm set Ramping Version.") s.DeploymentVersionOrUnversionedOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { diff --git a/temporalcli/commands.worker.deployment.go b/temporalcli/commands.worker.deployment.go index a86a4c907..a742cbd9e 100644 --- a/temporalcli/commands.worker.deployment.go +++ b/temporalcli/commands.worker.deployment.go @@ -1,12 +1,14 @@ package temporalcli import ( + "errors" "fmt" "time" "github.com/fatih/color" "github.com/temporalio/cli/temporalcli/internal/printer" "go.temporal.io/api/common/v1" + "go.temporal.io/api/serviceerror" "go.temporal.io/sdk/client" "go.temporal.io/sdk/worker" ) @@ -619,7 +621,7 @@ func (c *TemporalWorkerDeploymentSetCurrentVersionCommand) run(cctx *CommandCont safeModeMessage: "Current", deploymentName: c.DeploymentName, }) - if err != nil { + if err != nil && !(errors.As(err, new(*serviceerror.NotFound)) && c.AllowNoPollers) { return err } @@ -628,6 +630,7 @@ func (c *TemporalWorkerDeploymentSetCurrentVersionCommand) run(cctx *CommandCont BuildID: c.BuildId, Identity: c.Parent.Parent.Identity, IgnoreMissingTaskQueues: c.IgnoreMissingTaskQueues, + AllowNoPollers: c.AllowNoPollers, ConflictToken: token, }) if err != nil { @@ -650,7 +653,7 @@ func (c *TemporalWorkerDeploymentSetRampingVersionCommand) run(cctx *CommandCont safeModeMessage: "Ramping", deploymentName: c.DeploymentName, }) - if err != nil { + if err != nil && !(errors.As(err, new(*serviceerror.NotFound)) && c.AllowNoPollers) { return err } @@ -666,6 +669,7 @@ func (c *TemporalWorkerDeploymentSetRampingVersionCommand) run(cctx *CommandCont ConflictToken: token, Identity: c.Parent.Parent.Identity, IgnoreMissingTaskQueues: c.IgnoreMissingTaskQueues, + AllowNoPollers: c.AllowNoPollers, }) if err != nil { return fmt.Errorf("error setting the ramping worker deployment version: %w", err) diff --git a/temporalcli/commands.worker.deployment_test.go b/temporalcli/commands.worker.deployment_test.go index 7a526b285..0311ae298 100644 --- a/temporalcli/commands.worker.deployment_test.go +++ b/temporalcli/commands.worker.deployment_test.go @@ -173,6 +173,133 @@ func (s *SharedServerSuite) TestDeployment_Set_Current_Version() { s.Nil(jsonVersionOut.Metadata) } +func (s *SharedServerSuite) TestDeployment_Set_Current_Version_AllowNoPollers() { + deploymentName := uuid.NewString() + buildId := uuid.NewString() + version := worker.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildID: buildId, + } + + // with --allow-no-pollers, no need to have a worker polling on this version + res := s.Execute( + "worker", "deployment", "set-current-version", + "--address", s.Address(), + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, + "--allow-no-pollers", + "--yes", + ) + s.NoError(res.Err) + + s.EventuallyWithT(func(t *assert.CollectT) { + res := s.Execute( + "worker", "deployment", "list", + "--address", s.Address(), + ) + assert.NoError(t, res.Err) + assert.Contains(t, res.Stdout.String(), deploymentName) + }, 30*time.Second, 100*time.Millisecond) + + s.EventuallyWithT(func(t *assert.CollectT) { + res := s.Execute( + "worker", "deployment", "describe-version", + "--address", s.Address(), + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, + ) + assert.NoError(t, res.Err) + }, 30*time.Second, 100*time.Millisecond) + + res = s.Execute( + "worker", "deployment", "describe", + "--address", s.Address(), + "--name", deploymentName, + ) + s.NoError(res.Err) + + s.ContainsOnSameLine(res.Stdout.String(), "Name", deploymentName) + s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionDeploymentName", version.DeploymentName) + s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version.BuildID) + + // json + res = s.Execute( + "worker", "deployment", "describe", + "--address", s.Address(), + "--name", deploymentName, + "--output", "json", + ) + s.NoError(res.Err) + + var jsonOut jsonDeploymentInfoType + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &jsonOut)) + s.Equal(deploymentName, jsonOut.Name) + s.Equal(version.DeploymentName, jsonOut.RoutingConfig.CurrentVersionDeploymentName) + s.Equal(version.BuildID, jsonOut.RoutingConfig.CurrentVersionBuildID) +} + +func (s *SharedServerSuite) TestDeployment_Set_Ramping_Version_AllowNoPollers() { + deploymentName := uuid.NewString() + buildId := uuid.NewString() + version := worker.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildID: buildId, + } + + // with --allow-no-pollers, no need to have a worker polling on this version + res := s.Execute( + "worker", "deployment", "set-ramping-version", + "--address", s.Address(), + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, + "--percentage", "5", + "--allow-no-pollers", + "--yes", + ) + s.NoError(res.Err) + + s.EventuallyWithT(func(t *assert.CollectT) { + res := s.Execute( + "worker", "deployment", "list", + "--address", s.Address(), + ) + assert.NoError(t, res.Err) + assert.Contains(t, res.Stdout.String(), deploymentName) + }, 30*time.Second, 100*time.Millisecond) + + s.EventuallyWithT(func(t *assert.CollectT) { + res := s.Execute( + "worker", "deployment", "describe-version", + "--address", s.Address(), + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, + ) + assert.NoError(t, res.Err) + }, 30*time.Second, 100*time.Millisecond) + + res = s.Execute( + "worker", "deployment", "describe", + "--address", s.Address(), + "--name", deploymentName, + ) + s.NoError(res.Err) + + s.ContainsOnSameLine(res.Stdout.String(), "Name", deploymentName) + s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionDeploymentName", version.DeploymentName) + s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionBuildID", version.BuildID) + + // json + res = s.Execute( + "worker", "deployment", "describe", + "--address", s.Address(), + "--name", deploymentName, + "--output", "json", + ) + s.NoError(res.Err) + + var jsonOut jsonDeploymentInfoType + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &jsonOut)) + s.Equal(deploymentName, jsonOut.Name) + s.Equal(version.DeploymentName, jsonOut.RoutingConfig.RampingVersionDeploymentName) + s.Equal(version.BuildID, jsonOut.RoutingConfig.RampingVersionBuildID) +} + func filterByNamePrefix(jsonOut []jsonDeploymentInfoType, prefix string) []jsonDeploymentInfoType { result := []jsonDeploymentInfoType{} for i := range jsonOut { diff --git a/temporalcli/commandsgen/commands.yml b/temporalcli/commandsgen/commands.yml index 8d1a738fd..6aeef5bc6 100644 --- a/temporalcli/commandsgen/commands.yml +++ b/temporalcli/commandsgen/commands.yml @@ -1137,6 +1137,9 @@ commands: - name: ignore-missing-task-queues type: bool description: Override protection to accidentally remove task queues. + - name: allow-no-pollers + type: bool + description: Override protection and set version as current even if it has no pollers. - name: yes short: y type: bool @@ -1204,6 +1207,9 @@ commands: - name: ignore-missing-task-queues type: bool description: Override protection to accidentally remove task queues. + - name: allow-no-pollers + type: bool + description: Override protection and set version as ramping even if it has no pollers. - name: yes short: y type: bool From 424a7dcaf4b5bccb4dc438248bdb57566085f08b Mon Sep 17 00:00:00 2001 From: Shahab Tajik Date: Fri, 10 Oct 2025 10:28:33 -0700 Subject: [PATCH 11/28] Upgrade UI to v2.41.0 (#861) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What was changed ## Why? ## Checklist 1. Closes 2. How was this tested: Made sure the UI server loads with the right version 3. Any docs updates needed? --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c0cd08d04..493fcb3c5 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 - github.com/temporalio/ui-server/v2 v2.39.0 + github.com/temporalio/ui-server/v2 v2.41.0 go.temporal.io/api v1.53.0 go.temporal.io/sdk v1.37.0 go.temporal.io/sdk/contrib/envconfig v0.1.0 diff --git a/go.sum b/go.sum index 202655411..ade1ab8be 100644 --- a/go.sum +++ b/go.sum @@ -327,8 +327,8 @@ github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb/go.mod h1:143 github.com/temporalio/tchannel-go v1.22.1-0.20220818200552-1be8d8cffa5b/go.mod h1:c+V9Z/ZgkzAdyGvHrvC5AsXgN+M9Qwey04cBdKYzV7U= github.com/temporalio/tchannel-go v1.22.1-0.20240528171429-1db37fdea938 h1:sEJGhmDo+0FaPWM6f0v8Tjia0H5pR6/Baj6+kS78B+M= github.com/temporalio/tchannel-go v1.22.1-0.20240528171429-1db37fdea938/go.mod h1:ezRQRwu9KQXy8Wuuv1aaFFxoCNz5CeNbVOOkh3xctbY= -github.com/temporalio/ui-server/v2 v2.39.0 h1:ZC6yoSCi74qKtDy3ly/tsgwYv3fSP+a748nADUSl3qo= -github.com/temporalio/ui-server/v2 v2.39.0/go.mod h1:f64k+N/ByniY9nI4c7cxbaiybwz10BkhapeIUDY3qIQ= +github.com/temporalio/ui-server/v2 v2.41.0 h1:m9F2jnFJy/dWxjk9d2oS+825r78EX4gUlipoQ1xNO6Y= +github.com/temporalio/ui-server/v2 v2.41.0/go.mod h1:ofEKGV5/NaPbjdmEQRcUDFE6nZPprOemNjLJYLF9IX4= github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/uber-common/bark v1.0.0/go.mod h1:g0ZuPcD7XiExKHynr93Q742G/sbrdVQkghrqLGOoFuY= From 8239c2301c598b831e88d759392d98a024b9c783 Mon Sep 17 00:00:00 2001 From: Sean Kane Date: Mon, 13 Oct 2025 11:01:30 -0600 Subject: [PATCH 12/28] fix(help text): wrap flag help texts within a column (#862) ## What was changed The flags help text does not wrap within the column, making it more difficult to read. If the flag description spills over a line it will start from the left instead of where the previous line started. Current behavior: ```sh Flags: --color string Output coloring. Accepted values: always, never, auto. (default "auto") --command-timeout duration The command execution timeout. 0s means no timeout. (default 0s) --config-file $CONFIG_PATH/temporal/temporal.toml File path to read TOML config from, defaults to $CONFIG_PATH/temporal/temporal.toml where `$CONFIG_PATH` is defined as `$HOME/ .config` on Unix, "$HOME/Library/Application Support" on macOS, and %AppData% on Windows. EXPERIMENTAL. --disable-config-env If set, disables loading environment config from environment variables. EXPERIMENTAL. --disable-config-file If set, disables loading environment config from config file. EXPERIMENTAL. --env ENV Active environment name (ENV). (default "default") --env-file $HOME/.config/temporalio/temporal.yaml Path to environment settings file. Defaults to $HOME/.config/temporalio/temporal.yaml. -h, --help help for temporal --log-format string Log format. Accepted values: text, json. (default "text") --log-level server start-dev Log level. Default is "info" for most commands and "warn" for server start-dev. Accepted values: debug, info, warn, error, nev er. (default "info") --no-json-shorthand-payloads Raw payload output, even if the JSON option was used. -o, --output string Non-logging data output format. Accepted values: text, json, jsonl, none. (default "text") --profile string Profile to use for config file. EXPERIMENTAL. --time-format string Time format. Accepted values: relative, iso, raw. (default "relative") -v, --version version for temporal ``` New behavior: ```sh Flags: --client-connect-timeout duration The client connection timeout. 0s means no timeout. (default 0s) --color string Output coloring. Accepted values: always, never, auto. (default "auto") --command-timeout duration The command execution timeout. 0s means no timeout. (default 0s) --config-file $CONFIG_PATH/temporal/temporal.toml File path to read TOML config from, defaults to $CONFIG_PATH/temporal/temporal.toml where `$CONFIG_PATH` is defined as `$HOME/.config` on Unix, "$HOME/Library/Application Support" on macOS, and %AppData% on Windows. EXPERIMENTAL. --disable-config-env If set, disables loading environment config from environment variables. EXPERIMENTAL. --disable-config-file If set, disables loading environment config from config file. EXPERIMENTAL. --env ENV Active environment name (ENV). (default "default") --env-file $HOME/.config/temporalio/temporal.yaml Path to environment settings file. Defaults to $HOME/.config/temporalio/temporal.yaml. -h, --help help for temporal --log-format string Log format. Accepted values: text, json. (default "text") --log-level server start-dev Log level. Default is "info" for most commands and "warn" for server start-dev. Accepted values: debug, info, warn, error, never. (default "info") --no-json-shorthand-payloads Raw payload output, even if the JSON option was used. -o, --output string Non-logging data output format. Accepted values: text, json, jsonl, none. (default "text") --profile string Profile to use for config file. EXPERIMENTAL. --time-format string Time format. Accepted values: relative, iso, raw. (default "relative") -v, --version version for temporal ``` ## Why? Better readability ## Checklist 1. Closes NA 2. How was this tested: Tested manually 3. Any docs updates needed? No --- go.mod | 1 + go.sum | 2 ++ temporalcli/commands.go | 50 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/go.mod b/go.mod index 493fcb3c5..8306ed551 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( go.temporal.io/sdk v1.37.0 go.temporal.io/sdk/contrib/envconfig v0.1.0 go.temporal.io/server v1.29.0 + golang.org/x/term v0.32.0 google.golang.org/grpc v1.72.2 google.golang.org/protobuf v1.36.6 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index ade1ab8be..9c394a149 100644 --- a/go.sum +++ b/go.sum @@ -488,6 +488,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX 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.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 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= diff --git a/temporalcli/commands.go b/temporalcli/commands.go index a549c80b5..01f956e58 100644 --- a/temporalcli/commands.go +++ b/temporalcli/commands.go @@ -29,6 +29,7 @@ import ( "go.temporal.io/sdk/converter" "go.temporal.io/sdk/temporal" "go.temporal.io/server/common/headers" + "golang.org/x/term" "google.golang.org/grpc" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" @@ -375,8 +376,57 @@ func Execute(ctx context.Context, options CommandOptions) { } } +// getUsageTemplate returns a custom usage template with proper flag wrapping +// The default template can be found here: https://github.com/spf13/cobra/blob/v1.9.1/command.go#L1937-L1966 +func getUsageTemplate() string { + // Get terminal width, default to 80 if unable to determine + width := 80 + if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 { + width = w + } + + // Use width - 1 for wrapping to avoid edge cases + flagWidth := width - 1 + + // Custom template that uses FlagUsagesWrapped for proper indentation + return fmt.Sprintf(`Usage:{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} + +Aliases: + {{.NameAndAliases}}{{end}}{{if .HasExample}} + +Examples: +{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}} + +Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}} + +{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}} + +Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +Flags: +{{.LocalFlags.FlagUsagesWrapped %d | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} + +Global Flags: +{{.InheritedFlags.FlagUsagesWrapped %d | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +`, flagWidth, flagWidth) +} + func (c *TemporalCommand) initCommand(cctx *CommandContext) { c.Command.Version = VersionString() + + // Set custom usage template with proper flag wrapping + c.Command.SetUsageTemplate(getUsageTemplate()) + // Unfortunately color is a global option, so we can set in pre-run but we // must unset in post-run origNoColor := color.NoColor From 18d670207e583639d4ffc23512e43c5c4f4326e7 Mon Sep 17 00:00:00 2001 From: Bill Richards Date: Fri, 17 Oct 2025 11:37:50 -0600 Subject: [PATCH 13/28] docs(task-queue): update describe samples and guidance (#866) This PR updates the Task Queue 'describe' command docs samples in commands.yml to clarify usage and include additional guidance.\n\n- Adjust example invocations\n- Add notes on stats and reachability\n\nGenerated docs can be produced locally via:\n\n\n\n --- temporalcli/commands.gen.go | 4 ++-- temporalcli/commandsgen/commands.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/temporalcli/commands.gen.go b/temporalcli/commands.gen.go index 714c94950..cab218eca 100644 --- a/temporalcli/commands.gen.go +++ b/temporalcli/commands.gen.go @@ -2293,9 +2293,9 @@ func NewTemporalTaskQueueDescribeCommand(cctx *CommandContext, parent *TemporalT s.Command.Use = "describe [flags]" s.Command.Short = "Show active Workers" if hasHighlighting { - s.Command.Long = "Display a list of active Workers that have recently polled a Task Queue. The\nTemporal Server records each poll request time. A \x1b[1mLastAccessTime\x1b[0m over one\nminute may indicate the Worker is at capacity or has shut down. Temporal\nWorkers are removed if 5 minutes have passed since the last poll request.\n\n\x1b[1mtemporal task-queue describe \\\n --task-queue YourTaskQueue\x1b[0m\n\nThis command provides poller information for a given Task Queue.\nWorkflow and Activity polling use separate Task Queues:\n\n\x1b[1mtemporal task-queue describe \\\n --task-queue YourTaskQueue \\\n --task-queue-type \"activity\"\x1b[0m\n\nThis command provides the following task queue statistics:\n- \x1b[1mApproximateBacklogCount\x1b[0m: The approximate number of tasks backlogged in this\n task queue. May count expired tasks but eventually converges to the right\n value.\n- \x1b[1mApproximateBacklogAge\x1b[0m: Approximate age of the oldest task in the backlog,\n based on its creation time, measured in seconds.\n- \x1b[1mTasksAddRate\x1b[0m: Approximate rate at which tasks are being added to the task\n queue, measured in tasks per second, averaged over the last 30 seconds.\n Includes tasks dispatched immediately without going to the backlog\n (sync-matched tasks), as well as tasks added to the backlog. (See note below.)\n- \x1b[1mTasksDispatchRate\x1b[0m: Approximate rate at which tasks are being dispatched from\n the task queue, measured in tasks per second, averaged over the last 30\n seconds. Includes tasks dispatched immediately without going to the backlog\n (sync-matched tasks), as well as tasks added to the backlog. (See note below.)\n- \x1b[1mBacklogIncreaseRate\x1b[0m: Approximate rate at which the backlog size is\n increasing (if positive) or decreasing (if negative), measured in tasks per\n second, averaged over the last 30 seconds. This is roughly equivalent to:\n \x1b[1mTasksAddRate\x1b[0m - \x1b[1mTasksDispatchRate\x1b[0m.\n\nNOTE: The \x1b[1mTasksAddRate\x1b[0m and \x1b[1mTasksDispatchRate\x1b[0m metrics may differ from the\nactual rate of add/dispatch, because tasks may be dispatched eagerly to an\navailable worker, or may apply only to specific workers (they are \"sticky\").\nSuch tasks are not counted by these metrics. Despite the inaccuracy of\nthese two metrics, the derived metric of \x1b[1mBacklogIncreaseRate\x1b[0m is accurate\nfor backlogs older than a few seconds.\n\nSafely retire Workers assigned a Build ID by checking reachability across\nall task types. Use the flag \x1b[1m--report-reachability\x1b[0m:\n\n\x1b[1mtemporal task-queue describe \\\n --task-queue YourTaskQueue \\\n --build-id \"YourBuildId\" \\\n --report-reachability\x1b[0m\n\nTask reachability information is returned for the requested versions and all\ntask types, which can be used to safely retire Workers with old code versions,\nprovided that they were assigned a Build ID.\n\nNote that task reachability status is experimental and may significantly change\nor be removed in a future release. Also, determining task reachability incurs a\nnon-trivial computing cost.\n\nTask reachability states are reported per build ID. The state may be one of the\nfollowing:\n\n- \x1b[1mReachable\x1b[0m: using the current versioning rules, the Build ID may be used\n by new Workflow Executions or Activities OR there are currently open\n Workflow or backlogged Activity tasks assigned to the queue.\n- \x1b[1mClosedWorkflowsOnly\x1b[0m: the Build ID does not have open Workflow Executions\n and can't be reached by new Workflow Executions. It MAY have closed\n Workflow Executions within the Namespace retention period.\n- \x1b[1mUnreachable\x1b[0m: this Build ID is not used for new Workflow Executions and\n isn't used by any existing Workflow Execution within the retention period.\n\nTask reachability is eventually consistent. You may experience a delay until\nreachability converges to the most accurate value. This is designed to act\nin the most conservative way until convergence. For example, \x1b[1mReachable\x1b[0m is\nmore conservative than \x1b[1mClosedWorkflowsOnly\x1b[0m." + s.Command.Long = "Display a list of active Workers that have recently polled a Task Queue. The\nTemporal Server records each poll request time. A \x1b[1mLastAccessTime\x1b[0m over one\nminute may indicate the Worker is at capacity or has shut down. Temporal\nWorkers are removed if 5 minutes have passed since the last poll request.\n\n\x1b[1mtemporal task-queue describe \\\n --task-queue YourTaskQueue\x1b[0m\n\nThis command provides poller information for a given Task Queue.\nWorkflow and Activity polling use separate Task Queues:\n\n\x1b[1mtemporal task-queue describe \\\n --task-queue YourTaskQueue \\\n --task-queue-type \"activity\"\x1b[0m\n\nThis command provides the following task queue statistics:\n- \x1b[1mApproximateBacklogCount\x1b[0m: The approximate number of tasks backlogged in this\n task queue. May count expired tasks but eventually converges to the right\n value.\n- \x1b[1mApproximateBacklogAge\x1b[0m: Approximate age of the oldest task in the backlog,\n based on its creation time, measured in seconds.\n- \x1b[1mTasksAddRate\x1b[0m: Approximate rate at which tasks are being added to the task\n queue, measured in tasks per second, averaged over the last 30 seconds.\n Includes tasks dispatched immediately without going to the backlog\n (sync-matched tasks), as well as tasks added to the backlog. (See note below.)\n- \x1b[1mTasksDispatchRate\x1b[0m: Approximate rate at which tasks are being dispatched from\n the task queue, measured in tasks per second, averaged over the last 30\n seconds. Includes tasks dispatched immediately without going to the backlog\n (sync-matched tasks), as well as tasks added to the backlog. (See note below.)\n- \x1b[1mBacklogIncreaseRate\x1b[0m: Approximate rate at which the backlog size is\n increasing (if positive) or decreasing (if negative), measured in tasks per\n second, averaged over the last 30 seconds. This is roughly equivalent to:\n \x1b[1mTasksAddRate\x1b[0m - \x1b[1mTasksDispatchRate\x1b[0m.\n\nNOTE: The \x1b[1mTasksAddRate\x1b[0m and \x1b[1mTasksDispatchRate\x1b[0m metrics may differ from the\nactual rate of add/dispatch, because tasks may be dispatched eagerly to an\navailable worker, or may apply only to specific workers (they are \"sticky\").\nSuch tasks are not counted by these metrics. Despite the inaccuracy of\nthese two metrics, the derived metric of \x1b[1mBacklogIncreaseRate\x1b[0m is accurate\nfor backlogs older than a few seconds.\n\nSafely retire Workers assigned a Build ID by checking reachability across\nall task types. Use the flag \x1b[1m--report-reachability\x1b[0m:\n\n\x1b[1mtemporal task-queue describe \\\n --task-queue YourTaskQueue \\\n --select-build-id \"YourBuildId\" \\\n --report-reachability\x1b[0m\n\nTask reachability information is returned for the requested versions and all\ntask types, which can be used to safely retire Workers with old code versions,\nprovided that they were assigned a Build ID.\n\nNote that task reachability status is experimental and may significantly change\nor be removed in a future release. Also, determining task reachability incurs a\nnon-trivial computing cost.\n\nTask reachability states are reported per build ID. The state may be one of the\nfollowing:\n\n- \x1b[1mReachable\x1b[0m: using the current versioning rules, the Build ID may be used\n by new Workflow Executions or Activities OR there are currently open\n Workflow or backlogged Activity tasks assigned to the queue.\n- \x1b[1mClosedWorkflowsOnly\x1b[0m: the Build ID does not have open Workflow Executions\n and can't be reached by new Workflow Executions. It MAY have closed\n Workflow Executions within the Namespace retention period.\n- \x1b[1mUnreachable\x1b[0m: this Build ID is not used for new Workflow Executions and\n isn't used by any existing Workflow Execution within the retention period.\n\nTask reachability is eventually consistent. You may experience a delay until\nreachability converges to the most accurate value. This is designed to act\nin the most conservative way until convergence. For example, \x1b[1mReachable\x1b[0m is\nmore conservative than \x1b[1mClosedWorkflowsOnly\x1b[0m." } else { - s.Command.Long = "Display a list of active Workers that have recently polled a Task Queue. The\nTemporal Server records each poll request time. A `LastAccessTime` over one\nminute may indicate the Worker is at capacity or has shut down. Temporal\nWorkers are removed if 5 minutes have passed since the last poll request.\n\n```\ntemporal task-queue describe \\\n --task-queue YourTaskQueue\n```\n\nThis command provides poller information for a given Task Queue.\nWorkflow and Activity polling use separate Task Queues:\n\n```\ntemporal task-queue describe \\\n --task-queue YourTaskQueue \\\n --task-queue-type \"activity\"\n```\n\nThis command provides the following task queue statistics:\n- `ApproximateBacklogCount`: The approximate number of tasks backlogged in this\n task queue. May count expired tasks but eventually converges to the right\n value.\n- `ApproximateBacklogAge`: Approximate age of the oldest task in the backlog,\n based on its creation time, measured in seconds.\n- `TasksAddRate`: Approximate rate at which tasks are being added to the task\n queue, measured in tasks per second, averaged over the last 30 seconds.\n Includes tasks dispatched immediately without going to the backlog\n (sync-matched tasks), as well as tasks added to the backlog. (See note below.)\n- `TasksDispatchRate`: Approximate rate at which tasks are being dispatched from\n the task queue, measured in tasks per second, averaged over the last 30\n seconds. Includes tasks dispatched immediately without going to the backlog\n (sync-matched tasks), as well as tasks added to the backlog. (See note below.)\n- `BacklogIncreaseRate`: Approximate rate at which the backlog size is\n increasing (if positive) or decreasing (if negative), measured in tasks per\n second, averaged over the last 30 seconds. This is roughly equivalent to:\n `TasksAddRate` - `TasksDispatchRate`.\n\nNOTE: The `TasksAddRate` and `TasksDispatchRate` metrics may differ from the\nactual rate of add/dispatch, because tasks may be dispatched eagerly to an\navailable worker, or may apply only to specific workers (they are \"sticky\").\nSuch tasks are not counted by these metrics. Despite the inaccuracy of\nthese two metrics, the derived metric of `BacklogIncreaseRate` is accurate\nfor backlogs older than a few seconds.\n\nSafely retire Workers assigned a Build ID by checking reachability across\nall task types. Use the flag `--report-reachability`:\n\n```\ntemporal task-queue describe \\\n --task-queue YourTaskQueue \\\n --build-id \"YourBuildId\" \\\n --report-reachability\n```\n\nTask reachability information is returned for the requested versions and all\ntask types, which can be used to safely retire Workers with old code versions,\nprovided that they were assigned a Build ID.\n\nNote that task reachability status is experimental and may significantly change\nor be removed in a future release. Also, determining task reachability incurs a\nnon-trivial computing cost.\n\nTask reachability states are reported per build ID. The state may be one of the\nfollowing:\n\n- `Reachable`: using the current versioning rules, the Build ID may be used\n by new Workflow Executions or Activities OR there are currently open\n Workflow or backlogged Activity tasks assigned to the queue.\n- `ClosedWorkflowsOnly`: the Build ID does not have open Workflow Executions\n and can't be reached by new Workflow Executions. It MAY have closed\n Workflow Executions within the Namespace retention period.\n- `Unreachable`: this Build ID is not used for new Workflow Executions and\n isn't used by any existing Workflow Execution within the retention period.\n\nTask reachability is eventually consistent. You may experience a delay until\nreachability converges to the most accurate value. This is designed to act\nin the most conservative way until convergence. For example, `Reachable` is\nmore conservative than `ClosedWorkflowsOnly`." + s.Command.Long = "Display a list of active Workers that have recently polled a Task Queue. The\nTemporal Server records each poll request time. A `LastAccessTime` over one\nminute may indicate the Worker is at capacity or has shut down. Temporal\nWorkers are removed if 5 minutes have passed since the last poll request.\n\n```\ntemporal task-queue describe \\\n --task-queue YourTaskQueue\n```\n\nThis command provides poller information for a given Task Queue.\nWorkflow and Activity polling use separate Task Queues:\n\n```\ntemporal task-queue describe \\\n --task-queue YourTaskQueue \\\n --task-queue-type \"activity\"\n```\n\nThis command provides the following task queue statistics:\n- `ApproximateBacklogCount`: The approximate number of tasks backlogged in this\n task queue. May count expired tasks but eventually converges to the right\n value.\n- `ApproximateBacklogAge`: Approximate age of the oldest task in the backlog,\n based on its creation time, measured in seconds.\n- `TasksAddRate`: Approximate rate at which tasks are being added to the task\n queue, measured in tasks per second, averaged over the last 30 seconds.\n Includes tasks dispatched immediately without going to the backlog\n (sync-matched tasks), as well as tasks added to the backlog. (See note below.)\n- `TasksDispatchRate`: Approximate rate at which tasks are being dispatched from\n the task queue, measured in tasks per second, averaged over the last 30\n seconds. Includes tasks dispatched immediately without going to the backlog\n (sync-matched tasks), as well as tasks added to the backlog. (See note below.)\n- `BacklogIncreaseRate`: Approximate rate at which the backlog size is\n increasing (if positive) or decreasing (if negative), measured in tasks per\n second, averaged over the last 30 seconds. This is roughly equivalent to:\n `TasksAddRate` - `TasksDispatchRate`.\n\nNOTE: The `TasksAddRate` and `TasksDispatchRate` metrics may differ from the\nactual rate of add/dispatch, because tasks may be dispatched eagerly to an\navailable worker, or may apply only to specific workers (they are \"sticky\").\nSuch tasks are not counted by these metrics. Despite the inaccuracy of\nthese two metrics, the derived metric of `BacklogIncreaseRate` is accurate\nfor backlogs older than a few seconds.\n\nSafely retire Workers assigned a Build ID by checking reachability across\nall task types. Use the flag `--report-reachability`:\n\n```\ntemporal task-queue describe \\\n --task-queue YourTaskQueue \\\n --select-build-id \"YourBuildId\" \\\n --report-reachability\n```\n\nTask reachability information is returned for the requested versions and all\ntask types, which can be used to safely retire Workers with old code versions,\nprovided that they were assigned a Build ID.\n\nNote that task reachability status is experimental and may significantly change\nor be removed in a future release. Also, determining task reachability incurs a\nnon-trivial computing cost.\n\nTask reachability states are reported per build ID. The state may be one of the\nfollowing:\n\n- `Reachable`: using the current versioning rules, the Build ID may be used\n by new Workflow Executions or Activities OR there are currently open\n Workflow or backlogged Activity tasks assigned to the queue.\n- `ClosedWorkflowsOnly`: the Build ID does not have open Workflow Executions\n and can't be reached by new Workflow Executions. It MAY have closed\n Workflow Executions within the Namespace retention period.\n- `Unreachable`: this Build ID is not used for new Workflow Executions and\n isn't used by any existing Workflow Execution within the retention period.\n\nTask reachability is eventually consistent. You may experience a delay until\nreachability converges to the most accurate value. This is designed to act\nin the most conservative way until convergence. For example, `Reachable` is\nmore conservative than `ClosedWorkflowsOnly`." } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVarP(&s.TaskQueue, "task-queue", "t", "", "Task Queue name. Required.") diff --git a/temporalcli/commandsgen/commands.yml b/temporalcli/commandsgen/commands.yml index 6aeef5bc6..8fe546feb 100644 --- a/temporalcli/commandsgen/commands.yml +++ b/temporalcli/commandsgen/commands.yml @@ -2666,7 +2666,7 @@ commands: ``` temporal task-queue describe \ --task-queue YourTaskQueue \ - --build-id "YourBuildId" \ + --select-build-id "YourBuildId" \ --report-reachability ``` From 1a98d0ad3c940107b74ed305dca9bfafe017d024 Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Thu, 30 Oct 2025 10:12:17 -0700 Subject: [PATCH 14/28] Upgrade server version to 1.29.1 (#870) ## What was changed Upgrade server version to v1.29.1 ## Why? Need new changes for sdk-core integration tests ## Checklist 1. Closes 2. How was this tested: 3. Any docs updates needed? --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8306ed551..708e7427f 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( go.temporal.io/api v1.53.0 go.temporal.io/sdk v1.37.0 go.temporal.io/sdk/contrib/envconfig v0.1.0 - go.temporal.io/server v1.29.0 + go.temporal.io/server v1.29.1 golang.org/x/term v0.32.0 google.golang.org/grpc v1.72.2 google.golang.org/protobuf v1.36.6 diff --git a/go.sum b/go.sum index 9c394a149..dd72dd0cf 100644 --- a/go.sum +++ b/go.sum @@ -389,8 +389,8 @@ go.temporal.io/sdk v1.37.0 h1:RbwCkUQuqY4rfCzdrDZF9lgT7QWG/pHlxfZFq0NPpDQ= go.temporal.io/sdk v1.37.0/go.mod h1:tOy6vGonfAjrpCl6Bbw/8slTgQMiqvoyegRv2ZHPm5M= go.temporal.io/sdk/contrib/envconfig v0.1.0 h1:s+G/Ujph+Xl2jzLiiIm2T1vuijDkUL4Kse49dgDVGBE= go.temporal.io/sdk/contrib/envconfig v0.1.0/go.mod h1:FQEO3C56h9C7M6sDgSanB8HnBTmopw9qgVx4F1S6pJk= -go.temporal.io/server v1.29.0 h1:BGBCvI7vcPokCjuDsfitLx2eS+8ow+yQ4frLJZcn2nQ= -go.temporal.io/server v1.29.0/go.mod h1:pc0n6DRcN06V4WNhaxdxE3KaZIS3KSDNKdca6uu6RuU= +go.temporal.io/server v1.29.1 h1:sgiMveg/8e51lA62pWb9VzC5WTRitKFyT6XsjonHUhE= +go.temporal.io/server v1.29.1/go.mod h1:pc0n6DRcN06V4WNhaxdxE3KaZIS3KSDNKdca6uu6RuU= 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= From 711cdac85c12033b940b05b2ef52f9946ace8feb Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Thu, 30 Oct 2025 14:53:14 -0700 Subject: [PATCH 15/28] Bump temporal UI version (#871) ## What was changed Update UI version to latest release ## Why? Prepare for patch release ## Checklist 1. Closes 2. How was this tested: 3. Any docs updates needed? --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 708e7427f..cf4032f74 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 - github.com/temporalio/ui-server/v2 v2.41.0 + github.com/temporalio/ui-server/v2 v2.42.1 go.temporal.io/api v1.53.0 go.temporal.io/sdk v1.37.0 go.temporal.io/sdk/contrib/envconfig v0.1.0 diff --git a/go.sum b/go.sum index dd72dd0cf..6926371e2 100644 --- a/go.sum +++ b/go.sum @@ -327,8 +327,8 @@ github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb/go.mod h1:143 github.com/temporalio/tchannel-go v1.22.1-0.20220818200552-1be8d8cffa5b/go.mod h1:c+V9Z/ZgkzAdyGvHrvC5AsXgN+M9Qwey04cBdKYzV7U= github.com/temporalio/tchannel-go v1.22.1-0.20240528171429-1db37fdea938 h1:sEJGhmDo+0FaPWM6f0v8Tjia0H5pR6/Baj6+kS78B+M= github.com/temporalio/tchannel-go v1.22.1-0.20240528171429-1db37fdea938/go.mod h1:ezRQRwu9KQXy8Wuuv1aaFFxoCNz5CeNbVOOkh3xctbY= -github.com/temporalio/ui-server/v2 v2.41.0 h1:m9F2jnFJy/dWxjk9d2oS+825r78EX4gUlipoQ1xNO6Y= -github.com/temporalio/ui-server/v2 v2.41.0/go.mod h1:ofEKGV5/NaPbjdmEQRcUDFE6nZPprOemNjLJYLF9IX4= +github.com/temporalio/ui-server/v2 v2.42.1 h1:ajeOxqCnUiCRQQhQYLxaT7wUgF/slqZJtdW4pLjVqCs= +github.com/temporalio/ui-server/v2 v2.42.1/go.mod h1:lKTnn50t8yQvcrarxAOjX33YcfkomkiNB5BH06wQwEE= github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/uber-common/bark v1.0.0/go.mod h1:g0ZuPcD7XiExKHynr93Q742G/sbrdVQkghrqLGOoFuY= From 2445cdaeca0d2501a09837e388aa78ba1d65edaa Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Mon, 3 Nov 2025 09:30:57 -0800 Subject: [PATCH 16/28] Implement ListWorkers and DescribeWorker (#867) ## What was changed Implemented the ListWorkers and DescribeWorker gRPC requests note: list workers doesn't display all heartbeat information. Open to feedback on if we should display more/all/less fields. See screenshots in testing section for an example. PageSize is also not supported in server for the ListWorkers call, so that was left out. Added a `limit` field to allow users to limit how many results are displayed, similar to other CLI commands. Sample output of the two new commands image ## Why? New feature. ## Checklist 1. Closes #868 2. How was this tested: Added tests, because Go SDK doesn't support worker heartbeating yet, we manually send a `RecordWorkerHeartbeatRequest` gRPC request to mimic the behavior of a worker. 3. Any docs updates needed? --- temporalcli/commands.gen.go | 59 +++++ temporalcli/commands.worker.go | 381 +++++++++++++++++++++++++++ temporalcli/commands.worker_test.go | 182 +++++++++++++ temporalcli/commands_test.go | 3 + temporalcli/commandsgen/commands.yml | 33 +++ 5 files changed, 658 insertions(+) create mode 100644 temporalcli/commands.worker.go create mode 100644 temporalcli/commands.worker_test.go diff --git a/temporalcli/commands.gen.go b/temporalcli/commands.gen.go index cab218eca..32da54046 100644 --- a/temporalcli/commands.gen.go +++ b/temporalcli/commands.gen.go @@ -2866,6 +2866,8 @@ func NewTemporalWorkerCommand(cctx *CommandContext, parent *TemporalCommand) *Te } s.Command.Args = cobra.NoArgs s.Command.AddCommand(&NewTemporalWorkerDeploymentCommand(cctx, &s).Command) + s.Command.AddCommand(&NewTemporalWorkerDescribeCommand(cctx, &s).Command) + s.Command.AddCommand(&NewTemporalWorkerListCommand(cctx, &s).Command) s.ClientOptions.buildFlags(cctx, s.Command.PersistentFlags()) return &s } @@ -3217,6 +3219,63 @@ func NewTemporalWorkerDeploymentUpdateMetadataVersionCommand(cctx *CommandContex return &s } +type TemporalWorkerDescribeCommand struct { + Parent *TemporalWorkerCommand + Command cobra.Command + WorkerInstanceKey string +} + +func NewTemporalWorkerDescribeCommand(cctx *CommandContext, parent *TemporalWorkerCommand) *TemporalWorkerDescribeCommand { + var s TemporalWorkerDescribeCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "describe [flags]" + s.Command.Short = "Returns information about a specific worker (EXPERIMENTAL)" + if hasHighlighting { + s.Command.Long = "Look up information of a specific worker.\n\n\x1b[1mtemporal worker describe --namespace YourNamespace --worker-instance-key YourKey\x1b[0m" + } else { + s.Command.Long = "Look up information of a specific worker.\n\n```\ntemporal worker describe --namespace YourNamespace --worker-instance-key YourKey\n```" + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVar(&s.WorkerInstanceKey, "worker-instance-key", "", "Worker instance key to describe. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "worker-instance-key") + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + +type TemporalWorkerListCommand struct { + Parent *TemporalWorkerCommand + Command cobra.Command + Query string + Limit int +} + +func NewTemporalWorkerListCommand(cctx *CommandContext, parent *TemporalWorkerCommand) *TemporalWorkerListCommand { + var s TemporalWorkerListCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "list [flags]" + s.Command.Short = "List worker status information in a specific namespace (EXPERIMENTAL)" + if hasHighlighting { + s.Command.Long = "Get a list of workers to the specified namespace.\n\n\x1b[1mtemporal worker list --namespace YourNamespace --query 'taskQueue=\"YourTaskQueue\"'\x1b[0m" + } else { + s.Command.Long = "Get a list of workers to the specified namespace.\n\n```\ntemporal worker list --namespace YourNamespace --query 'taskQueue=\"YourTaskQueue\"'\n```" + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVarP(&s.Query, "query", "q", "", "Content for an SQL-like `QUERY` List Filter.") + s.Command.Flags().IntVar(&s.Limit, "limit", 0, "Maximum number of workers to display.") + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + type TemporalWorkflowCommand struct { Parent *TemporalCommand Command cobra.Command diff --git a/temporalcli/commands.worker.go b/temporalcli/commands.worker.go new file mode 100644 index 000000000..29eb399fb --- /dev/null +++ b/temporalcli/commands.worker.go @@ -0,0 +1,381 @@ +package temporalcli + +import ( + "fmt" + "strings" + "time" + + "github.com/temporalio/cli/temporalcli/internal/printer" + deploymentpb "go.temporal.io/api/deployment/v1" + enumspb "go.temporal.io/api/enums/v1" + workerpb "go.temporal.io/api/worker/v1" + "go.temporal.io/api/workflowservice/v1" + "google.golang.org/protobuf/types/known/durationpb" +) + +type workerListRow struct { + WorkerInstanceKey string + Status string + TaskQueue string + WorkerIdentity string + HostName string + Deployment string + HeartbeatTime time.Time + Elapsed string +} + +type workerDeploymentVersionRef struct { + DeploymentName string + BuildId string +} + +type workerHostInfo struct { + HostName string + ProcessId string + ProcessKey string + CurrentHostCPUUsage float32 + CurrentHostMemUsage float32 +} + +type workerSlotsInfo struct { + CurrentAvailableSlots int32 + CurrentUsedSlots int32 + SlotSupplierKind string + TotalProcessedTasks int32 + TotalFailedTasks int32 + LastIntervalProcessedTasks int32 + LastIntervalFailureTasks int32 +} + +type workerPollerInfo struct { + CurrentPollers int32 + LastSuccessfulPollTime time.Time + IsAutoscaling bool +} + +type pluginInfo struct { + Name string + Version string +} + +type workerDescribeDetail struct { + WorkerInstanceKey string + WorkerIdentity string + Status string + TaskQueue string + DeploymentVersion *workerDeploymentVersionRef `cli:",cardOmitEmpty"` + SdkName string + SdkVersion string + StartTime time.Time + HeartbeatTime time.Time + ElapsedSinceLastHeartbeat string + HostInfo *workerHostInfo + WorkflowTaskSlotsInfo *workerSlotsInfo `cli:",cardOmitEmpty"` + ActivityTaskSlotsInfo *workerSlotsInfo `cli:",cardOmitEmpty"` + NexusTaskSlotsInfo *workerSlotsInfo `cli:",cardOmitEmpty"` + LocalActivityTaskSlotsInfo *workerSlotsInfo `cli:",cardOmitEmpty"` + WorkflowPollerInfo *workerPollerInfo `cli:",cardOmitEmpty"` + WorkflowStickyPollerInfo *workerPollerInfo `cli:",cardOmitEmpty"` + ActivityPollerInfo *workerPollerInfo `cli:",cardOmitEmpty"` + NexusPollerInfo *workerPollerInfo `cli:",cardOmitEmpty"` + TotalStickyCacheHit int32 + TotalStickyCacheMiss int32 + CurrentStickyCacheSize int32 + Plugins []pluginInfo `cli:",cardOmitEmpty"` +} + +func (c *TemporalWorkerDescribeCommand) run(cctx *CommandContext, args []string) error { + cl, err := c.Parent.ClientOptions.dialClient(cctx) + if err != nil { + return err + } + defer cl.Close() + + resp, err := cl.WorkflowService().DescribeWorker(cctx, &workflowservice.DescribeWorkerRequest{ + Namespace: c.Parent.Namespace, + WorkerInstanceKey: c.WorkerInstanceKey, + }) + if err != nil { + return err + } + + if cctx.JSONOutput { + return cctx.Printer.PrintStructured(resp.GetWorkerInfo(), printer.StructuredOptions{}) + } + + info := resp.GetWorkerInfo() + if info == nil { + return fmt.Errorf("worker info not found in response") + } + + hb := info.GetWorkerHeartbeat() + if hb == nil { + return fmt.Errorf("worker heartbeat not found in response") + } + + formatted := formatWorkerDescribeDetail(hb) + return cctx.Printer.PrintStructured(formatted, printer.StructuredOptions{}) +} + +func (c *TemporalWorkerListCommand) run(cctx *CommandContext, args []string) error { + cl, err := c.Parent.ClientOptions.dialClient(cctx) + if err != nil { + return err + } + defer cl.Close() + + svc := cl.WorkflowService() + + limit := c.Limit + + cctx.Printer.StartList() + defer cctx.Printer.EndList() + + printOpts := printer.StructuredOptions{Table: &printer.TableOptions{}} + page := make([]*workerListRow, 0, 100) + printed := 0 + var token []byte + + for { + req := &workflowservice.ListWorkersRequest{ + Namespace: c.Parent.Namespace, + NextPageToken: token, + Query: c.Query, + } + + resp, err := svc.ListWorkers(cctx, req) + if err != nil { + return err + } + + workers := resp.GetWorkersInfo() + if cctx.JSONOutput { + for _, info := range workers { + if limit > 0 && printed >= limit { + break + } + if info == nil { + continue + } + if err := cctx.Printer.PrintStructured(info, printer.StructuredOptions{}); err != nil { + return err + } + printed++ + } + } else { + for _, info := range workers { + if limit > 0 && printed >= limit { + break + } + if info == nil { + continue + } + hb := info.GetWorkerHeartbeat() + if hb == nil { + continue + } + row := formatWorkerListRow(hb) + page = append(page, &row) + printed++ + if len(page) == cap(page) { + if err := cctx.Printer.PrintStructured(page, printOpts); err != nil { + return err + } + page = page[:0] + printOpts.Table.NoHeader = true + } + } + } + + if limit > 0 && printed >= limit { + break + } + + token = resp.GetNextPageToken() + if len(token) == 0 { + break + } + } + + if !cctx.JSONOutput { + if err := cctx.Printer.PrintStructured(page, printOpts); err != nil { + return err + } + } + + return nil +} + +func formatWorkerListRow(hb *workerpb.WorkerHeartbeat) workerListRow { + if hb == nil { + return workerListRow{} + } + + row := workerListRow{ + WorkerInstanceKey: hb.GetWorkerInstanceKey(), + Status: workerStatusToString(hb.GetStatus()), + TaskQueue: hb.GetTaskQueue(), + WorkerIdentity: hb.GetWorkerIdentity(), + HeartbeatTime: timestampToTime(hb.GetHeartbeatTime()), + Elapsed: durationToString(hb.GetElapsedSinceLastHeartbeat()), + } + + if host := hb.GetHostInfo(); host != nil { + row.HostName = host.GetHostName() + } + if dv := hb.GetDeploymentVersion(); dv != nil { + row.Deployment = formatDeploymentVersion(dv) + } + + return row +} + +func formatWorkerDescribeDetail(hb *workerpb.WorkerHeartbeat) workerDescribeDetail { + if hb == nil { + return workerDescribeDetail{} + } + + detail := workerDescribeDetail{ + WorkerInstanceKey: hb.GetWorkerInstanceKey(), + WorkerIdentity: hb.GetWorkerIdentity(), + Status: workerStatusToString(hb.GetStatus()), + TaskQueue: hb.GetTaskQueue(), + SdkName: hb.GetSdkName(), + SdkVersion: hb.GetSdkVersion(), + StartTime: timestampToTime(hb.GetStartTime()), + HeartbeatTime: timestampToTime(hb.GetHeartbeatTime()), + ElapsedSinceLastHeartbeat: durationToString(hb.GetElapsedSinceLastHeartbeat()), + HostInfo: formatWorkerHostInfo(hb.GetHostInfo()), + WorkflowTaskSlotsInfo: formatWorkerSlots(hb.GetWorkflowTaskSlotsInfo()), + ActivityTaskSlotsInfo: formatWorkerSlots(hb.GetActivityTaskSlotsInfo()), + NexusTaskSlotsInfo: formatWorkerSlots(hb.GetNexusTaskSlotsInfo()), + LocalActivityTaskSlotsInfo: formatWorkerSlots(hb.GetLocalActivitySlotsInfo()), + WorkflowPollerInfo: formatWorkerPoller(hb.GetWorkflowPollerInfo()), + WorkflowStickyPollerInfo: formatWorkerPoller(hb.GetWorkflowStickyPollerInfo()), + ActivityPollerInfo: formatWorkerPoller(hb.GetActivityPollerInfo()), + NexusPollerInfo: formatWorkerPoller(hb.GetNexusPollerInfo()), + TotalStickyCacheHit: hb.GetTotalStickyCacheHit(), + TotalStickyCacheMiss: hb.GetTotalStickyCacheMiss(), + CurrentStickyCacheSize: hb.GetCurrentStickyCacheSize(), + Plugins: formatPlugins(hb.GetPlugins()), + } + + if dv := hb.GetDeploymentVersion(); dv != nil { + if dv.GetDeploymentName() != "" || dv.GetBuildId() != "" { + detail.DeploymentVersion = &workerDeploymentVersionRef{ + DeploymentName: dv.GetDeploymentName(), + BuildId: dv.GetBuildId(), + } + } + } + + return detail +} + +func workerStatusToString(status enumspb.WorkerStatus) string { + statusStr := status.String() + statusStr = strings.TrimPrefix(statusStr, "WORKER_STATUS_") + if statusStr == "" { + return "unspecified" + } + return statusStr +} + +func formatDeploymentVersion(dv *deploymentpb.WorkerDeploymentVersion) string { + if dv == nil { + return "" + } + depName := dv.GetDeploymentName() + buildID := dv.GetBuildId() + switch { + case depName != "" && buildID != "": + return depName + "@" + buildID + case depName != "": + return depName + case buildID != "": + return buildID + default: + return "" + } +} + +func formatWorkerHostInfo(info *workerpb.WorkerHostInfo) *workerHostInfo { + if info == nil { + return nil + } + formatted := &workerHostInfo{ + HostName: info.GetHostName(), + ProcessId: info.GetProcessId(), + ProcessKey: info.GetProcessKey(), + CurrentHostCPUUsage: info.GetCurrentHostCpuUsage(), + CurrentHostMemUsage: info.GetCurrentHostMemUsage(), + } + if formatted.HostName == "" && formatted.ProcessId == "" && formatted.ProcessKey == "" && + formatted.CurrentHostCPUUsage == 0 && formatted.CurrentHostMemUsage == 0 { + return nil + } + return formatted +} + +func formatWorkerSlots(info *workerpb.WorkerSlotsInfo) *workerSlotsInfo { + if info == nil { + return nil + } + formatted := &workerSlotsInfo{ + CurrentAvailableSlots: info.GetCurrentAvailableSlots(), + CurrentUsedSlots: info.GetCurrentUsedSlots(), + SlotSupplierKind: info.GetSlotSupplierKind(), + TotalProcessedTasks: info.GetTotalProcessedTasks(), + TotalFailedTasks: info.GetTotalFailedTasks(), + LastIntervalProcessedTasks: info.GetLastIntervalProcessedTasks(), + LastIntervalFailureTasks: info.GetLastIntervalFailureTasks(), + } + if formatted.CurrentAvailableSlots == 0 && formatted.CurrentUsedSlots == 0 && formatted.SlotSupplierKind == "Fixed" && + formatted.TotalProcessedTasks == 0 && formatted.TotalFailedTasks == 0 && + formatted.LastIntervalProcessedTasks == 0 && formatted.LastIntervalFailureTasks == 0 { + return nil + } + return formatted +} + +func formatWorkerPoller(info *workerpb.WorkerPollerInfo) *workerPollerInfo { + if info == nil { + return nil + } + formatted := &workerPollerInfo{ + CurrentPollers: info.GetCurrentPollers(), + LastSuccessfulPollTime: timestampToTime(info.GetLastSuccessfulPollTime()), + IsAutoscaling: info.GetIsAutoscaling(), + } + if formatted.CurrentPollers == 0 && formatted.LastSuccessfulPollTime.IsZero() && !formatted.IsAutoscaling { + return nil + } + return formatted +} + +func formatPlugins(plugins []*workerpb.PluginInfo) []pluginInfo { + if len(plugins) == 0 { + return nil + } + formatted := make([]pluginInfo, 0, len(plugins)) + for _, plugin := range plugins { + if plugin == nil { + continue + } + formatted = append(formatted, pluginInfo{ + Name: plugin.GetName(), + Version: plugin.GetVersion(), + }) + } + if len(formatted) == 0 { + return nil + } + return formatted +} + +func durationToString(d *durationpb.Duration) string { + if d == nil { + return "" + } + return d.AsDuration().String() +} diff --git a/temporalcli/commands.worker_test.go b/temporalcli/commands.worker_test.go new file mode 100644 index 000000000..65505186f --- /dev/null +++ b/temporalcli/commands.worker_test.go @@ -0,0 +1,182 @@ +package temporalcli_test + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + deploymentpb "go.temporal.io/api/deployment/v1" + enumspb "go.temporal.io/api/enums/v1" + workerpb "go.temporal.io/api/worker/v1" + "go.temporal.io/api/workflowservice/v1" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type heartbeatJSON struct { + Heartbeat workerHeartbeat `json:"workerHeartbeat"` +} + +type workerHeartbeat struct { + WorkerInstanceKey string `json:"workerInstanceKey"` + WorkerIdentity string `json:"workerIdentity"` + HostInfo hostInfo `json:"hostInfo"` + TaskQueue string `json:"taskQueue"` + DeploymentVersion deploymentVersion `json:"deploymentVersion"` + SdkName string `json:"sdkName"` + SdkVersion string `json:"sdkVersion"` + Status string `json:"status"` + StartTime time.Time `json:"startTime"` + HeartbeatTime time.Time `json:"heartbeatTime"` + ElapsedSinceLastHeartbeat string `json:"elapsedSinceLastHeartbeat"` +} +type hostInfo struct { + HostName string `json:"hostName"` +} + +type deploymentVersion struct { + DeploymentName string `json:"deploymentName"` + BuildId string `json:"buildId"` +} + +func (s *SharedServerSuite) TestWorkerHeartbeat_List() { + heartbeat, err := s.recordWorkerHeartbeat() + s.NoError(err) + + sentHeartbeatJSON := s.waitForWorkerListJSON(heartbeat.TaskQueue, heartbeat.WorkerInstanceKey) + + s.Equal(heartbeat.WorkerIdentity, sentHeartbeatJSON.Heartbeat.WorkerIdentity) + s.Equal(heartbeat.TaskQueue, sentHeartbeatJSON.Heartbeat.TaskQueue) + s.Equal(heartbeat.WorkerInstanceKey, sentHeartbeatJSON.Heartbeat.WorkerInstanceKey) + s.False(sentHeartbeatJSON.Heartbeat.HeartbeatTime.IsZero()) + + res := s.Execute( + "worker", "list", + "--address", s.Address(), + "--query", fmt.Sprintf("TaskQueue=\"%s\"", heartbeat.TaskQueue), + ) + s.NoError(res.Err) + s.ContainsOnSameLine(res.Stdout.String(), heartbeat.WorkerInstanceKey, heartbeat.TaskQueue, heartbeat.WorkerIdentity) +} + +func (s *SharedServerSuite) TestWorkerHeartbeat_Describe() { + heartbeat, err := s.recordWorkerHeartbeat() + s.NoError(err) + + listJSON := s.waitForWorkerListJSON(heartbeat.TaskQueue, heartbeat.WorkerInstanceKey) + + var detail heartbeatJSON + res := s.Execute( + "worker", "describe", + "--address", s.Address(), + "--worker-instance-key", heartbeat.WorkerInstanceKey, + "--output", "json", + ) + s.NoError(res.Err) + + var parsed heartbeatJSON + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &parsed)) + + s.Equal(heartbeat.WorkerInstanceKey, parsed.Heartbeat.WorkerInstanceKey) + s.Equal(heartbeat.TaskQueue, parsed.Heartbeat.TaskQueue) + s.Equal(heartbeat.WorkerIdentity, parsed.Heartbeat.WorkerIdentity) + s.False(parsed.Heartbeat.StartTime.IsZero()) + s.False(parsed.Heartbeat.HeartbeatTime.IsZero()) + + detail = parsed + + s.Equal(heartbeat.WorkerInstanceKey, detail.Heartbeat.WorkerInstanceKey) + s.Equal(heartbeat.TaskQueue, detail.Heartbeat.TaskQueue) + s.Equal(heartbeat.WorkerIdentity, detail.Heartbeat.WorkerIdentity) + // Ensure that JSON output for list and describe are identical + s.Equal(listJSON, detail) + + res = s.Execute( + "worker", "describe", + "--address", s.Address(), + "--worker-instance-key", heartbeat.WorkerInstanceKey, + ) + s.NoError(res.Err) + s.Contains(res.Stdout.String(), heartbeat.WorkerIdentity) +} + +func (s *SharedServerSuite) recordWorkerHeartbeat() (*workerpb.WorkerHeartbeat, error) { + workerIdentity := "heartbeat-list-" + uuid.NewString() + taskQueue := "heartbeat-tq-" + uuid.NewString() + instanceKey := "heartbeat-instance-" + uuid.NewString() + hostName := "heartbeat-host-" + uuid.NewString() + deploymentName := "heartbeat-deployment-" + uuid.NewString() + buildID := "heartbeat-build-" + uuid.NewString() + status := enumspb.WORKER_STATUS_RUNNING + startTime := time.Now().UTC().Add(-1 * time.Minute).Truncate(time.Second) + heartbeatTime := time.Now().UTC().Truncate(time.Second) + + hb := &workerpb.WorkerHeartbeat{ + WorkerInstanceKey: instanceKey, + WorkerIdentity: workerIdentity, + TaskQueue: taskQueue, + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildId: buildID, + }, + HostInfo: &workerpb.WorkerHostInfo{ + HostName: hostName, + }, + SdkName: "temporal-go-sdk", + SdkVersion: "v0.0.0-test", + Status: status, + StartTime: timestamppb.New(startTime), + HeartbeatTime: timestamppb.New(heartbeatTime), + ElapsedSinceLastHeartbeat: durationpb.New(5 * time.Second), + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + _, err := s.Client.WorkflowService().RecordWorkerHeartbeat(ctx, &workflowservice.RecordWorkerHeartbeatRequest{ + Namespace: s.Namespace(), + Identity: identity, + WorkerHeartbeat: []*workerpb.WorkerHeartbeat{hb}, + }) + s.NoError(err) + + return hb, nil +} + +func (s *SharedServerSuite) waitForWorkerListJSON(taskQueue, instanceKey string) heartbeatJSON { + var row heartbeatJSON + + s.EventuallyWithT(func(t *assert.CollectT) { + res := s.Execute( + "worker", "list", + "--address", s.Address(), + "--query", fmt.Sprintf("TaskQueue=\"%s\"", taskQueue), + "--output", "json", + ) + assert.NoError(t, res.Err) + + var parsed []heartbeatJSON + if !assert.NoError(t, json.Unmarshal(res.Stdout.Bytes(), &parsed)) { + return + } + if len(parsed) == 0 { + assert.Fail(t, "no workers returned yet") + return + } + + for _, candidate := range parsed { + if candidate.Heartbeat.WorkerInstanceKey == instanceKey { + row = candidate + return + } + } + + assert.Failf(t, "worker instance key not yet found", "%s", instanceKey) + }, 5*time.Second, 200*time.Millisecond) + + s.NotEmpty(row.Heartbeat.WorkerInstanceKey) + return row +} diff --git a/temporalcli/commands_test.go b/temporalcli/commands_test.go index 5d1e50289..7680e9e7c 100644 --- a/temporalcli/commands_test.go +++ b/temporalcli/commands_test.go @@ -238,6 +238,9 @@ func (s *SharedServerSuite) SetupSuite() { // this is overridden since we don't want caching to be enabled // while testing DescribeTaskQueue behaviour related to versioning "matching.TaskQueueInfoByBuildIdTTL": 0 * time.Second, + // worker heartbeating + "frontend.WorkerHeartbeatsEnabled": true, + "frontend.ListWorkersEnabled": true, }, }, }) diff --git a/temporalcli/commandsgen/commands.yml b/temporalcli/commandsgen/commands.yml index 8fe546feb..631a83ffd 100644 --- a/temporalcli/commandsgen/commands.yml +++ b/temporalcli/commandsgen/commands.yml @@ -886,6 +886,8 @@ commands: keywords: - worker - worker deployment + - worker list + - worker describe tags: - Temporal CLI @@ -1404,6 +1406,37 @@ commands: type: bool description: Don't prompt to confirm unset Manager Identity. + - name: temporal worker list + summary: List worker status information in a specific namespace (EXPERIMENTAL) + description: | + Get a list of workers to the specified namespace. + + ``` + temporal worker list --namespace YourNamespace --query 'taskQueue="YourTaskQueue"' + ``` + options: + - name: query + short: q + type: string + description: Content for an SQL-like `QUERY` List Filter. + - name: limit + type: int + description: Maximum number of workers to display. + + - name: temporal worker describe + summary: Returns information about a specific worker (EXPERIMENTAL) + description: | + Look up information of a specific worker. + + ``` + temporal worker describe --namespace YourNamespace --worker-instance-key YourKey + ``` + options: + - name: worker-instance-key + type: string + description: Worker instance key to describe. + required: true + - name: temporal env summary: Manage environments description: | From d1bdd23a126b3b813c7e367fc88132441ec900fd Mon Sep 17 00:00:00 2001 From: Kent Gruber Date: Wed, 12 Nov 2025 10:51:44 -0500 Subject: [PATCH 17/28] VLN-442: Use first-party action for GitHub app tokens (#872) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - `.github/workflows/trigger-publish.yml`: Set the GitHub App token step to include owner temporalio and added repositories list for cli and docker-builds so the generated token retains access to both repos. - `.github/workflows/trigger-docs.yml`: Upgraded the GitHub App token step to actions/create-github-app-token@v2 and switched repositories input to a multiline list (documentation) with an explanatory comment while keeping the owner scoped to the repository owner. --- Previous summary: ## Summary - `.github/workflows/trigger-publish.yml`: Replaced tibdex/github-app-token with actions/create-github-app-token@v2 and converted inputs to the new action’s kebab-case names so the workflow continues generating the app token securely. --- .github/workflows/trigger-docs.yml | 6 ++++-- .github/workflows/trigger-publish.yml | 10 +++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/trigger-docs.yml b/.github/workflows/trigger-docs.yml index 8b62778bf..645d976b5 100644 --- a/.github/workflows/trigger-docs.yml +++ b/.github/workflows/trigger-docs.yml @@ -32,12 +32,14 @@ jobs: - name: Generate token id: generate_token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 with: app-id: ${{ secrets.TEMPORAL_CICD_APP_ID }} private-key: ${{ secrets.TEMPORAL_CICD_PRIVATE_KEY }} owner: ${{ github.repository_owner }} - repositories: documentation # generate a token with permissions to trigger GHA in documentation repo + # Generate a token with permissions to trigger GHA in documentation repo. + repositories: | + documentation - name: Trigger Documentation Workflow env: diff --git a/.github/workflows/trigger-publish.yml b/.github/workflows/trigger-publish.yml index 04eee3192..6f65fefe4 100644 --- a/.github/workflows/trigger-publish.yml +++ b/.github/workflows/trigger-publish.yml @@ -18,10 +18,14 @@ jobs: steps: - name: Generate a token id: generate_token - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + uses: actions/create-github-app-token@v2 with: - app_id: ${{ secrets.TEMPORAL_CICD_APP_ID }} - private_key: ${{ secrets.TEMPORAL_CICD_PRIVATE_KEY }} + app-id: ${{ secrets.TEMPORAL_CICD_APP_ID }} + private-key: ${{ secrets.TEMPORAL_CICD_PRIVATE_KEY }} + owner: temporalio + repositories: | + cli + docker-builds - name: Dispatch docker builds Github Action env: From 1b9b20c94ebf0ae7c9a5b0a96a9dd65bd1e6291b Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 13 Nov 2025 09:30:29 -0600 Subject: [PATCH 18/28] fix: improve consistency of backticks in usage messages (#863) ## What was changed * Remove extraneous trailing backticks * Add missing backticks where obviously needed * Improve consistency of quoting This doesn't fix every inconsistent use of backticks, but it does remove a few dangling backticks, and in places where quoting was inconsistent (i.e., a mix of backticks, quotes, and/or no quotes in a single usage message) makes it slightly more consistent. ## Why? Prettier usage :) --- temporalcli/commands.gen.go | 18 +++++++++--------- temporalcli/commandsgen/commands.yml | 20 ++++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/temporalcli/commands.gen.go b/temporalcli/commands.gen.go index 32da54046..f01ce7d82 100644 --- a/temporalcli/commands.gen.go +++ b/temporalcli/commands.gen.go @@ -379,7 +379,7 @@ func NewTemporalCommand(cctx *CommandContext) *TemporalCommand { s.Command.PersistentFlags().StringVar(&s.Env, "env", "default", "Active environment name (`ENV`).") cctx.BindFlagEnvVar(s.Command.PersistentFlags().Lookup("env"), "TEMPORAL_ENV") s.Command.PersistentFlags().StringVar(&s.EnvFile, "env-file", "", "Path to environment settings file. Defaults to `$HOME/.config/temporalio/temporal.yaml`.") - s.Command.PersistentFlags().StringVar(&s.ConfigFile, "config-file", "", "File path to read TOML config from, defaults to `$CONFIG_PATH/temporal/temporal.toml` where `$CONFIG_PATH` is defined as `$HOME/.config` on Unix, \"$HOME/Library/Application Support\" on macOS, and %AppData% on Windows. EXPERIMENTAL.") + s.Command.PersistentFlags().StringVar(&s.ConfigFile, "config-file", "", "File path to read TOML config from, defaults to `$CONFIG_PATH/temporal/temporal.toml` where `$CONFIG_PATH` is defined as `$HOME/.config` on Unix, `$HOME/Library/Application Support` on macOS, and `%AppData%` on Windows. EXPERIMENTAL.") s.Command.PersistentFlags().StringVar(&s.Profile, "profile", "", "Profile to use for config file. EXPERIMENTAL.") s.Command.PersistentFlags().BoolVar(&s.DisableConfigFile, "disable-config-file", false, "If set, disables loading environment config from config file. EXPERIMENTAL.") s.Command.PersistentFlags().BoolVar(&s.DisableConfigEnv, "disable-config-env", false, "If set, disables loading environment config from environment variables. EXPERIMENTAL.") @@ -1332,14 +1332,14 @@ func NewTemporalOperatorNamespaceCreateCommand(cctx *CommandContext, parent *Tem s.Command.Use = "create [flags]" s.Command.Short = "Register a new Namespace" if hasHighlighting { - s.Command.Long = "Create a new Namespace on the Temporal Service:\n\n\x1b[1mtemporal operator namespace create \\\n --namespace YourNewNamespaceName \\\n [options]\x1b[0m`\n\nCreate a Namespace with multi-region data replication:\n\n\x1b[1mtemporal operator namespace create \\\n --global \\\n --namespace YourNewNamespaceName\x1b[0m\n\nConfigure settings like retention and Visibility Archival State as needed.\nFor example, the Visibility Archive can be set on a separate URI:\n\n\x1b[1mtemporal operator namespace create \\\n --retention 5d \\\n --visibility-archival-state enabled \\\n --visibility-uri YourURI \\\n --namespace YourNewNamespaceName\x1b[0m\n\nNote: URI values for archival states can't be changed once enabled." + s.Command.Long = "Create a new Namespace on the Temporal Service:\n\n\x1b[1mtemporal operator namespace create \\\n --namespace YourNewNamespaceName \\\n [options]\x1b[0m\n\nCreate a Namespace with multi-region data replication:\n\n\x1b[1mtemporal operator namespace create \\\n --global \\\n --namespace YourNewNamespaceName\x1b[0m\n\nConfigure settings like retention and Visibility Archival State as needed.\nFor example, the Visibility Archive can be set on a separate URI:\n\n\x1b[1mtemporal operator namespace create \\\n --retention 5d \\\n --visibility-archival-state enabled \\\n --visibility-uri YourURI \\\n --namespace YourNewNamespaceName\x1b[0m\n\nNote: URI values for archival states can't be changed once enabled." } else { - s.Command.Long = "Create a new Namespace on the Temporal Service:\n\n```\ntemporal operator namespace create \\\n --namespace YourNewNamespaceName \\\n [options]\n````\n\nCreate a Namespace with multi-region data replication:\n\n```\ntemporal operator namespace create \\\n --global \\\n --namespace YourNewNamespaceName\n```\n\nConfigure settings like retention and Visibility Archival State as needed.\nFor example, the Visibility Archive can be set on a separate URI:\n\n```\ntemporal operator namespace create \\\n --retention 5d \\\n --visibility-archival-state enabled \\\n --visibility-uri YourURI \\\n --namespace YourNewNamespaceName\n```\n\nNote: URI values for archival states can't be changed once enabled." + s.Command.Long = "Create a new Namespace on the Temporal Service:\n\n```\ntemporal operator namespace create \\\n --namespace YourNewNamespaceName \\\n [options]\n```\n\nCreate a Namespace with multi-region data replication:\n\n```\ntemporal operator namespace create \\\n --global \\\n --namespace YourNewNamespaceName\n```\n\nConfigure settings like retention and Visibility Archival State as needed.\nFor example, the Visibility Archive can be set on a separate URI:\n\n```\ntemporal operator namespace create \\\n --retention 5d \\\n --visibility-archival-state enabled \\\n --visibility-uri YourURI \\\n --namespace YourNewNamespaceName\n```\n\nNote: URI values for archival states can't be changed once enabled." } s.Command.Args = cobra.MaximumNArgs(1) s.Command.Flags().StringVar(&s.ActiveCluster, "active-cluster", "", "Active Cluster (Service) name.") s.Command.Flags().StringArrayVar(&s.Cluster, "cluster", nil, "Cluster (Service) names for Namespace creation. Can be passed multiple times.") - s.Command.Flags().StringArrayVar(&s.Data, "data", nil, "Namespace data as `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. For example: 'YourKey={\"your\": \"value\"}'. Can be passed multiple times.") + s.Command.Flags().StringArrayVar(&s.Data, "data", nil, "Namespace data as `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. For example: `YourKey={\"your\": \"value\"}` Can be passed multiple times.") s.Command.Flags().StringVar(&s.Description, "description", "", "Namespace description.") s.Command.Flags().StringVar(&s.Email, "email", "", "Owner email.") s.Command.Flags().BoolVar(&s.Global, "global", false, "Enable multi-region data replication.") @@ -1469,7 +1469,7 @@ func NewTemporalOperatorNamespaceUpdateCommand(cctx *CommandContext, parent *Tem s.Command.Args = cobra.MaximumNArgs(1) s.Command.Flags().StringVar(&s.ActiveCluster, "active-cluster", "", "Active Cluster (Service) name.") s.Command.Flags().StringArrayVar(&s.Cluster, "cluster", nil, "Cluster (Service) names.") - s.Command.Flags().StringArrayVar(&s.Data, "data", nil, "Namespace data as `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. For example: 'YourKey={\"your\": \"value\"}'. Can be passed multiple times.") + s.Command.Flags().StringArrayVar(&s.Data, "data", nil, "Namespace data as `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. For example: `YourKey={\"your\": \"value\"}` Can be passed multiple times.") s.Command.Flags().StringVar(&s.Description, "description", "", "Namespace description.") s.Command.Flags().StringVar(&s.Email, "email", "", "Owner email.") s.Command.Flags().BoolVar(&s.PromoteGlobal, "promote-global", false, "Enable multi-region data replication.") @@ -2136,7 +2136,7 @@ func NewTemporalServerStartDevCommand(cctx *CommandContext, parent *TemporalServ s.Command.Flags().StringVar(&s.UiAssetPath, "ui-asset-path", "", "UI custom assets path.") s.Command.Flags().StringVar(&s.UiCodecEndpoint, "ui-codec-endpoint", "", "UI remote codec HTTP endpoint.") s.Command.Flags().StringArrayVar(&s.SqlitePragma, "sqlite-pragma", nil, "SQLite pragma statements in \"PRAGMA=VALUE\" format.") - s.Command.Flags().StringArrayVar(&s.DynamicConfigValue, "dynamic-config-value", nil, "Dynamic configuration value using `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. For example: 'YourKey=\"YourString\"'. Can be passed multiple times.") + s.Command.Flags().StringArrayVar(&s.DynamicConfigValue, "dynamic-config-value", nil, "Dynamic configuration value using `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. For example: `YourKey=\"YourString\"` Can be passed multiple times.") s.Command.Flags().BoolVar(&s.LogConfig, "log-config", false, "Log the server config to stderr.") s.Command.Flags().StringArrayVar(&s.SearchAttribute, "search-attribute", nil, "Search attributes to register using `KEY=VALUE` pairs. Keys must be identifiers, and values must be the search attribute type, which is one of the following: Text, Keyword, Int, Double, Bool, Datetime, KeywordList.") s.Command.Run = func(c *cobra.Command, args []string) { @@ -3208,7 +3208,7 @@ func NewTemporalWorkerDeploymentUpdateMetadataVersionCommand(cctx *CommandContex s.Command.Long = "```\n+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\n```\n\nUpdate metadata associated with a Worker Deployment Version.\n\nFor example:\n\n```\n temporal worker deployment update-metadata-version \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --metadata bar=1 \\\n --metadata foo=true\n```\n\nThe current metadata is also returned with `describe-version`:\n```\n temporal worker deployment describe-version \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n```" } s.Command.Args = cobra.NoArgs - s.Command.Flags().StringArrayVar(&s.Metadata, "metadata", nil, "Set deployment metadata using `KEY=\"VALUE\"` pairs. Keys must be identifiers, and values must be JSON values. For example: 'YourKey={\"your\": \"value\"}'. Can be passed multiple times.") + s.Command.Flags().StringArrayVar(&s.Metadata, "metadata", nil, "Set deployment metadata using `KEY=\"VALUE\"` pairs. Keys must be identifiers, and values must be JSON values. For example: `YourKey={\"your\": \"value\"}` Can be passed multiple times.") s.Command.Flags().StringArrayVar(&s.RemoveEntries, "remove-entries", nil, "Keys of entries to be deleted from metadata. Can be passed multiple times.") s.DeploymentVersionOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { @@ -3565,9 +3565,9 @@ func NewTemporalWorkflowListCommand(cctx *CommandContext, parent *TemporalWorkfl s.Command.Use = "list [flags]" s.Command.Short = "Show Workflow Executions" if hasHighlighting { - s.Command.Long = "List Workflow Executions. The optional \x1b[1m--query\x1b[0m limits the output to\nWorkflows matching a Query:\n\n\x1b[1mtemporal workflow list \\\n --query YourQuery\x1b[1m\x1b[0m\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See \x1b[0mtemporal batch --help` for a quick reference.\n\nView a list of archived Workflow Executions:\n\n\x1b[1mtemporal workflow list \\\n --archived\x1b[0m" + s.Command.Long = "List Workflow Executions. The optional \x1b[1m--query\x1b[0m limits the output to\nWorkflows matching a Query:\n\n\x1b[1mtemporal workflow list \\\n --query YourQuery\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.\n\nView a list of archived Workflow Executions:\n\n\x1b[1mtemporal workflow list \\\n --archived\x1b[0m" } else { - s.Command.Long = "List Workflow Executions. The optional `--query` limits the output to\nWorkflows matching a Query:\n\n```\ntemporal workflow list \\\n --query YourQuery`\n```\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See `temporal batch --help` for a quick reference.\n\nView a list of archived Workflow Executions:\n\n```\ntemporal workflow list \\\n --archived\n```" + s.Command.Long = "List Workflow Executions. The optional `--query` limits the output to\nWorkflows matching a Query:\n\n```\ntemporal workflow list \\\n --query YourQuery\n```\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See `temporal batch --help` for a quick reference.\n\nView a list of archived Workflow Executions:\n\n```\ntemporal workflow list \\\n --archived\n```" } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVarP(&s.Query, "query", "q", "", "Content for an SQL-like `QUERY` List Filter.") diff --git a/temporalcli/commandsgen/commands.yml b/temporalcli/commandsgen/commands.yml index 631a83ffd..cc595d8be 100644 --- a/temporalcli/commandsgen/commands.yml +++ b/temporalcli/commandsgen/commands.yml @@ -38,7 +38,7 @@ # temporal env set \ # --env prod \ # --key tls-cert-path \ -# --value /home/my-user/certs/cluster.cert` +# --value /home/my-user/certs/cluster.cert # ``` # * No: `temporal env set prod.tls-cert-path /home/my-user/certs/cluster.cert`. # * Split invocation samples to multiple lines. @@ -113,7 +113,7 @@ # * name, summary, and descrption are required fields. All other fields are optional. # * Available option types are `bool`, `duration`, `int`, `float`, `string`, `string[]`, -# `string-enum`, string-enum[], or `timestamp`. +# `string-enum`, `string-enum[]`, or `timestamp`. # * Include a new-line after each command entry. # OPTION SET OVERVIEW @@ -166,8 +166,8 @@ commands: description: | File path to read TOML config from, defaults to `$CONFIG_PATH/temporal/temporal.toml` where `$CONFIG_PATH` is defined - as `$HOME/.config` on Unix, "$HOME/Library/Application Support" on - macOS, and %AppData% on Windows. + as `$HOME/.config` on Unix, `$HOME/Library/Application Support` on + macOS, and `%AppData%` on Windows. experimental: true implied-env: TEMPORAL_CONFIG_FILE - name: profile @@ -1251,7 +1251,7 @@ commands: description: | Set deployment metadata using `KEY="VALUE"` pairs. Keys must be identifiers, and values must be JSON values. - For example: 'YourKey={"your": "value"}'. + For example: `YourKey={"your": "value"}` Can be passed multiple times. - name: remove-entries type: string[] @@ -1768,7 +1768,7 @@ commands: temporal operator namespace create \ --namespace YourNewNamespaceName \ [options] - ```` + ``` Create a Namespace with multi-region data replication: @@ -1805,7 +1805,7 @@ commands: description: | Namespace data as `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. - For example: 'YourKey={"your": "value"}'. + For example: `YourKey={"your": "value"}` Can be passed multiple times. - name: description type: string @@ -1945,7 +1945,7 @@ commands: description: | Namespace data as `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. - For example: 'YourKey={"your": "value"}'. + For example: `YourKey={"your": "value"}` Can be passed multiple times. - name: description type: string @@ -2597,7 +2597,7 @@ commands: description: | Dynamic configuration value using `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. - For example: 'YourKey="YourString"'. + For example: `YourKey="YourString"` Can be passed multiple times. - name: log-config type: bool @@ -3698,7 +3698,7 @@ commands: ``` temporal workflow list \ - --query YourQuery` + --query YourQuery ``` Visit https://docs.temporal.io/visibility to read more about Search Attributes From 959f910ab3e52c887befe3c0cf4c978b41ec6f7c Mon Sep 17 00:00:00 2001 From: Stephan Behnke Date: Mon, 17 Nov 2025 16:15:31 -0800 Subject: [PATCH 19/28] Move to internal package (#874) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What was changed **Before** ``` /temporalcli /internal /cmd /... commands.go ``` **After** ``` /internal /cmd /temporalcli /... commands.go ``` Also see the related https://github.com/temporalio/documentation/pull/3997. ## Why? We want to establish a `cliext` that will contain the shared code for [writing CLI extensions](https://github.com/temporalio/proposals/blob/master/cli/cli-extensions.md). > The existing github.com/temporalio/cli/temporalcli package should not be used and may be moved to internal as part of this project. ## Checklist 1. Closes 2. How was this tested: 3. Any docs updates needed? --- .github/workflows/ci.yaml | 6 +++--- .goreleaser.yml | 2 +- CONTRIBUTING.md | 14 +++++++------- Makefile | 8 ++++---- cmd/temporal/main.go | 2 +- .../internal => internal}/cmd/gen-commands/main.go | 7 ++++--- .../internal => internal}/cmd/gen-docs/main.go | 9 +++++---- {temporalcli => internal}/commandsgen/code.go | 0 {temporalcli => internal}/commandsgen/commands.yml | 0 {temporalcli => internal}/commandsgen/docs.go | 2 +- {temporalcli => internal}/commandsgen/parse.go | 2 +- {temporalcli => internal}/devserver/freeport.go | 0 .../devserver/freeport_test.go | 2 +- {temporalcli => internal}/devserver/log.go | 0 {temporalcli => internal}/devserver/server.go | 0 .../internal => internal}/printer/printer.go | 0 .../internal => internal}/printer/printer_test.go | 2 +- .../internal => internal}/printer/test/main.go | 2 +- {temporalcli => internal/temporalcli}/client.go | 0 .../temporalcli}/commands.activity.go | 2 +- .../temporalcli}/commands.activity_test.go | 1 - .../temporalcli}/commands.batch.go | 2 +- .../temporalcli}/commands.batch_test.go | 0 .../temporalcli}/commands.config.go | 2 +- .../temporalcli}/commands.config_test.go | 0 .../temporalcli}/commands.env.go | 2 +- .../temporalcli}/commands.env_test.go | 0 .../temporalcli}/commands.gen.go | 0 {temporalcli => internal/temporalcli}/commands.go | 4 ++-- .../temporalcli}/commands.operator_cluster.go | 2 +- .../temporalcli}/commands.operator_cluster_test.go | 4 ++-- .../temporalcli}/commands.operator_namespace.go | 2 +- .../commands.operator_namespace_test.go | 2 +- .../temporalcli}/commands.operator_nexus.go | 2 +- .../temporalcli}/commands.operator_nexus_test.go | 2 +- .../commands.operator_search_attribute.go | 2 +- .../commands.operator_search_attribute_test.go | 2 +- .../temporalcli}/commands.schedule.go | 2 +- .../temporalcli}/commands.schedule_test.go | 3 +-- .../temporalcli}/commands.server.go | 3 ++- .../temporalcli}/commands.server_test.go | 2 +- .../temporalcli}/commands.taskqueue.go | 2 +- .../temporalcli}/commands.taskqueue.helper.go | 2 +- .../commands.taskqueue_build_id_test.go | 0 .../temporalcli}/commands.taskqueue_config.go | 2 +- .../temporalcli}/commands.taskqueue_config_test.go | 0 .../commands.taskqueue_get_build_id.go | 2 +- .../temporalcli}/commands.taskqueue_test.go | 7 +++---- .../commands.taskqueue_update_build_ids.go | 0 .../commands.taskqueue_versioning_rules.go | 2 +- .../commands.taskqueue_versioning_rules_test.go | 0 .../temporalcli}/commands.worker.deployment.go | 2 +- .../commands.worker.deployment_test.go | 0 .../temporalcli}/commands.worker.go | 2 +- .../temporalcli}/commands.worker_test.go | 0 .../temporalcli}/commands.workflow.go | 2 +- .../temporalcli}/commands.workflow_exec.go | 2 +- .../temporalcli}/commands.workflow_exec_test.go | 0 .../temporalcli}/commands.workflow_fix.go | 0 .../temporalcli}/commands.workflow_reset.go | 3 +-- .../temporalcli}/commands.workflow_reset_test.go | 0 .../commands.workflow_reset_update_options.go | 0 .../commands.workflow_reset_update_options_test.go | 0 .../temporalcli}/commands.workflow_test.go | 2 +- .../temporalcli}/commands.workflow_trace.go | 4 ++-- .../temporalcli}/commands.workflow_trace_test.go | 0 .../temporalcli}/commands.workflow_view.go | 2 +- .../temporalcli}/commands.workflow_view_test.go | 5 ++--- .../temporalcli}/commands_test.go | 6 ++---- {temporalcli => internal/temporalcli}/duration.go | 0 {temporalcli => internal/temporalcli}/payload.go | 0 .../temporalcli}/sqlite_test.go | 5 +++-- {temporalcli => internal/temporalcli}/strings.go | 0 {temporalcli => internal/temporalcli}/timestamp.go | 0 .../tracer/execution_icons.go | 0 .../tracer/execution_state.go | 0 .../tracer/execution_state_test.go | 0 .../internal => internal}/tracer/execution_test.go | 0 .../tracer/execution_tmpls.go | 0 .../internal => internal}/tracer/tail_buffer.go | 0 .../tracer/tail_buffer_test.go | 0 .../tracer/templates/activity.tmpl | 0 .../tracer/templates/common.tmpl | 0 .../tracer/templates/timer.tmpl | 0 .../tracer/templates/workflow.tmpl | 0 .../internal => internal}/tracer/term_size_unix.go | 0 .../tracer/term_size_windows.go | 0 .../internal => internal}/tracer/term_writer.go | 0 .../tracer/term_writer_test.go | 0 .../tracer/workflow_execution_update.go | 0 .../tracer/workflow_execution_update_test.go | 0 .../tracer/workflow_state_worker.go | 0 .../tracer/workflow_tracer.go | 0 93 files changed, 74 insertions(+), 77 deletions(-) rename {temporalcli/internal => internal}/cmd/gen-commands/main.go (76%) rename {temporalcli/internal => internal}/cmd/gen-docs/main.go (77%) rename {temporalcli => internal}/commandsgen/code.go (100%) rename {temporalcli => internal}/commandsgen/commands.yml (100%) rename {temporalcli => internal}/commandsgen/docs.go (98%) rename {temporalcli => internal}/commandsgen/parse.go (99%) rename {temporalcli => internal}/devserver/freeport.go (100%) rename {temporalcli => internal}/devserver/freeport_test.go (97%) rename {temporalcli => internal}/devserver/log.go (100%) rename {temporalcli => internal}/devserver/server.go (100%) rename {temporalcli/internal => internal}/printer/printer.go (100%) rename {temporalcli/internal => internal}/printer/printer_test.go (98%) rename {temporalcli/internal => internal}/printer/test/main.go (90%) rename {temporalcli => internal/temporalcli}/client.go (100%) rename {temporalcli => internal/temporalcli}/commands.activity.go (99%) rename {temporalcli => internal/temporalcli}/commands.activity_test.go (99%) rename {temporalcli => internal/temporalcli}/commands.batch.go (98%) rename {temporalcli => internal/temporalcli}/commands.batch_test.go (100%) rename {temporalcli => internal/temporalcli}/commands.config.go (99%) rename {temporalcli => internal/temporalcli}/commands.config_test.go (100%) rename {temporalcli => internal/temporalcli}/commands.env.go (98%) rename {temporalcli => internal/temporalcli}/commands.env_test.go (100%) rename {temporalcli => internal/temporalcli}/commands.gen.go (100%) rename {temporalcli => internal/temporalcli}/commands.go (99%) rename {temporalcli => internal/temporalcli}/commands.operator_cluster.go (99%) rename {temporalcli => internal/temporalcli}/commands.operator_cluster_test.go (98%) rename {temporalcli => internal/temporalcli}/commands.operator_namespace.go (99%) rename {temporalcli => internal/temporalcli}/commands.operator_namespace_test.go (99%) rename {temporalcli => internal/temporalcli}/commands.operator_nexus.go (99%) rename {temporalcli => internal/temporalcli}/commands.operator_nexus_test.go (99%) rename {temporalcli => internal/temporalcli}/commands.operator_search_attribute.go (98%) rename {temporalcli => internal/temporalcli}/commands.operator_search_attribute_test.go (98%) rename {temporalcli => internal/temporalcli}/commands.schedule.go (99%) rename {temporalcli => internal/temporalcli}/commands.schedule_test.go (99%) rename {temporalcli => internal/temporalcli}/commands.server.go (99%) rename {temporalcli => internal/temporalcli}/commands.server_test.go (99%) rename {temporalcli => internal/temporalcli}/commands.taskqueue.go (99%) rename {temporalcli => internal/temporalcli}/commands.taskqueue.helper.go (97%) rename {temporalcli => internal/temporalcli}/commands.taskqueue_build_id_test.go (100%) rename {temporalcli => internal/temporalcli}/commands.taskqueue_config.go (98%) rename {temporalcli => internal/temporalcli}/commands.taskqueue_config_test.go (100%) rename {temporalcli => internal/temporalcli}/commands.taskqueue_get_build_id.go (98%) rename {temporalcli => internal/temporalcli}/commands.taskqueue_test.go (99%) rename {temporalcli => internal/temporalcli}/commands.taskqueue_update_build_ids.go (100%) rename {temporalcli => internal/temporalcli}/commands.taskqueue_versioning_rules.go (99%) rename {temporalcli => internal/temporalcli}/commands.taskqueue_versioning_rules_test.go (100%) rename {temporalcli => internal/temporalcli}/commands.worker.deployment.go (99%) rename {temporalcli => internal/temporalcli}/commands.worker.deployment_test.go (100%) rename {temporalcli => internal/temporalcli}/commands.worker.go (99%) rename {temporalcli => internal/temporalcli}/commands.worker_test.go (100%) rename {temporalcli => internal/temporalcli}/commands.workflow.go (99%) rename {temporalcli => internal/temporalcli}/commands.workflow_exec.go (99%) rename {temporalcli => internal/temporalcli}/commands.workflow_exec_test.go (100%) rename {temporalcli => internal/temporalcli}/commands.workflow_fix.go (100%) rename {temporalcli => internal/temporalcli}/commands.workflow_reset.go (99%) rename {temporalcli => internal/temporalcli}/commands.workflow_reset_test.go (100%) rename {temporalcli => internal/temporalcli}/commands.workflow_reset_update_options.go (100%) rename {temporalcli => internal/temporalcli}/commands.workflow_reset_update_options_test.go (100%) rename {temporalcli => internal/temporalcli}/commands.workflow_test.go (99%) rename {temporalcli => internal/temporalcli}/commands.workflow_trace.go (97%) rename {temporalcli => internal/temporalcli}/commands.workflow_trace_test.go (100%) rename {temporalcli => internal/temporalcli}/commands.workflow_view.go (99%) rename {temporalcli => internal/temporalcli}/commands.workflow_view_test.go (99%) rename {temporalcli => internal/temporalcli}/commands_test.go (99%) rename {temporalcli => internal/temporalcli}/duration.go (100%) rename {temporalcli => internal/temporalcli}/payload.go (100%) rename {temporalcli => internal/temporalcli}/sqlite_test.go (92%) rename {temporalcli => internal/temporalcli}/strings.go (100%) rename {temporalcli => internal/temporalcli}/timestamp.go (100%) rename {temporalcli/internal => internal}/tracer/execution_icons.go (100%) rename {temporalcli/internal => internal}/tracer/execution_state.go (100%) rename {temporalcli/internal => internal}/tracer/execution_state_test.go (100%) rename {temporalcli/internal => internal}/tracer/execution_test.go (100%) rename {temporalcli/internal => internal}/tracer/execution_tmpls.go (100%) rename {temporalcli/internal => internal}/tracer/tail_buffer.go (100%) rename {temporalcli/internal => internal}/tracer/tail_buffer_test.go (100%) rename {temporalcli/internal => internal}/tracer/templates/activity.tmpl (100%) rename {temporalcli/internal => internal}/tracer/templates/common.tmpl (100%) rename {temporalcli/internal => internal}/tracer/templates/timer.tmpl (100%) rename {temporalcli/internal => internal}/tracer/templates/workflow.tmpl (100%) rename {temporalcli/internal => internal}/tracer/term_size_unix.go (100%) rename {temporalcli/internal => internal}/tracer/term_size_windows.go (100%) rename {temporalcli/internal => internal}/tracer/term_writer.go (100%) rename {temporalcli/internal => internal}/tracer/term_writer_test.go (100%) rename {temporalcli/internal => internal}/tracer/workflow_execution_update.go (100%) rename {temporalcli/internal => internal}/tracer/workflow_execution_update_test.go (100%) rename {temporalcli/internal => internal}/tracer/workflow_state_worker.go (100%) rename {temporalcli/internal => internal}/tracer/workflow_tracer.go (100%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bf2750297..c3678144d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,7 +31,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version-file: 'go.mod' + go-version-file: go.mod - name: Install gotestsum run: go install gotest.tools/gotestsum@latest @@ -42,7 +42,7 @@ jobs: - name: Test run: gotestsum --junitfile junit-xml/${{matrix.os}}.xml -- ./... - - name: 'Upload junit-xml artifacts' + - name: Upload junit-xml artifacts uses: actions/upload-artifact@v4 if: always() with: @@ -53,7 +53,7 @@ jobs: - name: Regen code, confirm unchanged if: ${{ matrix.checkGenCodeTarget }} run: | - go run ./temporalcli/internal/cmd/gen-commands + go run ./internal/cmd/gen-commands git diff --exit-code - name: Test cloud mTLS diff --git a/.goreleaser.yml b/.goreleaser.yml index caa2005d1..9c94e1b4f 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -38,7 +38,7 @@ builds: dir: cmd/temporal binary: temporal ldflags: - - -s -w -X github.com/temporalio/cli/temporalcli.Version={{.Version}} + - -s -w -X github.com/temporalio/cli/internal.Version={{.Version}} goarch: - amd64 - arm64 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 122c7140f..ed8a3e303 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,10 +20,10 @@ Example to run a single test case: ## Adding/updating commands -First, update [commands.yml](temporalcli/commandsgen/commands.yml) following the rules in that file. Then to regenerate the -[commands.gen.go](temporalcli/commands.gen.go) file from code, simply run: +First, update [commands.yml](internal/commandsgen/commands.yml) following the rules in that file. Then to regenerate the +[commands.gen.go](internal/commands.gen.go) file from code, simply run: - go run ./temporalcli/internal/cmd/gen-commands + go run ./internal/cmd/gen-commands This will expect every non-parent command to have a `run` method, so for new commands developers will have to implement `run` on the new command in a separate file before it will compile. @@ -31,21 +31,21 @@ This will expect every non-parent command to have a `run` method, so for new com Once a command is updated, the CI will automatically generate new docs and create a PR in the Documentation repo with the corresponding updates. To generate these docs locally, run: - go run ./temporalcli/internal/cmd/gen-docs + go run ./internal/cmd/gen-docs -This will auto-generate a new set of docs to `temporalcli/docs/`. If a new root command is added, a new file will be automatically generated, like `temporal activity` and `activity.mdx`. +This will auto-generate a new set of docs to `dist/docs/`. If a new root command is added, a new file will be automatically generated, like `temporal activity` and `activity.mdx`. ## Inject additional build-time information To add build-time information to the version string printed by the binary, use - go build -ldflags "-X github.com/temporalio/cli/temporalcli.buildInfo=" + go build -ldflags "-X github.com/temporalio/cli/internal.buildInfo=" This can be useful if, for example, you've used a `replace` statement in go.mod pointing to a local directory. Note that inclusion of space characters in the value supplied via `-ldflags` is tricky. Here's an example that adds branch info from a local repo to the version string, and includes a space character: - go build -ldflags "-X 'github.com/temporalio/cli/temporalcli.buildInfo=ServerBranch $(git -C ../temporal rev-parse --abbrev-ref HEAD)'" -o temporal ./cmd/temporal/main.go + go build -ldflags "-X 'github.com/temporalio/cli/internal.buildInfo=ServerBranch $(git -C ../temporal rev-parse --abbrev-ref HEAD)'" -o temporal ./cmd/temporal/main.go ## Building Docker image diff --git a/Makefile b/Makefile index 90a87893f..448ee5246 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ -.PHONY: all gen build +.PHONY: all gen build fmt-imports all: gen build -gen: temporalcli/commands.gen.go +gen: internal/commands.gen.go -temporalcli/commands.gen.go: temporalcli/commandsgen/commands.yml - go run ./temporalcli/internal/cmd/gen-commands +internal/commands.gen.go: internal/commandsgen/commands.yml + go run ./internal/cmd/gen-commands build: go build ./cmd/temporal diff --git a/cmd/temporal/main.go b/cmd/temporal/main.go index 82203bf0c..0c482a6ed 100644 --- a/cmd/temporal/main.go +++ b/cmd/temporal/main.go @@ -3,7 +3,7 @@ package main import ( "context" - "github.com/temporalio/cli/temporalcli" + "github.com/temporalio/cli/internal/temporalcli" // Prevent the pinned version of sqlite driver from unintentionally changing // until https://gitlab.com/cznic/sqlite/-/issues/196 is resolved. diff --git a/temporalcli/internal/cmd/gen-commands/main.go b/internal/cmd/gen-commands/main.go similarity index 76% rename from temporalcli/internal/cmd/gen-commands/main.go rename to internal/cmd/gen-commands/main.go index 817ff2968..21d8e5529 100644 --- a/temporalcli/internal/cmd/gen-commands/main.go +++ b/internal/cmd/gen-commands/main.go @@ -7,7 +7,7 @@ import ( "path/filepath" "runtime" - "github.com/temporalio/cli/temporalcli/commandsgen" + "github.com/temporalio/cli/internal/commandsgen" ) func main() { @@ -19,12 +19,13 @@ func main() { func run() error { // Get commands dir _, file, _, _ := runtime.Caller(0) - commandsDir := filepath.Join(file, "../../../../") + genCommandsDir := filepath.Dir(file) + commandsDir := filepath.Join(genCommandsDir, "../../") // Parse YAML cmds, err := commandsgen.ParseCommands() if err != nil { - return fmt.Errorf("failed parsing markdown: %w", err) + return fmt.Errorf("failed parsing YAML: %w", err) } // Generate code diff --git a/temporalcli/internal/cmd/gen-docs/main.go b/internal/cmd/gen-docs/main.go similarity index 77% rename from temporalcli/internal/cmd/gen-docs/main.go rename to internal/cmd/gen-docs/main.go index 94ff20ba4..0c6e0b942 100644 --- a/temporalcli/internal/cmd/gen-docs/main.go +++ b/internal/cmd/gen-docs/main.go @@ -7,7 +7,7 @@ import ( "path/filepath" "runtime" - "github.com/temporalio/cli/temporalcli/commandsgen" + "github.com/temporalio/cli/internal/commandsgen" ) func main() { @@ -19,17 +19,18 @@ func main() { func run() error { // Get commands dir _, file, _, _ := runtime.Caller(0) - docsDir := filepath.Join(file, "../../../../docs/") + genDocsDir := filepath.Dir(file) + docsDir := filepath.Join(genDocsDir, "../../../dist/docs/") err := os.MkdirAll(docsDir, os.ModePerm) if err != nil { log.Fatalf("Error creating directory: %v", err) } - // Parse markdown + // Parse YAML cmds, err := commandsgen.ParseCommands() if err != nil { - return fmt.Errorf("failed parsing markdown: %w", err) + return fmt.Errorf("failed parsing YAML: %w", err) } // Generate docs diff --git a/temporalcli/commandsgen/code.go b/internal/commandsgen/code.go similarity index 100% rename from temporalcli/commandsgen/code.go rename to internal/commandsgen/code.go diff --git a/temporalcli/commandsgen/commands.yml b/internal/commandsgen/commands.yml similarity index 100% rename from temporalcli/commandsgen/commands.yml rename to internal/commandsgen/commands.yml diff --git a/temporalcli/commandsgen/docs.go b/internal/commandsgen/docs.go similarity index 98% rename from temporalcli/commandsgen/docs.go rename to internal/commandsgen/docs.go index 2cd912a94..089a6120e 100644 --- a/temporalcli/commandsgen/docs.go +++ b/internal/commandsgen/docs.go @@ -76,7 +76,7 @@ func (w *docWriter) writeCommand(c *Command) { w.fileMap[fileName].WriteString("---") w.fileMap[fileName].WriteString("\n\n") w.fileMap[fileName].WriteString("{/* NOTE: This is an auto-generated file. Any edit to this file will be overwritten.\n") - w.fileMap[fileName].WriteString("This file is generated from https://github.com/temporalio/cli/blob/main/temporalcli/commandsgen/commands.yml */}\n") + w.fileMap[fileName].WriteString("This file is generated from https://github.com/temporalio/cli/blob/main/internal/commandsgen/commands.yml via internal/cmd/gen-docs */}\n") } func (w *docWriter) writeSubcommand(c *Command) { diff --git a/temporalcli/commandsgen/parse.go b/internal/commandsgen/parse.go similarity index 99% rename from temporalcli/commandsgen/parse.go rename to internal/commandsgen/parse.go index 7945e1c7e..4fab512a6 100644 --- a/temporalcli/commandsgen/parse.go +++ b/internal/commandsgen/parse.go @@ -1,5 +1,5 @@ // Package commandsgen is built to read the YAML format described in -// temporalcli/commandsgen/commands.yml and generate code from it. +// internal/commandsgen/commands.yml and generate code from it. package commandsgen import ( diff --git a/temporalcli/devserver/freeport.go b/internal/devserver/freeport.go similarity index 100% rename from temporalcli/devserver/freeport.go rename to internal/devserver/freeport.go diff --git a/temporalcli/devserver/freeport_test.go b/internal/devserver/freeport_test.go similarity index 97% rename from temporalcli/devserver/freeport_test.go rename to internal/devserver/freeport_test.go index b6cf57703..5839cef65 100644 --- a/temporalcli/devserver/freeport_test.go +++ b/internal/devserver/freeport_test.go @@ -5,7 +5,7 @@ import ( "net" "testing" - "github.com/temporalio/cli/temporalcli/devserver" + "github.com/temporalio/cli/internal/devserver" ) func TestFreePort_NoDouble(t *testing.T) { diff --git a/temporalcli/devserver/log.go b/internal/devserver/log.go similarity index 100% rename from temporalcli/devserver/log.go rename to internal/devserver/log.go diff --git a/temporalcli/devserver/server.go b/internal/devserver/server.go similarity index 100% rename from temporalcli/devserver/server.go rename to internal/devserver/server.go diff --git a/temporalcli/internal/printer/printer.go b/internal/printer/printer.go similarity index 100% rename from temporalcli/internal/printer/printer.go rename to internal/printer/printer.go diff --git a/temporalcli/internal/printer/printer_test.go b/internal/printer/printer_test.go similarity index 98% rename from temporalcli/internal/printer/printer_test.go rename to internal/printer/printer_test.go index 8965ac60b..054332043 100644 --- a/temporalcli/internal/printer/printer_test.go +++ b/internal/printer/printer_test.go @@ -10,7 +10,7 @@ import ( "unicode" "github.com/stretchr/testify/require" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" ) // TODO(cretz): Test: diff --git a/temporalcli/internal/printer/test/main.go b/internal/printer/test/main.go similarity index 90% rename from temporalcli/internal/printer/test/main.go rename to internal/printer/test/main.go index 80bce277b..48615483c 100644 --- a/temporalcli/internal/printer/test/main.go +++ b/internal/printer/test/main.go @@ -3,7 +3,7 @@ package main import ( "os" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" ) // This main function is used to test that the printer package don't panic if diff --git a/temporalcli/client.go b/internal/temporalcli/client.go similarity index 100% rename from temporalcli/client.go rename to internal/temporalcli/client.go diff --git a/temporalcli/commands.activity.go b/internal/temporalcli/commands.activity.go similarity index 99% rename from temporalcli/commands.activity.go rename to internal/temporalcli/commands.activity.go index 0d297a4a2..503a5e1e1 100644 --- a/temporalcli/commands.activity.go +++ b/internal/temporalcli/commands.activity.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" activitypb "go.temporal.io/api/activity/v1" "go.temporal.io/api/batch/v1" "go.temporal.io/api/common/v1" diff --git a/temporalcli/commands.activity_test.go b/internal/temporalcli/commands.activity_test.go similarity index 99% rename from temporalcli/commands.activity_test.go rename to internal/temporalcli/commands.activity_test.go index 6ce214472..28ff7a13b 100644 --- a/temporalcli/commands.activity_test.go +++ b/internal/temporalcli/commands.activity_test.go @@ -436,7 +436,6 @@ func (s *SharedServerSuite) TestUnpauseActivity_BatchSuccess() { failActivity.Store(false) } - func (s *SharedServerSuite) TestResetActivity_BatchSuccess() { var failActivity atomic.Bool failActivity.Store(true) diff --git a/temporalcli/commands.batch.go b/internal/temporalcli/commands.batch.go similarity index 98% rename from temporalcli/commands.batch.go rename to internal/temporalcli/commands.batch.go index 93686732c..c96990143 100644 --- a/temporalcli/commands.batch.go +++ b/internal/temporalcli/commands.batch.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/sdk/client" diff --git a/temporalcli/commands.batch_test.go b/internal/temporalcli/commands.batch_test.go similarity index 100% rename from temporalcli/commands.batch_test.go rename to internal/temporalcli/commands.batch_test.go diff --git a/temporalcli/commands.config.go b/internal/temporalcli/commands.config.go similarity index 99% rename from temporalcli/commands.config.go rename to internal/temporalcli/commands.config.go index a4028ff46..79467328d 100644 --- a/temporalcli/commands.config.go +++ b/internal/temporalcli/commands.config.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/BurntSushi/toml" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/sdk/contrib/envconfig" ) diff --git a/temporalcli/commands.config_test.go b/internal/temporalcli/commands.config_test.go similarity index 100% rename from temporalcli/commands.config_test.go rename to internal/temporalcli/commands.config_test.go diff --git a/temporalcli/commands.env.go b/internal/temporalcli/commands.env.go similarity index 98% rename from temporalcli/commands.env.go rename to internal/temporalcli/commands.env.go index c39921689..d98c06f04 100644 --- a/temporalcli/commands.env.go +++ b/internal/temporalcli/commands.env.go @@ -7,7 +7,7 @@ import ( "sort" "strings" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "gopkg.in/yaml.v3" ) diff --git a/temporalcli/commands.env_test.go b/internal/temporalcli/commands.env_test.go similarity index 100% rename from temporalcli/commands.env_test.go rename to internal/temporalcli/commands.env_test.go diff --git a/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go similarity index 100% rename from temporalcli/commands.gen.go rename to internal/temporalcli/commands.gen.go diff --git a/temporalcli/commands.go b/internal/temporalcli/commands.go similarity index 99% rename from temporalcli/commands.go rename to internal/temporalcli/commands.go index 01f956e58..135cb113f 100644 --- a/temporalcli/commands.go +++ b/internal/temporalcli/commands.go @@ -19,7 +19,7 @@ import ( "github.com/fatih/color" "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "github.com/temporalio/ui-server/v2/server/version" "go.temporal.io/api/common/v1" commonpb "go.temporal.io/api/common/v1" @@ -477,7 +477,7 @@ var buildInfo string func VersionString() string { // To add build-time information to the version string, use - // go build -ldflags "-X github.com/temporalio/cli/temporalcli.buildInfo=" + // go build -ldflags "-X github.com/temporalio/cli/internal.buildInfo=" var bi = buildInfo if bi != "" { bi = fmt.Sprintf(", %s", bi) diff --git a/temporalcli/commands.operator_cluster.go b/internal/temporalcli/commands.operator_cluster.go similarity index 99% rename from temporalcli/commands.operator_cluster.go rename to internal/temporalcli/commands.operator_cluster.go index 36539593e..6d6e6bd25 100644 --- a/temporalcli/commands.operator_cluster.go +++ b/internal/temporalcli/commands.operator_cluster.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/operatorservice/v1" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/sdk/client" diff --git a/temporalcli/commands.operator_cluster_test.go b/internal/temporalcli/commands.operator_cluster_test.go similarity index 98% rename from temporalcli/commands.operator_cluster_test.go rename to internal/temporalcli/commands.operator_cluster_test.go index 4cb3b337c..0e637e23e 100644 --- a/temporalcli/commands.operator_cluster_test.go +++ b/internal/temporalcli/commands.operator_cluster_test.go @@ -6,8 +6,8 @@ import ( "strconv" "time" - "github.com/temporalio/cli/temporalcli" - "github.com/temporalio/cli/temporalcli/devserver" + "github.com/temporalio/cli/internal/devserver" + "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/workflowservice/v1" ) diff --git a/temporalcli/commands.operator_namespace.go b/internal/temporalcli/commands.operator_namespace.go similarity index 99% rename from temporalcli/commands.operator_namespace.go rename to internal/temporalcli/commands.operator_namespace.go index 47a5830b7..a46b851d2 100644 --- a/temporalcli/commands.operator_namespace.go +++ b/internal/temporalcli/commands.operator_namespace.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/enums/v1" "go.temporal.io/api/namespace/v1" "go.temporal.io/api/operatorservice/v1" diff --git a/temporalcli/commands.operator_namespace_test.go b/internal/temporalcli/commands.operator_namespace_test.go similarity index 99% rename from temporalcli/commands.operator_namespace_test.go rename to internal/temporalcli/commands.operator_namespace_test.go index 65b46cf1e..6effed480 100644 --- a/temporalcli/commands.operator_namespace_test.go +++ b/internal/temporalcli/commands.operator_namespace_test.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - "github.com/temporalio/cli/temporalcli" + "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/enums/v1" "go.temporal.io/api/workflowservice/v1" ) diff --git a/temporalcli/commands.operator_nexus.go b/internal/temporalcli/commands.operator_nexus.go similarity index 99% rename from temporalcli/commands.operator_nexus.go rename to internal/temporalcli/commands.operator_nexus.go index aeef936fa..03e337e6f 100644 --- a/temporalcli/commands.operator_nexus.go +++ b/internal/temporalcli/commands.operator_nexus.go @@ -7,7 +7,7 @@ import ( "os" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" commonpb "go.temporal.io/api/common/v1" nexuspb "go.temporal.io/api/nexus/v1" "go.temporal.io/api/operatorservice/v1" diff --git a/temporalcli/commands.operator_nexus_test.go b/internal/temporalcli/commands.operator_nexus_test.go similarity index 99% rename from temporalcli/commands.operator_nexus_test.go rename to internal/temporalcli/commands.operator_nexus_test.go index 16bcfbd28..2cc31cf19 100644 --- a/temporalcli/commands.operator_nexus_test.go +++ b/internal/temporalcli/commands.operator_nexus_test.go @@ -11,7 +11,7 @@ import ( "time" "github.com/stretchr/testify/require" - "github.com/temporalio/cli/temporalcli" + "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/nexus/v1" "go.temporal.io/api/operatorservice/v1" ) diff --git a/temporalcli/commands.operator_search_attribute.go b/internal/temporalcli/commands.operator_search_attribute.go similarity index 98% rename from temporalcli/commands.operator_search_attribute.go rename to internal/temporalcli/commands.operator_search_attribute.go index afdca478c..d1187be20 100644 --- a/temporalcli/commands.operator_search_attribute.go +++ b/internal/temporalcli/commands.operator_search_attribute.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/enums/v1" "go.temporal.io/api/operatorservice/v1" ) diff --git a/temporalcli/commands.operator_search_attribute_test.go b/internal/temporalcli/commands.operator_search_attribute_test.go similarity index 98% rename from temporalcli/commands.operator_search_attribute_test.go rename to internal/temporalcli/commands.operator_search_attribute_test.go index efed38b61..eba4ee81b 100644 --- a/temporalcli/commands.operator_search_attribute_test.go +++ b/internal/temporalcli/commands.operator_search_attribute_test.go @@ -1,7 +1,7 @@ package temporalcli_test import ( - "github.com/temporalio/cli/temporalcli" + "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/enums/v1" "go.temporal.io/api/operatorservice/v1" ) diff --git a/temporalcli/commands.schedule.go b/internal/temporalcli/commands.schedule.go similarity index 99% rename from temporalcli/commands.schedule.go rename to internal/temporalcli/commands.schedule.go index 7cf300e88..382e48346 100644 --- a/temporalcli/commands.schedule.go +++ b/internal/temporalcli/commands.schedule.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "google.golang.org/protobuf/encoding/protojson" commonpb "go.temporal.io/api/common/v1" diff --git a/temporalcli/commands.schedule_test.go b/internal/temporalcli/commands.schedule_test.go similarity index 99% rename from temporalcli/commands.schedule_test.go rename to internal/temporalcli/commands.schedule_test.go index d24dc02c2..993fb3f2b 100644 --- a/temporalcli/commands.schedule_test.go +++ b/internal/temporalcli/commands.schedule_test.go @@ -12,8 +12,7 @@ import ( "time" "github.com/stretchr/testify/assert" - "github.com/temporalio/cli/temporalcli" - + "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/enums/v1" "go.temporal.io/api/operatorservice/v1" "go.temporal.io/sdk/workflow" diff --git a/temporalcli/commands.server.go b/internal/temporalcli/commands.server.go similarity index 99% rename from temporalcli/commands.server.go rename to internal/temporalcli/commands.server.go index 5b2ffc253..91af9d801 100644 --- a/temporalcli/commands.server.go +++ b/internal/temporalcli/commands.server.go @@ -6,8 +6,9 @@ import ( "strings" "github.com/google/uuid" - "github.com/temporalio/cli/temporalcli/devserver" "go.temporal.io/api/enums/v1" + + "github.com/temporalio/cli/internal/devserver" ) var defaultDynamicConfigValues = map[string]any{ diff --git a/temporalcli/commands.server_test.go b/internal/temporalcli/commands.server_test.go similarity index 99% rename from temporalcli/commands.server_test.go rename to internal/temporalcli/commands.server_test.go index e1b4586d8..8c25558d9 100644 --- a/temporalcli/commands.server_test.go +++ b/internal/temporalcli/commands.server_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/temporalio/cli/temporalcli/devserver" + "github.com/temporalio/cli/internal/devserver" "go.temporal.io/api/operatorservice/v1" "go.temporal.io/sdk/client" "go.temporal.io/sdk/temporal" diff --git a/temporalcli/commands.taskqueue.go b/internal/temporalcli/commands.taskqueue.go similarity index 99% rename from temporalcli/commands.taskqueue.go rename to internal/temporalcli/commands.taskqueue.go index a769190a4..a11cc1793 100644 --- a/temporalcli/commands.taskqueue.go +++ b/internal/temporalcli/commands.taskqueue.go @@ -5,7 +5,7 @@ import ( "time" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" commonpb "go.temporal.io/api/common/v1" "go.temporal.io/api/enums/v1" "go.temporal.io/api/taskqueue/v1" diff --git a/temporalcli/commands.taskqueue.helper.go b/internal/temporalcli/commands.taskqueue.helper.go similarity index 97% rename from temporalcli/commands.taskqueue.helper.go rename to internal/temporalcli/commands.taskqueue.helper.go index 591531c3d..f63e8b22b 100644 --- a/temporalcli/commands.taskqueue.helper.go +++ b/internal/temporalcli/commands.taskqueue.helper.go @@ -5,7 +5,7 @@ import ( "time" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/enums/v1" "go.temporal.io/api/taskqueue/v1" ) diff --git a/temporalcli/commands.taskqueue_build_id_test.go b/internal/temporalcli/commands.taskqueue_build_id_test.go similarity index 100% rename from temporalcli/commands.taskqueue_build_id_test.go rename to internal/temporalcli/commands.taskqueue_build_id_test.go diff --git a/temporalcli/commands.taskqueue_config.go b/internal/temporalcli/commands.taskqueue_config.go similarity index 98% rename from temporalcli/commands.taskqueue_config.go rename to internal/temporalcli/commands.taskqueue_config.go index a9de915ef..bd2948ca7 100644 --- a/temporalcli/commands.taskqueue_config.go +++ b/internal/temporalcli/commands.taskqueue_config.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" enums "go.temporal.io/api/enums/v1" "go.temporal.io/api/taskqueue/v1" "go.temporal.io/api/workflowservice/v1" diff --git a/temporalcli/commands.taskqueue_config_test.go b/internal/temporalcli/commands.taskqueue_config_test.go similarity index 100% rename from temporalcli/commands.taskqueue_config_test.go rename to internal/temporalcli/commands.taskqueue_config_test.go diff --git a/temporalcli/commands.taskqueue_get_build_id.go b/internal/temporalcli/commands.taskqueue_get_build_id.go similarity index 98% rename from temporalcli/commands.taskqueue_get_build_id.go rename to internal/temporalcli/commands.taskqueue_get_build_id.go index ad51ae177..56cc521b0 100644 --- a/temporalcli/commands.taskqueue_get_build_id.go +++ b/internal/temporalcli/commands.taskqueue_get_build_id.go @@ -5,7 +5,7 @@ import ( "strconv" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/sdk/client" ) diff --git a/temporalcli/commands.taskqueue_test.go b/internal/temporalcli/commands.taskqueue_test.go similarity index 99% rename from temporalcli/commands.taskqueue_test.go rename to internal/temporalcli/commands.taskqueue_test.go index 346df180d..b695758b2 100644 --- a/temporalcli/commands.taskqueue_test.go +++ b/internal/temporalcli/commands.taskqueue_test.go @@ -5,13 +5,12 @@ import ( "strings" "time" - "github.com/stretchr/testify/assert" - "go.temporal.io/sdk/workflow" - "github.com/google/uuid" - "github.com/temporalio/cli/temporalcli" + "github.com/stretchr/testify/assert" + "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/enums/v1" "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/sdk/workflow" ) type statsRowType struct { diff --git a/temporalcli/commands.taskqueue_update_build_ids.go b/internal/temporalcli/commands.taskqueue_update_build_ids.go similarity index 100% rename from temporalcli/commands.taskqueue_update_build_ids.go rename to internal/temporalcli/commands.taskqueue_update_build_ids.go diff --git a/temporalcli/commands.taskqueue_versioning_rules.go b/internal/temporalcli/commands.taskqueue_versioning_rules.go similarity index 99% rename from temporalcli/commands.taskqueue_versioning_rules.go rename to internal/temporalcli/commands.taskqueue_versioning_rules.go index d64095e8b..4552492fc 100644 --- a/temporalcli/commands.taskqueue_versioning_rules.go +++ b/internal/temporalcli/commands.taskqueue_versioning_rules.go @@ -5,7 +5,7 @@ import ( "time" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/sdk/client" ) diff --git a/temporalcli/commands.taskqueue_versioning_rules_test.go b/internal/temporalcli/commands.taskqueue_versioning_rules_test.go similarity index 100% rename from temporalcli/commands.taskqueue_versioning_rules_test.go rename to internal/temporalcli/commands.taskqueue_versioning_rules_test.go diff --git a/temporalcli/commands.worker.deployment.go b/internal/temporalcli/commands.worker.deployment.go similarity index 99% rename from temporalcli/commands.worker.deployment.go rename to internal/temporalcli/commands.worker.deployment.go index a742cbd9e..532b390dd 100644 --- a/temporalcli/commands.worker.deployment.go +++ b/internal/temporalcli/commands.worker.deployment.go @@ -6,7 +6,7 @@ import ( "time" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/common/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/sdk/client" diff --git a/temporalcli/commands.worker.deployment_test.go b/internal/temporalcli/commands.worker.deployment_test.go similarity index 100% rename from temporalcli/commands.worker.deployment_test.go rename to internal/temporalcli/commands.worker.deployment_test.go diff --git a/temporalcli/commands.worker.go b/internal/temporalcli/commands.worker.go similarity index 99% rename from temporalcli/commands.worker.go rename to internal/temporalcli/commands.worker.go index 29eb399fb..44b5e31e9 100644 --- a/temporalcli/commands.worker.go +++ b/internal/temporalcli/commands.worker.go @@ -5,7 +5,7 @@ import ( "strings" "time" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" deploymentpb "go.temporal.io/api/deployment/v1" enumspb "go.temporal.io/api/enums/v1" workerpb "go.temporal.io/api/worker/v1" diff --git a/temporalcli/commands.worker_test.go b/internal/temporalcli/commands.worker_test.go similarity index 100% rename from temporalcli/commands.worker_test.go rename to internal/temporalcli/commands.worker_test.go diff --git a/temporalcli/commands.workflow.go b/internal/temporalcli/commands.workflow.go similarity index 99% rename from temporalcli/commands.workflow.go rename to internal/temporalcli/commands.workflow.go index 4daf81b91..975be9cec 100644 --- a/temporalcli/commands.workflow.go +++ b/internal/temporalcli/commands.workflow.go @@ -12,7 +12,7 @@ import ( "github.com/fatih/color" "github.com/google/uuid" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/batch/v1" "go.temporal.io/api/common/v1" deploymentpb "go.temporal.io/api/deployment/v1" diff --git a/temporalcli/commands.workflow_exec.go b/internal/temporalcli/commands.workflow_exec.go similarity index 99% rename from temporalcli/commands.workflow_exec.go rename to internal/temporalcli/commands.workflow_exec.go index fce190b25..94dbec320 100644 --- a/temporalcli/commands.workflow_exec.go +++ b/internal/temporalcli/commands.workflow_exec.go @@ -14,7 +14,7 @@ import ( "github.com/fatih/color" "github.com/google/uuid" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/common/v1" commonpb "go.temporal.io/api/common/v1" "go.temporal.io/api/enums/v1" diff --git a/temporalcli/commands.workflow_exec_test.go b/internal/temporalcli/commands.workflow_exec_test.go similarity index 100% rename from temporalcli/commands.workflow_exec_test.go rename to internal/temporalcli/commands.workflow_exec_test.go diff --git a/temporalcli/commands.workflow_fix.go b/internal/temporalcli/commands.workflow_fix.go similarity index 100% rename from temporalcli/commands.workflow_fix.go rename to internal/temporalcli/commands.workflow_fix.go diff --git a/temporalcli/commands.workflow_reset.go b/internal/temporalcli/commands.workflow_reset.go similarity index 99% rename from temporalcli/commands.workflow_reset.go rename to internal/temporalcli/commands.workflow_reset.go index 9a4b6f135..2da4e0633 100644 --- a/temporalcli/commands.workflow_reset.go +++ b/internal/temporalcli/commands.workflow_reset.go @@ -7,14 +7,13 @@ import ( "strings" "github.com/google/uuid" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/batch/v1" "go.temporal.io/api/common/v1" "go.temporal.io/api/enums/v1" workflow "go.temporal.io/api/workflow/v1" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/sdk/client" - - "github.com/temporalio/cli/temporalcli/internal/printer" ) func (c *TemporalWorkflowResetCommand) run(cctx *CommandContext, _ []string) error { diff --git a/temporalcli/commands.workflow_reset_test.go b/internal/temporalcli/commands.workflow_reset_test.go similarity index 100% rename from temporalcli/commands.workflow_reset_test.go rename to internal/temporalcli/commands.workflow_reset_test.go diff --git a/temporalcli/commands.workflow_reset_update_options.go b/internal/temporalcli/commands.workflow_reset_update_options.go similarity index 100% rename from temporalcli/commands.workflow_reset_update_options.go rename to internal/temporalcli/commands.workflow_reset_update_options.go diff --git a/temporalcli/commands.workflow_reset_update_options_test.go b/internal/temporalcli/commands.workflow_reset_update_options_test.go similarity index 100% rename from temporalcli/commands.workflow_reset_update_options_test.go rename to internal/temporalcli/commands.workflow_reset_update_options_test.go diff --git a/temporalcli/commands.workflow_test.go b/internal/temporalcli/commands.workflow_test.go similarity index 99% rename from temporalcli/commands.workflow_test.go rename to internal/temporalcli/commands.workflow_test.go index 91024ed66..44119740f 100644 --- a/temporalcli/commands.workflow_test.go +++ b/internal/temporalcli/commands.workflow_test.go @@ -13,7 +13,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/temporalio/cli/temporalcli" + "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/enums/v1" workflowpb "go.temporal.io/api/workflow/v1" "go.temporal.io/api/workflowservice/v1" diff --git a/temporalcli/commands.workflow_trace.go b/internal/temporalcli/commands.workflow_trace.go similarity index 97% rename from temporalcli/commands.workflow_trace.go rename to internal/temporalcli/commands.workflow_trace.go index d6fc41a70..74062a836 100644 --- a/temporalcli/commands.workflow_trace.go +++ b/internal/temporalcli/commands.workflow_trace.go @@ -7,8 +7,8 @@ import ( "time" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" - "github.com/temporalio/cli/temporalcli/internal/tracer" + "github.com/temporalio/cli/internal/printer" + "github.com/temporalio/cli/internal/tracer" "go.temporal.io/api/enums/v1" "go.temporal.io/sdk/client" ) diff --git a/temporalcli/commands.workflow_trace_test.go b/internal/temporalcli/commands.workflow_trace_test.go similarity index 100% rename from temporalcli/commands.workflow_trace_test.go rename to internal/temporalcli/commands.workflow_trace_test.go diff --git a/temporalcli/commands.workflow_view.go b/internal/temporalcli/commands.workflow_view.go similarity index 99% rename from temporalcli/commands.workflow_view.go rename to internal/temporalcli/commands.workflow_view.go index 896bd76ef..0226350d0 100644 --- a/temporalcli/commands.workflow_view.go +++ b/internal/temporalcli/commands.workflow_view.go @@ -7,7 +7,7 @@ import ( "time" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/common/v1" "go.temporal.io/api/enums/v1" "go.temporal.io/api/failure/v1" diff --git a/temporalcli/commands.workflow_view_test.go b/internal/temporalcli/commands.workflow_view_test.go similarity index 99% rename from temporalcli/commands.workflow_view_test.go rename to internal/temporalcli/commands.workflow_view_test.go index 3ac697209..980d4b632 100644 --- a/temporalcli/commands.workflow_view_test.go +++ b/internal/temporalcli/commands.workflow_view_test.go @@ -9,12 +9,11 @@ import ( "strings" "time" - "go.temporal.io/api/common/v1" - "github.com/google/uuid" "github.com/nexus-rpc/sdk-go/nexus" "github.com/stretchr/testify/assert" - "github.com/temporalio/cli/temporalcli" + "github.com/temporalio/cli/internal/temporalcli" + "go.temporal.io/api/common/v1" "go.temporal.io/api/enums/v1" nexuspb "go.temporal.io/api/nexus/v1" "go.temporal.io/api/operatorservice/v1" diff --git a/temporalcli/commands_test.go b/internal/temporalcli/commands_test.go similarity index 99% rename from temporalcli/commands_test.go rename to internal/temporalcli/commands_test.go index 7680e9e7c..e23eccacc 100644 --- a/temporalcli/commands_test.go +++ b/internal/temporalcli/commands_test.go @@ -18,16 +18,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/temporalio/cli/internal/devserver" + "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/enums/v1" "go.temporal.io/api/operatorservice/v1" "go.temporal.io/sdk/client" "go.temporal.io/sdk/temporal" "go.temporal.io/sdk/worker" "go.temporal.io/sdk/workflow" - - "github.com/temporalio/cli/temporalcli" - "github.com/temporalio/cli/temporalcli/devserver" - "google.golang.org/grpc" ) diff --git a/temporalcli/duration.go b/internal/temporalcli/duration.go similarity index 100% rename from temporalcli/duration.go rename to internal/temporalcli/duration.go diff --git a/temporalcli/payload.go b/internal/temporalcli/payload.go similarity index 100% rename from temporalcli/payload.go rename to internal/temporalcli/payload.go diff --git a/temporalcli/sqlite_test.go b/internal/temporalcli/sqlite_test.go similarity index 92% rename from temporalcli/sqlite_test.go rename to internal/temporalcli/sqlite_test.go index fc95fd952..35e7a916e 100644 --- a/temporalcli/sqlite_test.go +++ b/internal/temporalcli/sqlite_test.go @@ -1,15 +1,16 @@ package temporalcli import ( - _ "modernc.org/sqlite" "os" "strings" "testing" + + _ "modernc.org/sqlite" ) // Pinning modernc.org/sqlite to this version until https://gitlab.com/cznic/sqlite/-/issues/196 is resolved func TestSqliteVersion(t *testing.T) { - content, err := os.ReadFile("../go.mod") + content, err := os.ReadFile("../../go.mod") if err != nil { t.Fatalf("Failed to read go.mod: %v", err) } diff --git a/temporalcli/strings.go b/internal/temporalcli/strings.go similarity index 100% rename from temporalcli/strings.go rename to internal/temporalcli/strings.go diff --git a/temporalcli/timestamp.go b/internal/temporalcli/timestamp.go similarity index 100% rename from temporalcli/timestamp.go rename to internal/temporalcli/timestamp.go diff --git a/temporalcli/internal/tracer/execution_icons.go b/internal/tracer/execution_icons.go similarity index 100% rename from temporalcli/internal/tracer/execution_icons.go rename to internal/tracer/execution_icons.go diff --git a/temporalcli/internal/tracer/execution_state.go b/internal/tracer/execution_state.go similarity index 100% rename from temporalcli/internal/tracer/execution_state.go rename to internal/tracer/execution_state.go diff --git a/temporalcli/internal/tracer/execution_state_test.go b/internal/tracer/execution_state_test.go similarity index 100% rename from temporalcli/internal/tracer/execution_state_test.go rename to internal/tracer/execution_state_test.go diff --git a/temporalcli/internal/tracer/execution_test.go b/internal/tracer/execution_test.go similarity index 100% rename from temporalcli/internal/tracer/execution_test.go rename to internal/tracer/execution_test.go diff --git a/temporalcli/internal/tracer/execution_tmpls.go b/internal/tracer/execution_tmpls.go similarity index 100% rename from temporalcli/internal/tracer/execution_tmpls.go rename to internal/tracer/execution_tmpls.go diff --git a/temporalcli/internal/tracer/tail_buffer.go b/internal/tracer/tail_buffer.go similarity index 100% rename from temporalcli/internal/tracer/tail_buffer.go rename to internal/tracer/tail_buffer.go diff --git a/temporalcli/internal/tracer/tail_buffer_test.go b/internal/tracer/tail_buffer_test.go similarity index 100% rename from temporalcli/internal/tracer/tail_buffer_test.go rename to internal/tracer/tail_buffer_test.go diff --git a/temporalcli/internal/tracer/templates/activity.tmpl b/internal/tracer/templates/activity.tmpl similarity index 100% rename from temporalcli/internal/tracer/templates/activity.tmpl rename to internal/tracer/templates/activity.tmpl diff --git a/temporalcli/internal/tracer/templates/common.tmpl b/internal/tracer/templates/common.tmpl similarity index 100% rename from temporalcli/internal/tracer/templates/common.tmpl rename to internal/tracer/templates/common.tmpl diff --git a/temporalcli/internal/tracer/templates/timer.tmpl b/internal/tracer/templates/timer.tmpl similarity index 100% rename from temporalcli/internal/tracer/templates/timer.tmpl rename to internal/tracer/templates/timer.tmpl diff --git a/temporalcli/internal/tracer/templates/workflow.tmpl b/internal/tracer/templates/workflow.tmpl similarity index 100% rename from temporalcli/internal/tracer/templates/workflow.tmpl rename to internal/tracer/templates/workflow.tmpl diff --git a/temporalcli/internal/tracer/term_size_unix.go b/internal/tracer/term_size_unix.go similarity index 100% rename from temporalcli/internal/tracer/term_size_unix.go rename to internal/tracer/term_size_unix.go diff --git a/temporalcli/internal/tracer/term_size_windows.go b/internal/tracer/term_size_windows.go similarity index 100% rename from temporalcli/internal/tracer/term_size_windows.go rename to internal/tracer/term_size_windows.go diff --git a/temporalcli/internal/tracer/term_writer.go b/internal/tracer/term_writer.go similarity index 100% rename from temporalcli/internal/tracer/term_writer.go rename to internal/tracer/term_writer.go diff --git a/temporalcli/internal/tracer/term_writer_test.go b/internal/tracer/term_writer_test.go similarity index 100% rename from temporalcli/internal/tracer/term_writer_test.go rename to internal/tracer/term_writer_test.go diff --git a/temporalcli/internal/tracer/workflow_execution_update.go b/internal/tracer/workflow_execution_update.go similarity index 100% rename from temporalcli/internal/tracer/workflow_execution_update.go rename to internal/tracer/workflow_execution_update.go diff --git a/temporalcli/internal/tracer/workflow_execution_update_test.go b/internal/tracer/workflow_execution_update_test.go similarity index 100% rename from temporalcli/internal/tracer/workflow_execution_update_test.go rename to internal/tracer/workflow_execution_update_test.go diff --git a/temporalcli/internal/tracer/workflow_state_worker.go b/internal/tracer/workflow_state_worker.go similarity index 100% rename from temporalcli/internal/tracer/workflow_state_worker.go rename to internal/tracer/workflow_state_worker.go diff --git a/temporalcli/internal/tracer/workflow_tracer.go b/internal/tracer/workflow_tracer.go similarity index 100% rename from temporalcli/internal/tracer/workflow_tracer.go rename to internal/tracer/workflow_tracer.go From 19449ab0bb4ed6813be0495aa43a73e1db41a511 Mon Sep 17 00:00:00 2001 From: Stephan Behnke Date: Tue, 2 Dec 2025 11:15:36 -0800 Subject: [PATCH 20/28] Commands generator binary (#875) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What was changed Make `gen-commands` and `gen-docs` a binary that can be used any YAML input. ## Why? Temporal wants to re-use the `commandsgen` tool for other _internal_ CLIs. **It is not supported for users outside of Temporal.** ## Checklist 1. Closes 2. How was this tested: 3. Any docs updates needed? --- .github/workflows/ci.yaml | 20 ++- CONTRIBUTING.md | 8 +- Makefile | 11 +- internal/cmd/gen-commands/README.md | 4 + internal/cmd/gen-commands/main.go | 37 ++++-- internal/cmd/gen-docs/README.md | 4 + internal/cmd/gen-docs/main.go | 41 ++++-- internal/commandsgen/code.go | 75 +++++++++-- internal/commandsgen/parse.go | 12 +- internal/commandsgen/types/duration.go | 49 +++++++ internal/commandsgen/types/duration_test.go | 45 +++++++ internal/commandsgen/types/stringenum.go | 63 +++++++++ .../types}/timestamp.go | 4 +- internal/temporalcli/commands.gen.go | 124 ++++++++++++++++++ internal/temporalcli/commands.schedule.go | 5 +- .../temporalcli/commands.schedule_test.go | 19 +-- .../commands.worker.deployment_test.go | 4 +- .../commands.yaml} | 0 internal/temporalcli/duration.go | 30 ----- internal/temporalcli/strings.go | 60 --------- 20 files changed, 457 insertions(+), 158 deletions(-) create mode 100644 internal/cmd/gen-commands/README.md create mode 100644 internal/cmd/gen-docs/README.md create mode 100644 internal/commandsgen/types/duration.go create mode 100644 internal/commandsgen/types/duration_test.go create mode 100644 internal/commandsgen/types/stringenum.go rename internal/{temporalcli => commandsgen/types}/timestamp.go (81%) rename internal/{commandsgen/commands.yml => temporalcli/commands.yaml} (100%) delete mode 100644 internal/temporalcli/duration.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c3678144d..afa9884e7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,10 +10,17 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, macos-13, windows-latest, ubuntu-arm] + os: + [ + ubuntu-latest, + macos-latest, + macos-15-intel, + windows-latest, + ubuntu-arm, + ] include: - os: ubuntu-latest - checkGenCodeTarget: true + checkGenCommands: true cloudTestTarget: true - os: ubuntu-arm runsOn: buildjet-4vcpu-ubuntu-2204-arm @@ -51,11 +58,16 @@ jobs: retention-days: 14 - name: Regen code, confirm unchanged - if: ${{ matrix.checkGenCodeTarget }} + if: ${{ matrix.checkGenCommands }} run: | - go run ./internal/cmd/gen-commands + go run ./internal/cmd/gen-commands -input internal/temporalcli/commands.yaml -pkg temporalcli -context "*CommandContext" > internal/temporalcli/commands.gen.go git diff --exit-code + - name: Generate docs, confirm working + if: ${{ matrix.checkGenCommands }} + run: | + go run ./internal/cmd/gen-docs -input internal/temporalcli/commands.yaml -output dist/docs + - name: Test cloud mTLS if: ${{ matrix.cloudTestTarget && env.HAS_SECRETS == 'true' }} env: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed8a3e303..54db65aeb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,10 +20,10 @@ Example to run a single test case: ## Adding/updating commands -First, update [commands.yml](internal/commandsgen/commands.yml) following the rules in that file. Then to regenerate the -[commands.gen.go](internal/commands.gen.go) file from code, simply run: +First, update [commands.yaml](internal/temporalcli/commands.yaml) following the rules in that file. Then to regenerate the +[commands.gen.go](internal/temporalcli/commands.gen.go) file from code, run: - go run ./internal/cmd/gen-commands + go run ./internal/cmd/gen-commands -input internal/temporalcli/commands.yaml -pkg temporalcli -context "*CommandContext" > internal/temporalcli/commands.gen.go This will expect every non-parent command to have a `run` method, so for new commands developers will have to implement `run` on the new command in a separate file before it will compile. @@ -31,7 +31,7 @@ This will expect every non-parent command to have a `run` method, so for new com Once a command is updated, the CI will automatically generate new docs and create a PR in the Documentation repo with the corresponding updates. To generate these docs locally, run: - go run ./internal/cmd/gen-docs + go run ./internal/cmd/gen-docs -input internal/temporalcli/commands.yaml -output dist/docs This will auto-generate a new set of docs to `dist/docs/`. If a new root command is added, a new file will be automatically generated, like `temporal activity` and `activity.mdx`. diff --git a/Makefile b/Makefile index 448ee5246..0cf5d679d 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,14 @@ -.PHONY: all gen build fmt-imports +.PHONY: all gen gen-docs build all: gen build -gen: internal/commands.gen.go +gen: internal/temporalcli/commands.gen.go -internal/commands.gen.go: internal/commandsgen/commands.yml - go run ./internal/cmd/gen-commands +internal/temporalcli/commands.gen.go: internal/temporalcli/commands.yaml + go run ./internal/cmd/gen-commands -input $< -pkg temporalcli -context "*CommandContext" > $@ + +gen-docs: internal/temporalcli/commands.yaml + go run ./internal/cmd/gen-docs -input $< -output dist/docs build: go build ./cmd/temporal diff --git a/internal/cmd/gen-commands/README.md b/internal/cmd/gen-commands/README.md new file mode 100644 index 000000000..28c3bd956 --- /dev/null +++ b/internal/cmd/gen-commands/README.md @@ -0,0 +1,4 @@ +This package provides a CLI command generator for Temporal CLI applications. +It is designed specifically for the needs of Temporal CLIs, and not general-purpose command-line tools. + +Backwards-compatibility is not guaranteed. diff --git a/internal/cmd/gen-commands/main.go b/internal/cmd/gen-commands/main.go index 21d8e5529..5297a59be 100644 --- a/internal/cmd/gen-commands/main.go +++ b/internal/cmd/gen-commands/main.go @@ -1,11 +1,10 @@ package main import ( + "flag" "fmt" "log" "os" - "path/filepath" - "runtime" "github.com/temporalio/cli/internal/commandsgen" ) @@ -17,26 +16,42 @@ func main() { } func run() error { - // Get commands dir - _, file, _, _ := runtime.Caller(0) - genCommandsDir := filepath.Dir(file) - commandsDir := filepath.Join(genCommandsDir, "../../") + var ( + pkg string + contextType string + inputFile string + ) + + flag.StringVar(&pkg, "pkg", "main", "Package name for generated code") + flag.StringVar(&contextType, "context", "*CommandContext", "Context type for generated code") + flag.StringVar(&inputFile, "input", "", "Input YAML file (required)") + flag.Parse() + + // Read input from file + if inputFile == "" { + return fmt.Errorf("-input flag is required") + } + yamlBytes, err := os.ReadFile(inputFile) + if err != nil { + return fmt.Errorf("failed reading input: %w", err) + } // Parse YAML - cmds, err := commandsgen.ParseCommands() + cmds, err := commandsgen.ParseCommands(yamlBytes) if err != nil { return fmt.Errorf("failed parsing YAML: %w", err) } // Generate code - b, err := commandsgen.GenerateCommandsCode("temporalcli", cmds) + b, err := commandsgen.GenerateCommandsCode(pkg, contextType, cmds) if err != nil { return fmt.Errorf("failed generating code: %w", err) } - // Write - if err := os.WriteFile(filepath.Join(commandsDir, "commands.gen.go"), b, 0644); err != nil { - return fmt.Errorf("failed writing file: %w", err) + // Write output to stdout + if _, err = os.Stdout.Write(b); err != nil { + return fmt.Errorf("failed writing output: %w", err) } + return nil } diff --git a/internal/cmd/gen-docs/README.md b/internal/cmd/gen-docs/README.md new file mode 100644 index 000000000..c3f13ba25 --- /dev/null +++ b/internal/cmd/gen-docs/README.md @@ -0,0 +1,4 @@ +This package provides a CLI command documentation generator for Temporal CLI applications. +It is designed specifically for the needs of Temporal CLIs, and not general-purpose command-line tools. + +Backwards-compatibility is not guaranteed. diff --git a/internal/cmd/gen-docs/main.go b/internal/cmd/gen-docs/main.go index 0c6e0b942..72f4ecdec 100644 --- a/internal/cmd/gen-docs/main.go +++ b/internal/cmd/gen-docs/main.go @@ -1,11 +1,11 @@ package main import ( + "flag" "fmt" "log" "os" "path/filepath" - "runtime" "github.com/temporalio/cli/internal/commandsgen" ) @@ -17,33 +17,46 @@ func main() { } func run() error { - // Get commands dir - _, file, _, _ := runtime.Caller(0) - genDocsDir := filepath.Dir(file) - docsDir := filepath.Join(genDocsDir, "../../../dist/docs/") + var ( + outputDir string + inputFile string + ) - err := os.MkdirAll(docsDir, os.ModePerm) + flag.StringVar(&inputFile, "input", "", "Input YAML file (required)") + flag.StringVar(&outputDir, "output", ".", "Output directory for docs") + flag.Parse() + + // Read input from file + if inputFile == "" { + return fmt.Errorf("-input flag is required") + } + yamlBytes, err := os.ReadFile(inputFile) if err != nil { - log.Fatalf("Error creating directory: %v", err) + return fmt.Errorf("failed reading input: %w", err) + } + + // Create output directory + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed creating output directory: %w", err) } // Parse YAML - cmds, err := commandsgen.ParseCommands() + cmds, err := commandsgen.ParseCommands(yamlBytes) if err != nil { return fmt.Errorf("failed parsing YAML: %w", err) } // Generate docs - b, err := commandsgen.GenerateDocsFiles(cmds) + docs, err := commandsgen.GenerateDocsFiles(cmds) if err != nil { - return err + return fmt.Errorf("failed generating docs: %w", err) } - // Write - for filename, content := range b { - filePath := filepath.Join(docsDir, filename+".mdx") + // Write files + for filename, content := range docs { + filePath := filepath.Join(outputDir, filename+".mdx") if err := os.WriteFile(filePath, content, 0644); err != nil { - return fmt.Errorf("failed writing file: %w", err) + return fmt.Errorf("failed writing %s: %w", filePath, err) } } diff --git a/internal/commandsgen/code.go b/internal/commandsgen/code.go index 6c67082ba..9d4134a3b 100644 --- a/internal/commandsgen/code.go +++ b/internal/commandsgen/code.go @@ -2,18 +2,27 @@ package commandsgen import ( "bytes" + "embed" "fmt" + "go/ast" "go/format" + "go/parser" + "go/token" + "io/fs" "path" "regexp" "sort" "strings" - "go.temporal.io/server/common/primitives/timestamp" + "github.com/temporalio/cli/internal/commandsgen/types" ) -func GenerateCommandsCode(pkg string, commands Commands) ([]byte, error) { - w := &codeWriter{allCommands: commands.CommandList, OptionSets: commands.OptionSets} +//go:embed types/*.go +var typesFS embed.FS + +func GenerateCommandsCode(pkg string, contextType string, commands Commands) ([]byte, error) { + w := &codeWriter{allCommands: commands.CommandList, OptionSets: commands.OptionSets, contextType: contextType} + // Put terminal check at top w.writeLinef("var hasHighlighting = %v.IsTerminal(%v.Stdout.Fd())", w.importIsatty(), w.importPkg("os")) @@ -24,13 +33,28 @@ func GenerateCommandsCode(pkg string, commands Commands) ([]byte, error) { } } - // Write all commands, then come back and write package and imports + // Write all commands for _, cmd := range commands.CommandList { if err := cmd.writeCode(w); err != nil { return nil, fmt.Errorf("failed writing command %v: %w", cmd.FullName, err) } } + // Append embedded Go files from types/ (parse imports with go/ast, write code after imports) + err := fs.WalkDir(typesFS, "types", func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() || strings.Contains(path, "_test.go") { + return err + } + src, err := typesFS.ReadFile(path) + if err != nil { + return err + } + return w.appendGoSource(string(src)) + }) + if err != nil { + return nil, err + } + // Write package and imports to final buf var finalBuf bytes.Buffer finalBuf.WriteString("// Code generated. DO NOT EDIT.\n\n") @@ -59,6 +83,7 @@ type codeWriter struct { buf bytes.Buffer allCommands []Command OptionSets []OptionSets + contextType string // Key is short ref, value is full imports map[string]string } @@ -98,6 +123,36 @@ func (c *codeWriter) importPkg(pkg string) string { func (c *codeWriter) importCobra() string { return c.importPkg("github.com/spf13/cobra") } +// appendGoSource parses a Go source file, registers its imports, and appends +// everything after the import block to the output buffer. +func (c *codeWriter) appendGoSource(src string) error { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "", src, parser.ImportsOnly) + if err != nil { + return fmt.Errorf("failed to parse embedded source: %w", err) + } + + // Register imports + for _, imp := range f.Imports { + // imp.Path.Value includes quotes, so trim them + c.importPkg(strings.Trim(imp.Path.Value, `"`)) + } + + // Find end of imports and append the rest + var lastImportEnd token.Pos + for _, decl := range f.Decls { + if genDecl, ok := decl.(*ast.GenDecl); ok && genDecl.Tok == token.IMPORT { + if genDecl.End() > lastImportEnd { + lastImportEnd = genDecl.End() + } + } + } + + // Write everything after imports + c.buf.WriteString(src[fset.Position(lastImportEnd).Offset:]) + return nil +} + func (c *codeWriter) importPflag() string { return c.importPkg("github.com/spf13/pflag") } func (c *codeWriter) importIsatty() string { return c.importPkg("github.com/mattn/go-isatty") } @@ -120,8 +175,8 @@ func (o *OptionSets) writeCode(w *codeWriter) error { w.writeLinef("}\n") // write flags - w.writeLinef("func (v *%v) buildFlags(cctx *CommandContext, f *%v.FlagSet) {", - o.setStructName(), w.importPflag()) + w.writeLinef("func (v *%v) buildFlags(cctx %s, f *%v.FlagSet) {", + o.setStructName(), w.contextType, w.importPflag()) o.writeFlagBuilding("v", "f", w) w.writeLinef("}\n") @@ -164,10 +219,10 @@ func (c *Command) writeCode(w *codeWriter) error { // Constructor builds the struct and sets the flags if hasParent { - w.writeLinef("func New%v(cctx *CommandContext, parent *%v) *%v {", - c.structName(), parent.structName(), c.structName()) + w.writeLinef("func New%v(cctx %s, parent *%v) *%v {", + c.structName(), w.contextType, parent.structName(), c.structName()) } else { - w.writeLinef("func New%v(cctx *CommandContext) *%v {", c.structName(), c.structName()) + w.writeLinef("func New%v(cctx %s) *%v {", c.structName(), w.contextType, c.structName()) } w.writeLinef("var s %v", c.structName()) if hasParent { @@ -328,7 +383,7 @@ func (o *Option) writeFlagBuilding(selfVar, flagVar string, w *codeWriter) error case "duration": flagMeth, setDefault = "Var", "0" if o.Default != "" { - dur, err := timestamp.ParseDuration(o.Default) + dur, err := types.ParseDuration(o.Default) if err != nil { return fmt.Errorf("invalid default: %w", err) } diff --git a/internal/commandsgen/parse.go b/internal/commandsgen/parse.go index 4fab512a6..6ace2c004 100644 --- a/internal/commandsgen/parse.go +++ b/internal/commandsgen/parse.go @@ -1,10 +1,8 @@ -// Package commandsgen is built to read the YAML format described in -// internal/commandsgen/commands.yml and generate code from it. +// Package commandsgen reads YAML command definitions and generates code from them. package commandsgen import ( "bytes" - _ "embed" "fmt" "regexp" "slices" @@ -14,9 +12,6 @@ import ( "gopkg.in/yaml.v3" ) -//go:embed commands.yml -var CommandsYAML []byte - type ( // Option represents the structure of an option within option sets. Option struct { @@ -76,9 +71,10 @@ type ( } ) -func ParseCommands() (Commands, error) { +// ParseCommands parses command definitions from YAML bytes. +func ParseCommands(yamlData []byte) (Commands, error) { // Fix CRLF - md := bytes.ReplaceAll(CommandsYAML, []byte("\r\n"), []byte("\n")) + md := bytes.ReplaceAll(yamlData, []byte("\r\n"), []byte("\n")) var m Commands err := yaml.Unmarshal(md, &m) diff --git a/internal/commandsgen/types/duration.go b/internal/commandsgen/types/duration.go new file mode 100644 index 000000000..4ed8f080b --- /dev/null +++ b/internal/commandsgen/types/duration.go @@ -0,0 +1,49 @@ +// NOTE: this file is embedded inside the generated commands.gen.go output + +package types + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "time" +) + +var reDays = regexp.MustCompile(`(\d+(\.\d*)?|(\.\d+))d`) + +type Duration time.Duration + +// ParseDuration is like time.ParseDuration, but supports unit "d" for days +// (always interpreted as exactly 24 hours). +func ParseDuration(s string) (time.Duration, error) { + s = reDays.ReplaceAllStringFunc(s, func(v string) string { + fv, err := strconv.ParseFloat(strings.TrimSuffix(v, "d"), 64) + if err != nil { + return v // will cause time.ParseDuration to return an error + } + return fmt.Sprintf("%fh", 24*fv) + }) + return time.ParseDuration(s) +} + +func (d Duration) Duration() time.Duration { + return time.Duration(d) +} + +func (d *Duration) String() string { + return d.Duration().String() +} + +func (d *Duration) Set(s string) error { + p, err := ParseDuration(s) + if err != nil { + return err + } + *d = Duration(p) + return nil +} + +func (d *Duration) Type() string { + return "duration" +} diff --git a/internal/commandsgen/types/duration_test.go b/internal/commandsgen/types/duration_test.go new file mode 100644 index 000000000..38277190c --- /dev/null +++ b/internal/commandsgen/types/duration_test.go @@ -0,0 +1,45 @@ +package types_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/suite" + "github.com/temporalio/cli/internal/commandsgen/types" +) + +type ParseDurationSuite struct { + suite.Suite +} + +func TestParseDurationSuite(t *testing.T) { + suite.Run(t, new(ParseDurationSuite)) +} + +func (s *ParseDurationSuite) TestParseDuration() { + for _, c := range []struct { + input string + expected time.Duration // -1 means error + }{ + {"1h", time.Hour}, + {"3m30s", 3*time.Minute + 30*time.Second}, + {"1d", 24 * time.Hour}, + {"3d", 3 * 24 * time.Hour}, + {"5d6h15m", 5*24*time.Hour + 6*time.Hour + 15*time.Minute}, + {"5.25d15m", 5*24*time.Hour + 6*time.Hour + 15*time.Minute}, + {".5d", 12 * time.Hour}, + {"-10d12.25h", -(10*24*time.Hour + 12*time.Hour + 15*time.Minute)}, + {"3m2h1d", 3*time.Minute + 2*time.Hour + 1*24*time.Hour}, + {"8m7h6d5d4h3m", 8*time.Minute + 7*time.Hour + 6*24*time.Hour + 5*24*time.Hour + 4*time.Hour + 3*time.Minute}, + {"7", -1}, // error + {"", -1}, // error + {"10000000h", -1}, // error out of bounds + } { + got, err := types.ParseDuration(c.input) + if c.expected == -1 { + s.Error(err) + } else { + s.Equal(c.expected, got) + } + } +} diff --git a/internal/commandsgen/types/stringenum.go b/internal/commandsgen/types/stringenum.go new file mode 100644 index 000000000..2f0f7f437 --- /dev/null +++ b/internal/commandsgen/types/stringenum.go @@ -0,0 +1,63 @@ +// NOTE: this file is embedded inside the generated commands.gen.go output + +package types + +import ( + "fmt" + "strings" +) + +type StringEnum struct { + Allowed []string + Value string + ChangedFromDefault bool +} + +func NewStringEnum(allowed []string, value string) StringEnum { + return StringEnum{Allowed: allowed, Value: value} +} + +func (s *StringEnum) String() string { return s.Value } + +func (s *StringEnum) Set(p string) error { + for _, allowed := range s.Allowed { + if p == allowed { + s.Value = p + s.ChangedFromDefault = true + return nil + } + } + return fmt.Errorf("%v is not one of required values of %v", p, strings.Join(s.Allowed, ", ")) +} + +func (*StringEnum) Type() string { return "string" } + +type StringEnumArray struct { + Allowed map[string]string + Values []string +} + +func NewStringEnumArray(allowed []string, values []string) StringEnumArray { + var allowedMap = make(map[string]string) + for _, str := range allowed { + allowedMap[strings.ToLower(str)] = str + } + return StringEnumArray{Allowed: allowedMap, Values: values} +} + +func (s *StringEnumArray) String() string { return strings.Join(s.Values, ",") } + +func (s *StringEnumArray) Set(p string) error { + val, ok := s.Allowed[strings.ToLower(p)] + if !ok { + values := make([]string, 0, len(s.Allowed)) + for _, v := range s.Allowed { + values = append(values, v) + } + return fmt.Errorf("invalid value: %s, allowed values are: %s", p, strings.Join(values, ", ")) + } + s.Values = append(s.Values, val) + return nil +} + +func (*StringEnumArray) Type() string { return "string" } diff --git a/internal/temporalcli/timestamp.go b/internal/commandsgen/types/timestamp.go similarity index 81% rename from internal/temporalcli/timestamp.go rename to internal/commandsgen/types/timestamp.go index 82aac30a4..4adf4fcfc 100644 --- a/internal/temporalcli/timestamp.go +++ b/internal/commandsgen/types/timestamp.go @@ -1,4 +1,6 @@ -package temporalcli +// NOTE: this file is embedded inside the generated commands.gen.go output + +package types import "time" diff --git a/internal/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go index f01ce7d82..d981f4f27 100644 --- a/internal/temporalcli/commands.gen.go +++ b/internal/temporalcli/commands.gen.go @@ -3,6 +3,8 @@ package temporalcli import ( + "fmt" + "github.com/mattn/go-isatty" "github.com/spf13/cobra" @@ -11,6 +13,12 @@ import ( "os" + "regexp" + + "strconv" + + "strings" + "time" ) @@ -4227,3 +4235,119 @@ func NewTemporalWorkflowUpdateOptionsCommand(cctx *CommandContext, parent *Tempo } return &s } + +var reDays = regexp.MustCompile(`(\d+(\.\d*)?|(\.\d+))d`) + +type Duration time.Duration + +// ParseDuration is like time.ParseDuration, but supports unit "d" for days +// (always interpreted as exactly 24 hours). +func ParseDuration(s string) (time.Duration, error) { + s = reDays.ReplaceAllStringFunc(s, func(v string) string { + fv, err := strconv.ParseFloat(strings.TrimSuffix(v, "d"), 64) + if err != nil { + return v // will cause time.ParseDuration to return an error + } + return fmt.Sprintf("%fh", 24*fv) + }) + return time.ParseDuration(s) +} + +func (d Duration) Duration() time.Duration { + return time.Duration(d) +} + +func (d *Duration) String() string { + return d.Duration().String() +} + +func (d *Duration) Set(s string) error { + p, err := ParseDuration(s) + if err != nil { + return err + } + *d = Duration(p) + return nil +} + +func (d *Duration) Type() string { + return "duration" +} + +type StringEnum struct { + Allowed []string + Value string + ChangedFromDefault bool +} + +func NewStringEnum(allowed []string, value string) StringEnum { + return StringEnum{Allowed: allowed, Value: value} +} + +func (s *StringEnum) String() string { return s.Value } + +func (s *StringEnum) Set(p string) error { + for _, allowed := range s.Allowed { + if p == allowed { + s.Value = p + s.ChangedFromDefault = true + return nil + } + } + return fmt.Errorf("%v is not one of required values of %v", p, strings.Join(s.Allowed, ", ")) +} + +func (*StringEnum) Type() string { return "string" } + +type StringEnumArray struct { + Allowed map[string]string + Values []string +} + +func NewStringEnumArray(allowed []string, values []string) StringEnumArray { + var allowedMap = make(map[string]string) + for _, str := range allowed { + allowedMap[strings.ToLower(str)] = str + } + return StringEnumArray{Allowed: allowedMap, Values: values} +} + +func (s *StringEnumArray) String() string { return strings.Join(s.Values, ",") } + +func (s *StringEnumArray) Set(p string) error { + val, ok := s.Allowed[strings.ToLower(p)] + if !ok { + values := make([]string, 0, len(s.Allowed)) + for _, v := range s.Allowed { + values = append(values, v) + } + return fmt.Errorf("invalid value: %s, allowed values are: %s", p, strings.Join(values, ", ")) + } + s.Values = append(s.Values, val) + return nil +} + +func (*StringEnumArray) Type() string { return "string" } + +type Timestamp time.Time + +func (t Timestamp) Time() time.Time { + return time.Time(t) +} + +func (t *Timestamp) String() string { + return t.Time().Format(time.RFC3339) +} + +func (t *Timestamp) Set(s string) error { + p, err := time.Parse(time.RFC3339, s) + if err != nil { + return err + } + *t = Timestamp(p) + return nil +} + +func (t *Timestamp) Type() string { + return "timestamp" +} diff --git a/internal/temporalcli/commands.schedule.go b/internal/temporalcli/commands.schedule.go index 382e48346..88bd1fd87 100644 --- a/internal/temporalcli/commands.schedule.go +++ b/internal/temporalcli/commands.schedule.go @@ -16,7 +16,6 @@ import ( schedpb "go.temporal.io/api/schedule/v1" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/sdk/client" - "go.temporal.io/server/common/primitives/timestamp" ) type printableSchedule struct { @@ -210,11 +209,11 @@ func toIntervalSpec(str string) (client.ScheduleIntervalSpec, error) { if len(parts) > 2 { return spec, fmt.Errorf(`invalid interval: must be "" or "/"`) } else if len(parts) == 2 { - if spec.Offset, err = timestamp.ParseDuration(parts[1]); err != nil { + if spec.Offset, err = ParseDuration(parts[1]); err != nil { return spec, fmt.Errorf("invalid interval: %w", err) } } - if spec.Every, err = timestamp.ParseDuration(parts[0]); err != nil { + if spec.Every, err = ParseDuration(parts[0]); err != nil { return spec, fmt.Errorf("invalid interval: %w", err) } return spec, nil diff --git a/internal/temporalcli/commands.schedule_test.go b/internal/temporalcli/commands.schedule_test.go index 993fb3f2b..d5ec1baf9 100644 --- a/internal/temporalcli/commands.schedule_test.go +++ b/internal/temporalcli/commands.schedule_test.go @@ -222,14 +222,17 @@ func (s *SharedServerSuite) TestSchedule_List() { s.NoError(res.Err) // table really-long - - res = s.Execute( - "schedule", "list", - "--address", s.Address(), - "--really-long", - ) - s.NoError(res.Err) - out := res.Stdout.String() + var out string + s.EventuallyWithT(func(t *assert.CollectT) { + res = s.Execute( + "schedule", "list", + "--address", s.Address(), + "--really-long", + ) + assert.NoError(t, res.Err) + out = res.Stdout.String() + assert.Contains(t, out, schedId) + }, 10*time.Second, time.Second) s.ContainsOnSameLine(out, schedId, "DevWorkflow", "0s" /*jitter*/, "false", "nil" /*memo*/) s.ContainsOnSameLine(out, "TestSchedule_List") diff --git a/internal/temporalcli/commands.worker.deployment_test.go b/internal/temporalcli/commands.worker.deployment_test.go index 0311ae298..3ceb4c18c 100644 --- a/internal/temporalcli/commands.worker.deployment_test.go +++ b/internal/temporalcli/commands.worker.deployment_test.go @@ -393,7 +393,9 @@ func (s *SharedServerSuite) TestDeployment_List() { "worker", "deployment", "list", "--address", s.Address(), ) - s.NoError(res.Err) + assert.NoError(t, res.Err) + assert.Contains(t, res.Stdout.String(), version1.BuildID) + assert.Contains(t, res.Stdout.String(), version2.BuildID) }, 10*time.Second, 100*time.Millisecond) s.ContainsOnSameLine(res.Stdout.String(), deploymentName1, version1.BuildID) diff --git a/internal/commandsgen/commands.yml b/internal/temporalcli/commands.yaml similarity index 100% rename from internal/commandsgen/commands.yml rename to internal/temporalcli/commands.yaml diff --git a/internal/temporalcli/duration.go b/internal/temporalcli/duration.go deleted file mode 100644 index eef73473d..000000000 --- a/internal/temporalcli/duration.go +++ /dev/null @@ -1,30 +0,0 @@ -package temporalcli - -import ( - "time" - - "go.temporal.io/server/common/primitives/timestamp" -) - -type Duration time.Duration - -func (d Duration) Duration() time.Duration { - return time.Duration(d) -} - -func (d *Duration) String() string { - return d.Duration().String() -} - -func (d *Duration) Set(s string) error { - p, err := timestamp.ParseDuration(s) - if err != nil { - return err - } - *d = Duration(p) - return nil -} - -func (d *Duration) Type() string { - return "duration" -} diff --git a/internal/temporalcli/strings.go b/internal/temporalcli/strings.go index 5904cb5ea..8eab2c288 100644 --- a/internal/temporalcli/strings.go +++ b/internal/temporalcli/strings.go @@ -8,66 +8,6 @@ import ( "strings" ) -type StringEnum struct { - Allowed []string - Value string - ChangedFromDefault bool -} - -func NewStringEnum(allowed []string, value string) StringEnum { - return StringEnum{Allowed: allowed, Value: value} -} - -func (s *StringEnum) String() string { return s.Value } - -func (s *StringEnum) Set(p string) error { - for _, allowed := range s.Allowed { - if p == allowed { - s.Value = p - s.ChangedFromDefault = true - return nil - } - } - return fmt.Errorf("%v is not one of required values of %v", p, strings.Join(s.Allowed, ", ")) -} - -func (*StringEnum) Type() string { return "string" } - -type StringEnumArray struct { - // maps lower case value to original case - Allowed map[string]string - // values in original case - Values []string -} - -func NewStringEnumArray(allowed []string, values []string) StringEnumArray { - // maps lower case value to original case so we can do case insensitive comparison, - // while maintaining original case - var allowedMap = make(map[string]string) - for _, str := range allowed { - allowedMap[strings.ToLower(str)] = str - } - - return StringEnumArray{Allowed: allowedMap, Values: values} -} - -func (s *StringEnumArray) String() string { return strings.Join(s.Values, ",") } - -func (s *StringEnumArray) Set(p string) error { - val, ok := s.Allowed[strings.ToLower(p)] - if !ok { - values := make([]string, 0, len(s.Allowed)) - for _, v := range s.Allowed { - values = append(values, v) - } - return fmt.Errorf("invalid value: %s, allowed values are: %s", p, strings.Join(values, ", ")) - } - s.Values = append(s.Values, val) - return nil -} - -func (*StringEnumArray) Type() string { return "string" } - func stringToProtoEnum[T ~int32](s string, maps ...map[string]int32) (T, error) { // Go over each map looking, if not there, use first map to build set of // strings required From da6d754276dd59e569da797294ce3b3422731019 Mon Sep 17 00:00:00 2001 From: Alex Stanfield <13949480+chaptersix@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:45:20 -0600 Subject: [PATCH 21/28] Automate Docker Image Build and Publish (#877) ## What was changed * added GHA to build and publish docker images * Updated go releaser to v2 * Minor tweaks to other GHA ## Why? Make is easier to publish docker images on release. --- .../workflows/build-and-publish-docker.yml | 123 ++++++++++++++++++ .../workflows/{goreleaser.yml => release.yml} | 27 ++-- .github/workflows/trigger-docs.yml | 25 ++-- .github/workflows/trigger-publish.yml | 40 ------ .goreleaser.yml | 16 ++- Dockerfile | 10 +- docker-bake.hcl | 45 +++++++ 7 files changed, 214 insertions(+), 72 deletions(-) create mode 100644 .github/workflows/build-and-publish-docker.yml rename .github/workflows/{goreleaser.yml => release.yml} (58%) delete mode 100644 .github/workflows/trigger-publish.yml create mode 100644 docker-bake.hcl diff --git a/.github/workflows/build-and-publish-docker.yml b/.github/workflows/build-and-publish-docker.yml new file mode 100644 index 000000000..00bd416d8 --- /dev/null +++ b/.github/workflows/build-and-publish-docker.yml @@ -0,0 +1,123 @@ +name: Build and Publish Docker Image + +on: + workflow_dispatch: + inputs: + version: + description: 'Version tag for the Docker image (e.g., 1.2.3 or v1.2.3)' + required: true + type: string + publish: + description: 'Push the image to Docker Hub' + required: false + type: boolean + default: true + tag_latest: + description: 'Also tag this image as latest' + required: false + type: boolean + default: false + +permissions: + contents: read + +jobs: + build-and-publish: + name: Build and Publish Docker Image + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Get build metadata from release + id: meta + env: + INPUT_VERSION: ${{ inputs.version }} + INPUT_TAG_LATEST: ${{ inputs.tag_latest }} + uses: actions/github-script@v8 + with: + script: | + const inputVersion = process.env.INPUT_VERSION; + const inputTagLatest = process.env.INPUT_TAG_LATEST; + + const version = inputVersion.startsWith('v') ? inputVersion.slice(1) : inputVersion; + const releaseTag = inputVersion.startsWith('v') ? inputVersion : `v${inputVersion}`; + + const { data: release } = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: releaseTag + }); + + const cliSha = release.target_commitish; + const imageShaTag = cliSha.substring(0, 7); + + core.setOutput('cli_sha', cliSha); + core.setOutput('image_sha_tag', imageShaTag); + core.setOutput('version', version); + core.setOutput('release_tag', releaseTag); + core.setOutput('tag_latest', inputTagLatest === 'true'); + + - name: Download release assets + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ steps.meta.outputs.release_tag }} + run: | + echo "Downloading assets from release ${RELEASE_TAG}..." + gh release download "${RELEASE_TAG}" --pattern "temporal_*_linux_*.tar.gz" + + echo "Extracting and organizing binaries..." + mkdir -p dist/amd64 dist/arm64 + + tar -xzf temporal_*_linux_amd64.tar.gz + mv temporal dist/amd64/temporal + + tar -xzf temporal_*_linux_arm64.tar.gz + mv temporal dist/arm64/temporal + + echo "Verifying binaries..." + ls -lh dist/amd64/temporal + ls -lh dist/arm64/temporal + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + if: inputs.publish + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + if: inputs.publish + run: | + docker buildx bake \ + --file docker-bake.hcl \ + --push \ + cli + env: + CLI_SHA: ${{ steps.meta.outputs.cli_sha }} + IMAGE_SHA_TAG: ${{ steps.meta.outputs.image_sha_tag }} + VERSION: ${{ steps.meta.outputs.version }} + TAG_LATEST: ${{ steps.meta.outputs.tag_latest }} + IMAGE_NAMESPACE: temporalio + IMAGE_NAME: temporal + GITHUB_REPOSITORY: ${{ github.repository }} + + - name: Build Docker image (no push) + if: ${{ !inputs.publish }} + run: | + docker buildx bake \ + --file docker-bake.hcl \ + cli + env: + CLI_SHA: ${{ steps.meta.outputs.cli_sha }} + IMAGE_SHA_TAG: ${{ steps.meta.outputs.image_sha_tag }} + VERSION: ${{ steps.meta.outputs.version }} + TAG_LATEST: ${{ steps.meta.outputs.tag_latest }} + IMAGE_NAMESPACE: temporalio + IMAGE_NAME: temporal + GITHUB_REPOSITORY: ${{ github.repository }} diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/release.yml similarity index 58% rename from .github/workflows/goreleaser.yml rename to .github/workflows/release.yml index 3ef1d0b13..0bcd5d666 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: goreleaser +name: Release on: workflow_dispatch: @@ -6,45 +6,50 @@ on: types: - published +permissions: + contents: write + jobs: - goreleaser: + release: + name: Release runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 + uses: actions/setup-go@v6 with: go-version-file: "go.mod" check-latest: true + cache: true - name: Get build date id: date - run: echo "::set-output name=date::$(date '+%F-%T')" + run: echo "date=$(date '+%F-%T')" >> "$GITHUB_OUTPUT" - name: Get build unix timestamp id: timestamp - run: echo "::set-output name=timestamp::$(date '+%s')" + run: echo "timestamp=$(date '+%s')" >> "$GITHUB_OUTPUT" - name: Get git branch id: branch - run: echo "::set-output name=branch::$(git rev-parse --abbrev-ref HEAD)" + run: echo "branch=$(git rev-parse --abbrev-ref HEAD)" >> "$GITHUB_OUTPUT" - name: Get build platform id: platform - run: echo "::set-output name=platform::$(go version | cut -d ' ' -f 4)" + run: echo "platform=$(go version | cut -d ' ' -f 4)" >> "$GITHUB_OUTPUT" - name: Get Go version id: go - run: echo "::set-output name=go::$(go version | cut -d ' ' -f 3)" + run: echo "go=$(go version | cut -d ' ' -f 3)" >> "$GITHUB_OUTPUT" - name: Run GoReleaser - uses: goreleaser/goreleaser-action@336e29918d653399e599bfca99fadc1d7ffbc9f7 # v4.3.0 + uses: goreleaser/goreleaser-action@v6 with: - version: v1.26.2 + version: v2.12.7 args: release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/trigger-docs.yml b/.github/workflows/trigger-docs.yml index 645d976b5..cfa6349a8 100644 --- a/.github/workflows/trigger-docs.yml +++ b/.github/workflows/trigger-docs.yml @@ -3,8 +3,13 @@ on: workflow_dispatch: release: types: [published] + +permissions: + contents: read + jobs: update: + if: github.repository == 'temporalio/cli' runs-on: ubuntu-latest defaults: run: @@ -12,23 +17,25 @@ jobs: steps: - name: Get user info from GitHub API id: get_user + env: + GITHUB_ACTOR: ${{ github.actor }} run: | - echo "GitHub actor: ${{ github.actor }}" + echo "GitHub actor: ${GITHUB_ACTOR}" # Query the GitHub API for the user's details. curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ - https://api.github.com/users/${{ github.actor }} > user.json - + "https://api.github.com/users/${GITHUB_ACTOR}" > user.json + # Extract the user's full name if available, default to the username otherwise. git_name=$(jq -r '.name // empty' user.json) if [ -z "$git_name" ]; then - git_name="${{ github.actor }}" + git_name="${GITHUB_ACTOR}" fi - - git_email="${{ github.actor }}@users.noreply.github.com" - + + git_email="${GITHUB_ACTOR}@users.noreply.github.com" + # Set the outputs for subsequent steps. - echo "GIT_NAME=$git_name" >> $GITHUB_OUTPUT - echo "GIT_EMAIL=$git_email" >> $GITHUB_OUTPUT + echo "GIT_NAME=$git_name" >> "$GITHUB_OUTPUT" + echo "GIT_EMAIL=$git_email" >> "$GITHUB_OUTPUT" - name: Generate token id: generate_token diff --git a/.github/workflows/trigger-publish.yml b/.github/workflows/trigger-publish.yml deleted file mode 100644 index 6f65fefe4..000000000 --- a/.github/workflows/trigger-publish.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: 'Trigger Docker image build' - -on: - workflow_dispatch: - release: - types: [published] - -jobs: - trigger: - if: ${{ ! contains(github.ref, '-rc.') }} - name: 'trigger Docker image build' - runs-on: ubuntu-latest - - defaults: - run: - shell: bash - - steps: - - name: Generate a token - id: generate_token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.TEMPORAL_CICD_APP_ID }} - private-key: ${{ secrets.TEMPORAL_CICD_PRIVATE_KEY }} - owner: temporalio - repositories: | - cli - docker-builds - - - name: Dispatch docker builds Github Action - env: - PAT: ${{ steps.generate_token.outputs.token }} - PARENT_REPO: temporalio/docker-builds - PARENT_BRANCH: ${{ toJSON('main') }} - WORKFLOW_ID: update-submodules.yml - REPO: ${{ toJSON('cli') }} - BRANCH: ${{ toJSON('main') }} - COMMIT: ${{ toJSON(github.sha) }} - run: | - curl -fL -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $PAT" "https://api.github.com/repos/$PARENT_REPO/actions/workflows/$WORKFLOW_ID/dispatches" -d '{"ref":'"$PARENT_BRANCH"', "inputs": { "repo":'"$REPO"', "branch":'"$BRANCH"', "commit": '"$COMMIT"' }}' diff --git a/.goreleaser.yml b/.goreleaser.yml index 9c94e1b4f..f3e29ced2 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,3 +1,5 @@ +version: 2 + before: hooks: - go mod download @@ -11,24 +13,26 @@ archives: - <<: &archive_defaults name_template: "temporal_cli_{{ .Version }}_{{ .Os }}_{{ .Arch }}" id: nix - builds: + ids: - nix - format: tar.gz + formats: + - tar.gz files: - LICENSE - <<: *archive_defaults id: windows-zip - builds: + ids: - windows - format: zip + formats: + - zip files: - LICENSE # used by SDKs as zip cannot be used by rust https://github.com/zip-rs/zip/issues/108 - <<: *archive_defaults id: windows-targz - builds: + ids: - windows files: - LICENSE @@ -61,7 +65,7 @@ checksum: algorithm: sha256 changelog: - skip: true + disable: true announce: skip: "true" diff --git a/Dockerfile b/Dockerfile index 05c3938a3..ca9d986f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,9 @@ -FROM --platform=$BUILDARCH scratch AS dist -COPY ./dist/nix_linux_amd64_v1/temporal /dist/amd64/temporal -COPY ./dist/nix_linux_arm64/temporal /dist/arm64/temporal +FROM alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412 -FROM alpine:3.22 ARG TARGETARCH -RUN apk add --no-cache ca-certificates -COPY --from=dist /dist/$TARGETARCH/temporal /usr/local/bin/temporal + +RUN apk add --no-cache ca-certificates tzdata +COPY dist/${TARGETARCH}/temporal /usr/local/bin/temporal RUN adduser -u 1000 -D temporal USER temporal diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 000000000..66156cf2e --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,45 @@ +variable "IMAGE_NAMESPACE" { + default = "" +} + +variable "IMAGE_NAME" { + default = "temporal" +} + +variable "GITHUB_REPOSITORY" { + default = "temporalio/cli" +} + +variable "IMAGE_SHA_TAG" {} + +variable "CLI_SHA" { + default = "" +} + +variable "VERSION" { + default = "dev" +} + +variable "TAG_LATEST" { + default = false +} + +target "cli" { + dockerfile = "Dockerfile" + context = "." + tags = compact([ + "${IMAGE_NAMESPACE}/${IMAGE_NAME}:${IMAGE_SHA_TAG}", + "${IMAGE_NAMESPACE}/${IMAGE_NAME}:${VERSION}", + TAG_LATEST ? "${IMAGE_NAMESPACE}/${IMAGE_NAME}:latest" : "", + ]) + platforms = ["linux/amd64", "linux/arm64"] + labels = { + "org.opencontainers.image.title" = "temporal" + "org.opencontainers.image.description" = "Temporal CLI" + "org.opencontainers.image.url" = "https://github.com/${GITHUB_REPOSITORY}" + "org.opencontainers.image.source" = "https://github.com/${GITHUB_REPOSITORY}" + "org.opencontainers.image.licenses" = "MIT" + "org.opencontainers.image.revision" = "${CLI_SHA}" + "org.opencontainers.image.created" = timestamp() + } +} From bdf6b678fbfce128499f388c75ba8f3a7eea6649 Mon Sep 17 00:00:00 2001 From: Lenny Chen <55669665+lennessyy@users.noreply.github.com> Date: Tue, 9 Dec 2025 09:28:49 -0800 Subject: [PATCH 22/28] docs: updates cli docs gen script (#882) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What was changed - Updated the script to generate the CLI docs ## Why? The old reference had a big global flag duplication and the think format was slightly hard to read. I modified the script to: - use a table format to make the content more structured and readable - reduce the duplication of the global flags. Instead of duplicating dozens of lines with every subcommand, we now only mention the global flags once per page and link to it from every subcommand. ## Checklist 1. Closes 2. How was this tested: - I ran the script to generate the docs and previewed them here: ### 📖 Docs PR preview links - Cli - [activity](https://temporal-documentation-git-new-cli-autogen.preview.thundergun.io/cli/activity) - [batch](https://temporal-documentation-git-new-cli-autogen.preview.thundergun.io/cli/batch) - [config](https://temporal-documentation-git-new-cli-autogen.preview.thundergun.io/cli/config) - [env](https://temporal-documentation-git-new-cli-autogen.preview.thundergun.io/cli/env) - [operator](https://temporal-documentation-git-new-cli-autogen.preview.thundergun.io/cli/operator) - [schedule](https://temporal-documentation-git-new-cli-autogen.preview.thundergun.io/cli/schedule) - [server](https://temporal-documentation-git-new-cli-autogen.preview.thundergun.io/cli/server) - [task-queue](https://temporal-documentation-git-new-cli-autogen.preview.thundergun.io/cli/task-queue) - [worker](https://temporal-documentation-git-new-cli-autogen.preview.thundergun.io/cli/worker) - [workflow](https://temporal-documentation-git-new-cli-autogen.preview.thundergun.io/cli/workflow) 3. Any docs updates needed? --- internal/commandsgen/docs.go | 146 ++++++++++++++++++++++++++--------- 1 file changed, 109 insertions(+), 37 deletions(-) diff --git a/internal/commandsgen/docs.go b/internal/commandsgen/docs.go index 089a6120e..b3857503e 100644 --- a/internal/commandsgen/docs.go +++ b/internal/commandsgen/docs.go @@ -15,9 +15,10 @@ func GenerateDocsFiles(commands Commands) (map[string][]byte, error) { } w := &docWriter{ - fileMap: make(map[string]*bytes.Buffer), - optionSetMap: optionSetMap, - allCommands: commands.CommandList, + fileMap: make(map[string]*bytes.Buffer), + optionSetMap: optionSetMap, + allCommands: commands.CommandList, + globalFlagsMap: make(map[string]map[string]Option), } // sorted ascending by full name of command (activity complete, batch list, etc) @@ -27,6 +28,9 @@ func GenerateDocsFiles(commands Commands) (map[string][]byte, error) { } } + // Write global flags section once at the end of each file + w.writeGlobalFlagsSections() + // Format and return var finalMap = make(map[string][]byte) for key, buf := range w.fileMap { @@ -36,10 +40,11 @@ func GenerateDocsFiles(commands Commands) (map[string][]byte, error) { } type docWriter struct { - allCommands []Command - fileMap map[string]*bytes.Buffer - optionSetMap map[string]OptionSets - optionsStack [][]Option + allCommands []Command + fileMap map[string]*bytes.Buffer + optionSetMap map[string]OptionSets + optionsStack [][]Option + globalFlagsMap map[string]map[string]Option // fileName -> optionName -> Option } func (c *Command) writeDoc(w *docWriter) error { @@ -76,7 +81,11 @@ func (w *docWriter) writeCommand(c *Command) { w.fileMap[fileName].WriteString("---") w.fileMap[fileName].WriteString("\n\n") w.fileMap[fileName].WriteString("{/* NOTE: This is an auto-generated file. Any edit to this file will be overwritten.\n") - w.fileMap[fileName].WriteString("This file is generated from https://github.com/temporalio/cli/blob/main/internal/commandsgen/commands.yml via internal/cmd/gen-docs */}\n") + w.fileMap[fileName].WriteString("This file is generated from https://github.com/temporalio/cli/blob/main/internal/commandsgen/commands.yml via internal/cmd/gen-docs */}\n\n") + // Add introductory paragraph + w.fileMap[fileName].WriteString(fmt.Sprintf("This page provides a reference for the `temporal` CLI `%s` command. ", fileName)) + w.fileMap[fileName].WriteString("The flags applicable to each subcommand are presented in a table within the heading for the subcommand. ") + w.fileMap[fileName].WriteString("Refer to [Global Flags](#global-flags) for flags that you can use with every subcommand.\n\n") } func (w *docWriter) writeSubcommand(c *Command) { @@ -86,9 +95,7 @@ func (w *docWriter) writeSubcommand(c *Command) { w.fileMap[fileName].WriteString(c.Description + "\n\n") if w.isLeafCommand(c) { - w.fileMap[fileName].WriteString("Use the following options to change the behavior of this command.\n\n") - - // gather options from command and all options aviailable from parent commands + // gather options from command and all options available from parent commands var options = make([]Option, 0) var globalOptions = make([]Option, 0) for i, o := range w.optionsStack { @@ -104,51 +111,116 @@ func (w *docWriter) writeSubcommand(c *Command) { return options[i].Name < options[j].Name }) - sort.Slice(globalOptions, func(i, j int) bool { - return globalOptions[i].Name < globalOptions[j].Name - }) - - w.writeOptions("Flags", options, c) - w.writeOptions("Global Flags", globalOptions, c) + // Write command-specific flags or global flags message + if len(options) > 0 { + w.fileMap[fileName].WriteString("Use the following options to change the behavior of this command. ") + w.fileMap[fileName].WriteString("You can also use any of the [global flags](#global-flags) that apply to all subcommands.\n\n") + w.writeOptionsTable(options, c) + } else { + w.fileMap[fileName].WriteString("Use [global flags](#global-flags) to customize the connection to the Temporal Service for this command.\n\n") + } + // Collect global flags for later (deduplicated) + w.collectGlobalFlags(fileName, globalOptions) } } -func (w *docWriter) writeOptions(prefix string, options []Option, c *Command) { +func (w *docWriter) writeOptionsTable(options []Option, c *Command) { if len(options) == 0 { return } fileName := c.fileName() + buf := w.fileMap[fileName] - w.fileMap[fileName].WriteString(fmt.Sprintf("**%s:**\n\n", prefix)) + // Command-specific flags: 3 columns (no Default) + buf.WriteString("| Flag | Required | Description |\n") + buf.WriteString("|------|----------|-------------|\n") for _, o := range options { - // option name and alias - w.fileMap[fileName].WriteString(fmt.Sprintf("**--%s**", o.Name)) - if len(o.Short) > 0 { - w.fileMap[fileName].WriteString(fmt.Sprintf(", **-%s**", o.Short)) + w.writeOptionRow(buf, o, false) + } + buf.WriteString("\n") +} + +func (w *docWriter) writeOptionRow(buf *bytes.Buffer, o Option, includeDefault bool) { + // Flag name column + flagName := fmt.Sprintf("`--%s`", o.Name) + if len(o.Short) > 0 { + flagName += fmt.Sprintf(", `-%s`", o.Short) + } + + // Required column + required := "No" + if o.Required { + required = "Yes" + } + + // Description column - starts with data type + optionType := o.Type + if o.DisplayType != "" { + optionType = o.DisplayType + } + description := fmt.Sprintf("**%s** %s", optionType, encodeJSONExample(o.Description)) + if len(o.EnumValues) > 0 { + description += fmt.Sprintf(" Accepted values: %s.", strings.Join(o.EnumValues, ", ")) + } + if o.Experimental { + description += " _(Experimental)_" + } + // Escape pipes in description for table compatibility + description = strings.ReplaceAll(description, "|", "\\|") + + if includeDefault { + // Default column + defaultVal := "" + if len(o.Default) > 0 { + defaultVal = fmt.Sprintf("`%s`", o.Default) } - w.fileMap[fileName].WriteString(fmt.Sprintf(" _%s_\n\n", o.Type)) + buf.WriteString(fmt.Sprintf("| %s | %s | %s | %s |\n", flagName, required, description, defaultVal)) + } else { + buf.WriteString(fmt.Sprintf("| %s | %s | %s |\n", flagName, required, description)) + } +} - // description - w.fileMap[fileName].WriteString(encodeJSONExample(o.Description)) - if o.Required { - w.fileMap[fileName].WriteString(" Required.") +func (w *docWriter) collectGlobalFlags(fileName string, options []Option) { + if w.globalFlagsMap[fileName] == nil { + w.globalFlagsMap[fileName] = make(map[string]Option) + } + for _, o := range options { + // Only add if not already present (deduplication) + if _, exists := w.globalFlagsMap[fileName][o.Name]; !exists { + w.globalFlagsMap[fileName][o.Name] = o } - if len(o.EnumValues) > 0 { - w.fileMap[fileName].WriteString(fmt.Sprintf(" Accepted values: %s.", strings.Join(o.EnumValues, ", "))) + } +} + +func (w *docWriter) writeGlobalFlagsSections() { + for fileName, optionsMap := range w.globalFlagsMap { + if len(optionsMap) == 0 { + continue } - if len(o.Default) > 0 { - w.fileMap[fileName].WriteString(fmt.Sprintf(` (default "%s")`, o.Default)) + + // Convert map to slice and sort + options := make([]Option, 0, len(optionsMap)) + for _, o := range optionsMap { + options = append(options, o) } - w.fileMap[fileName].WriteString("\n\n") + sort.Slice(options, func(i, j int) bool { + return options[i].Name < options[j].Name + }) + + buf := w.fileMap[fileName] + buf.WriteString("## Global Flags\n\n") + buf.WriteString("The following options can be used with any command.\n\n") + // Global flags: 4 columns (with Default) + buf.WriteString("| Flag | Required | Description | Default |\n") + buf.WriteString("|------|----------|-------------|--------|\n") - if o.Experimental { - w.fileMap[fileName].WriteString(":::note" + "\n\n") - w.fileMap[fileName].WriteString("Option is experimental." + "\n\n") - w.fileMap[fileName].WriteString(":::" + "\n\n") + for _, o := range options { + w.writeOptionRow(buf, o, true) } + buf.WriteString("\n") } } From fbfc069a9daab336eea385840d5d7bfce0064b51 Mon Sep 17 00:00:00 2001 From: Tamoghna Sen <63703820+Tamoghnasen9@users.noreply.github.com> Date: Fri, 12 Dec 2025 00:25:44 +0530 Subject: [PATCH 23/28] Enable setting of Temporal headers through cli (#883) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What was changed Support setting of Temporal headers in workflows ## Why? The cli doesn't support the setting of workflow headers. With this change we would be able to pass the headers to Workflows using cli. ## Checklist 1. Closes - https://github.com/temporalio/cli/issues/876 2. How was this tested: - Brought up the dev server using : `temporal server start-dev` - Build the cli locally and passed the `--headers` flag to start a workflow. Checked in Temporal UI, the Header section shows the headers set in Workflow. 3. Any docs updates needed? https://docs.temporal.io/cli/workflow --- internal/temporalcli/client.go | 43 +++++++++++++++++++ internal/temporalcli/commands.gen.go | 8 ++++ internal/temporalcli/commands.workflow.go | 22 ++++++++-- .../temporalcli/commands.workflow_exec.go | 15 +++++++ .../commands.workflow_exec_test.go | 27 ++++++++++++ internal/temporalcli/commands.yaml | 28 ++++++++++++ 6 files changed, 140 insertions(+), 3 deletions(-) diff --git a/internal/temporalcli/client.go b/internal/temporalcli/client.go index cc8c73018..9299d4fc7 100644 --- a/internal/temporalcli/client.go +++ b/internal/temporalcli/client.go @@ -13,6 +13,7 @@ import ( "go.temporal.io/sdk/contrib/envconfig" "go.temporal.io/sdk/converter" "go.temporal.io/sdk/log" + "go.temporal.io/sdk/workflow" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) @@ -229,6 +230,8 @@ func (c *ClientOptions) dialClient(cctx *CommandContext) (client.Client, error) return client.DialContext(ctxWithTimeout, clientOptions) } + clientOptions.ContextPropagators = append(clientOptions.ContextPropagators, headerPropagator{}) + return client.DialContext(cctx, clientOptions) } @@ -314,3 +317,43 @@ func (rawValuePayloadConverter) Encoding() string { // Should never be used return "raw-value-encoding" } + +type headerPropagator struct{} + +type cliHeaderContextKey struct{} + +func (headerPropagator) Inject(ctx context.Context, writer workflow.HeaderWriter) error { + if headers, ok := ctx.Value(cliHeaderContextKey{}).(map[string]any); ok { + for k, v := range headers { + p, err := converter.GetDefaultDataConverter().ToPayload(v) + if err != nil { + return err + } + writer.Set(k, p) + } + } + return nil +} + +func (headerPropagator) InjectFromWorkflow(ctx workflow.Context, writer workflow.HeaderWriter) error { + return nil +} + +func (headerPropagator) Extract(ctx context.Context, _ workflow.HeaderReader) (context.Context, error) { + return ctx, nil +} + +func (headerPropagator) ExtractToWorkflow(ctx workflow.Context, _ workflow.HeaderReader) (workflow.Context, error) { + return ctx, nil +} + +func contextWithHeaders(ctx context.Context, headers []string) (context.Context, error) { + if len(headers) == 0 { + return ctx, nil + } + out, err := stringKeysJSONValues(headers, false) + if err != nil { + return ctx, err + } + return context.WithValue(ctx, cliHeaderContextKey{}, out), nil +} diff --git a/internal/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go index d981f4f27..7b7deec83 100644 --- a/internal/temporalcli/commands.gen.go +++ b/internal/temporalcli/commands.gen.go @@ -184,6 +184,7 @@ type SingleWorkflowOrBatchOptions struct { Reason string Yes bool Rps float32 + Headers []string } func (v *SingleWorkflowOrBatchOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSet) { @@ -193,6 +194,7 @@ func (v *SingleWorkflowOrBatchOptions) buildFlags(cctx *CommandContext, f *pflag f.StringVar(&v.Reason, "reason", "", "Reason for batch operation. Only use with --query. Defaults to user name.") f.BoolVarP(&v.Yes, "yes", "y", false, "Don't prompt to confirm signaling. Only allowed when --query is present.") f.Float32Var(&v.Rps, "rps", 0, "Limit batch's requests per second. Only allowed if query is present.") + f.StringArrayVar(&v.Headers, "headers", nil, "Temporal workflow headers in 'KEY=VALUE' format. Keys must be identifiers, and values must be JSON values. May be passed multiple times to set multiple Temporal headers. Note: These are workflow headers, not gRPC headers.") } type SharedWorkflowStartOptions struct { @@ -203,6 +205,7 @@ type SharedWorkflowStartOptions struct { ExecutionTimeout Duration TaskTimeout Duration SearchAttribute []string + Headers []string Memo []string StaticSummary string StaticDetails string @@ -224,6 +227,7 @@ func (v *SharedWorkflowStartOptions) buildFlags(cctx *CommandContext, f *pflag.F v.TaskTimeout = Duration(10000 * time.Millisecond) f.Var(&v.TaskTimeout, "task-timeout", "Fail a Workflow Task if it lasts longer than `DURATION`. This is the Start-to-close timeout for a Workflow Task.") f.StringArrayVar(&v.SearchAttribute, "search-attribute", nil, "Search Attribute in `KEY=VALUE` format. Keys must be identifiers, and values must be JSON values. For example: 'YourKey={\"your\": \"value\"}'. Can be passed multiple times.") + f.StringArrayVar(&v.Headers, "headers", nil, "Temporal workflow headers in 'KEY=VALUE' format. Keys must be identifiers, and values must be JSON values. May be passed multiple times to set multiple Temporal headers. Note: These are workflow headers, not gRPC headers.") f.StringArrayVar(&v.Memo, "memo", nil, "Memo using 'KEY=\"VALUE\"' pairs. Use JSON values.") f.StringVar(&v.StaticSummary, "static-summary", "", "Static Workflow summary for human consumption in UIs. Uses Temporal Markdown formatting, should be a single line. EXPERIMENTAL.") f.StringVar(&v.StaticDetails, "static-details", "", "Static Workflow details for human consumption in UIs. Uses Temporal Markdown formatting, may be multiple lines. EXPERIMENTAL.") @@ -272,6 +276,7 @@ type UpdateStartingOptions struct { WorkflowId string UpdateId string RunId string + Headers []string } func (v *UpdateStartingOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSet) { @@ -282,6 +287,7 @@ func (v *UpdateStartingOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSe _ = cobra.MarkFlagRequired(f, "workflow-id") f.StringVar(&v.UpdateId, "update-id", "", "Update ID. If unset, defaults to a UUID.") f.StringVarP(&v.RunId, "run-id", "r", "", "Run ID. If unset, looks for an Update against the currently-running Workflow Execution.") + f.StringArrayVar(&v.Headers, "headers", nil, "Temporal workflow headers in 'KEY=VALUE' format. Keys must be identifiers, and values must be JSON values. May be passed multiple times to set multiple Temporal headers. Note: These are workflow headers, not gRPC headers.") } type UpdateTargetingOptions struct { @@ -325,11 +331,13 @@ func (v *NexusEndpointConfigOptions) buildFlags(cctx *CommandContext, f *pflag.F type QueryModifiersOptions struct { RejectCondition StringEnum + Headers []string } func (v *QueryModifiersOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSet) { v.RejectCondition = NewStringEnum([]string{"not_open", "not_completed_cleanly"}, "") f.Var(&v.RejectCondition, "reject-condition", "Optional flag for rejecting Queries based on Workflow state. Accepted values: not_open, not_completed_cleanly.") + f.StringArrayVar(&v.Headers, "headers", nil, "Temporal workflow headers in 'KEY=VALUE' format. Keys must be identifiers, and values must be JSON values. May be passed multiple times to set multiple Temporal headers. Note: These are workflow headers, not gRPC headers.") } type WorkflowUpdateOptionsOptions struct { diff --git a/internal/temporalcli/commands.workflow.go b/internal/temporalcli/commands.workflow.go index 975be9cec..8915f4e35 100644 --- a/internal/temporalcli/commands.workflow.go +++ b/internal/temporalcli/commands.workflow.go @@ -190,12 +190,12 @@ func (c *TemporalWorkflowUpdateOptionsCommand) run(cctx *CommandContext, args [] func (c *TemporalWorkflowMetadataCommand) run(cctx *CommandContext, _ []string) error { return queryHelper(cctx, c.Parent, PayloadInputOptions{}, - metadataQueryName, c.RejectCondition, c.WorkflowReferenceOptions) + metadataQueryName, nil, c.RejectCondition, c.WorkflowReferenceOptions) } func (c *TemporalWorkflowQueryCommand) run(cctx *CommandContext, args []string) error { return queryHelper(cctx, c.Parent, c.PayloadInputOptions, - c.Name, c.RejectCondition, c.WorkflowReferenceOptions) + c.Name, c.Headers, c.RejectCondition, c.WorkflowReferenceOptions) } func (c *TemporalWorkflowSignalCommand) run(cctx *CommandContext, args []string) error { @@ -210,6 +210,11 @@ func (c *TemporalWorkflowSignalCommand) run(cctx *CommandContext, args []string) return err } + cctx.Context, err = contextWithHeaders(cctx.Context, c.Headers) + if err != nil { + return err + } + exec, batchReq, err := c.workflowExecOrBatch(cctx, c.Parent.Namespace, cl, singleOrBatchOverrides{}) // Run single or batch @@ -246,7 +251,7 @@ func (c *TemporalWorkflowSignalCommand) run(cctx *CommandContext, args []string) func (c *TemporalWorkflowStackCommand) run(cctx *CommandContext, args []string) error { return queryHelper(cctx, c.Parent, PayloadInputOptions{}, - "__stack_trace", c.RejectCondition, c.WorkflowReferenceOptions) + "__stack_trace", nil, c.RejectCondition, c.WorkflowReferenceOptions) } func (c *TemporalWorkflowTerminateCommand) run(cctx *CommandContext, _ []string) error { @@ -449,6 +454,11 @@ func workflowUpdateHelper(cctx *CommandContext, WaitForStage: waitForStage, } + cctx.Context, err = contextWithHeaders(cctx.Context, updateStartOpts.Headers) + if err != nil { + return err + } + updateHandle, err := cl.UpdateWorkflow(cctx, request) if err != nil { return fmt.Errorf("unable to update workflow: %w", err) @@ -580,6 +590,7 @@ func queryHelper(cctx *CommandContext, parent *TemporalWorkflowCommand, inputOpts PayloadInputOptions, queryType string, + headers []string, rejectCondition StringEnum, execution WorkflowReferenceOptions, ) error { @@ -605,6 +616,11 @@ func queryHelper(cctx *CommandContext, return fmt.Errorf("invalid query reject condition: %v, valid values are: 'not_open', 'not_completed_cleanly'", rejectCondition) } + cctx.Context, err = contextWithHeaders(cctx.Context, headers) + if err != nil { + return err + } + result, err := cl.WorkflowService().QueryWorkflow(cctx, &workflowservice.QueryWorkflowRequest{ Namespace: parent.Namespace, Execution: &common.WorkflowExecution{WorkflowId: execution.WorkflowId, RunId: execution.RunId}, diff --git a/internal/temporalcli/commands.workflow_exec.go b/internal/temporalcli/commands.workflow_exec.go index 94dbec320..37b2a54ad 100644 --- a/internal/temporalcli/commands.workflow_exec.go +++ b/internal/temporalcli/commands.workflow_exec.go @@ -158,6 +158,11 @@ func (c *TemporalWorkflowSignalWithStartCommand) run(cctx *CommandContext, _ []s searchAttr = &common.SearchAttributes{IndexedFields: fields} } + cctx.Context, err = contextWithHeaders(cctx.Context, c.SharedWorkflowStartOptions.Headers) + if err != nil { + return err + } + // We have to use the raw signal service call here because the Go SDK's // signal-with-start call doesn't accept multiple signal arguments. resp, err := cl.WorkflowService().SignalWithStartWorkflowExecution( @@ -363,6 +368,10 @@ func executeUpdateWithStartWorkflow( if err != nil { return nil, err } + cctx.Context, err = contextWithHeaders(cctx.Context, sharedWfOpts.Headers) + if err != nil { + return nil, err + } startOp := cl.NewWithStartWorkflowOperation( clStartWfOpts, @@ -521,6 +530,12 @@ func (c *TemporalWorkflowCommand) startWorkflow( if err != nil { return nil, err } + + cctx.Context, err = contextWithHeaders(cctx.Context, sharedWorkflowOpts.Headers) + if err != nil { + return nil, err + } + run, err := cl.ExecuteWorkflow(cctx, startOpts, sharedWorkflowOpts.Type, input...) if err != nil { return nil, fmt.Errorf("failed starting workflow: %w", err) diff --git a/internal/temporalcli/commands.workflow_exec_test.go b/internal/temporalcli/commands.workflow_exec_test.go index 690b4823f..62d809d3b 100644 --- a/internal/temporalcli/commands.workflow_exec_test.go +++ b/internal/temporalcli/commands.workflow_exec_test.go @@ -104,6 +104,33 @@ func (s *SharedServerSuite) TestWorkflow_Start_StartDelay() { ) } +func (s *SharedServerSuite) TestWorkflow_Start_With_headers() { + res := s.Execute( + "workflow", "start", + "--address", s.Address(), + "--headers", "id=123", + "--task-queue", s.Worker().Options.TaskQueue, + "--type", "DevWorkflow", + "--workflow-id", "id123", + "-i", `["val1", "val2"]`, + ) + s.NoError(res.Err) + eventIter := s.Client.GetWorkflowHistory(s.Context, "id123", "", false, enums.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) + for eventIter.HasNext() { + event, err := eventIter.Next() + s.NoError(err) + if event.EventType == enums.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED { + headers := event.GetWorkflowExecutionStartedEventAttributes().GetHeader() + payload := headers.Fields["id"] + s.NotNil(payload) + var val int + err := converter.GetDefaultDataConverter().FromPayload(payload, &val) + s.NoError(err) + s.Equal(123, val) + } + } +} + func (s *SharedServerSuite) TestWorkflow_Execute_SimpleSuccess() { // Text s.Worker().OnDevWorkflow(func(ctx workflow.Context, input any) (any, error) { diff --git a/internal/temporalcli/commands.yaml b/internal/temporalcli/commands.yaml index cc595d8be..bc9c22669 100644 --- a/internal/temporalcli/commands.yaml +++ b/internal/temporalcli/commands.yaml @@ -4720,6 +4720,13 @@ option-sets: description: | Limit batch's requests per second. Only allowed if query is present. + - name: headers + type: string[] + description: | + Temporal workflow headers in 'KEY=VALUE' format. + Keys must be identifiers, and values must be JSON values. + May be passed multiple times to set multiple Temporal headers. + Note: These are workflow headers, not gRPC headers. - name: shared-workflow-start options: @@ -4762,6 +4769,13 @@ option-sets: Keys must be identifiers, and values must be JSON values. For example: 'YourKey={"your": "value"}'. Can be passed multiple times. + - name: headers + type: string[] + description: | + Temporal workflow headers in 'KEY=VALUE' format. + Keys must be identifiers, and values must be JSON values. + May be passed multiple times to set multiple Temporal headers. + Note: These are workflow headers, not gRPC headers. - name: memo type: string[] description: | @@ -4889,6 +4903,13 @@ option-sets: description: | Run ID. If unset, looks for an Update against the currently-running Workflow Execution. + - name: headers + type: string[] + description: | + Temporal workflow headers in 'KEY=VALUE' format. + Keys must be identifiers, and values must be JSON values. + May be passed multiple times to set multiple Temporal headers. + Note: These are workflow headers, not gRPC headers. - name: update-targeting options: @@ -4952,6 +4973,13 @@ option-sets: enum-values: - not_open - not_completed_cleanly + - name: headers + type: string[] + description: | + Temporal workflow headers in 'KEY=VALUE' format. + Keys must be identifiers, and values must be JSON values. + May be passed multiple times to set multiple Temporal headers. + Note: These are workflow headers, not gRPC headers. - name: workflow-update-options options: From 32e30b10cd4fd902f205f10c37dd0cdb389c4a66 Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Thu, 11 Dec 2025 15:53:58 -0500 Subject: [PATCH 24/28] Fix worker list example query (#884) --- internal/temporalcli/commands.gen.go | 4 ++-- internal/temporalcli/commands.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go index 7b7deec83..6cdd717f7 100644 --- a/internal/temporalcli/commands.gen.go +++ b/internal/temporalcli/commands.gen.go @@ -3277,9 +3277,9 @@ func NewTemporalWorkerListCommand(cctx *CommandContext, parent *TemporalWorkerCo s.Command.Use = "list [flags]" s.Command.Short = "List worker status information in a specific namespace (EXPERIMENTAL)" if hasHighlighting { - s.Command.Long = "Get a list of workers to the specified namespace.\n\n\x1b[1mtemporal worker list --namespace YourNamespace --query 'taskQueue=\"YourTaskQueue\"'\x1b[0m" + s.Command.Long = "Get a list of workers to the specified namespace.\n\n\x1b[1mtemporal worker list --namespace YourNamespace --query 'TaskQueue=\"YourTaskQueue\"'\x1b[0m" } else { - s.Command.Long = "Get a list of workers to the specified namespace.\n\n```\ntemporal worker list --namespace YourNamespace --query 'taskQueue=\"YourTaskQueue\"'\n```" + s.Command.Long = "Get a list of workers to the specified namespace.\n\n```\ntemporal worker list --namespace YourNamespace --query 'TaskQueue=\"YourTaskQueue\"'\n```" } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVarP(&s.Query, "query", "q", "", "Content for an SQL-like `QUERY` List Filter.") diff --git a/internal/temporalcli/commands.yaml b/internal/temporalcli/commands.yaml index bc9c22669..7297d67fa 100644 --- a/internal/temporalcli/commands.yaml +++ b/internal/temporalcli/commands.yaml @@ -1412,7 +1412,7 @@ commands: Get a list of workers to the specified namespace. ``` - temporal worker list --namespace YourNamespace --query 'taskQueue="YourTaskQueue"' + temporal worker list --namespace YourNamespace --query 'TaskQueue="YourTaskQueue"' ``` options: - name: query From 36bff7182259ff454d55867289edbb241597419b Mon Sep 17 00:00:00 2001 From: Stephan Behnke Date: Fri, 12 Dec 2025 13:36:38 -0800 Subject: [PATCH 25/28] Move gen commands (#887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What was changed Moved `‎internal/cmd/gen-commands` to `cmd/gen-commands`; and same for `gen-docs`. ## Why? When they are in `internal/` they cannot be invoked via `go run` from outside the repository; making it cumbersome to use. ## Checklist 1. Closes 2. How was this tested: 3. Any docs updates needed? --- .github/workflows/ci.yaml | 4 ++-- Makefile | 4 ++-- {internal/cmd => cmd}/gen-commands/README.md | 0 {internal/cmd => cmd}/gen-commands/main.go | 0 {internal/cmd => cmd}/gen-docs/README.md | 0 {internal/cmd => cmd}/gen-docs/main.go | 0 6 files changed, 4 insertions(+), 4 deletions(-) rename {internal/cmd => cmd}/gen-commands/README.md (100%) rename {internal/cmd => cmd}/gen-commands/main.go (100%) rename {internal/cmd => cmd}/gen-docs/README.md (100%) rename {internal/cmd => cmd}/gen-docs/main.go (100%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index afa9884e7..a8cc5a6bc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -60,13 +60,13 @@ jobs: - name: Regen code, confirm unchanged if: ${{ matrix.checkGenCommands }} run: | - go run ./internal/cmd/gen-commands -input internal/temporalcli/commands.yaml -pkg temporalcli -context "*CommandContext" > internal/temporalcli/commands.gen.go + go run ./cmd/gen-commands -input internal/temporalcli/commands.yaml -pkg temporalcli -context "*CommandContext" > internal/temporalcli/commands.gen.go git diff --exit-code - name: Generate docs, confirm working if: ${{ matrix.checkGenCommands }} run: | - go run ./internal/cmd/gen-docs -input internal/temporalcli/commands.yaml -output dist/docs + go run ./cmd/gen-docs -input internal/temporalcli/commands.yaml -output dist/docs - name: Test cloud mTLS if: ${{ matrix.cloudTestTarget && env.HAS_SECRETS == 'true' }} diff --git a/Makefile b/Makefile index 0cf5d679d..ca21efd74 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,10 @@ all: gen build gen: internal/temporalcli/commands.gen.go internal/temporalcli/commands.gen.go: internal/temporalcli/commands.yaml - go run ./internal/cmd/gen-commands -input $< -pkg temporalcli -context "*CommandContext" > $@ + go run ./cmd/gen-commands -input $< -pkg temporalcli -context "*CommandContext" > $@ gen-docs: internal/temporalcli/commands.yaml - go run ./internal/cmd/gen-docs -input $< -output dist/docs + go run ./cmd/gen-docs -input $< -output dist/docs build: go build ./cmd/temporal diff --git a/internal/cmd/gen-commands/README.md b/cmd/gen-commands/README.md similarity index 100% rename from internal/cmd/gen-commands/README.md rename to cmd/gen-commands/README.md diff --git a/internal/cmd/gen-commands/main.go b/cmd/gen-commands/main.go similarity index 100% rename from internal/cmd/gen-commands/main.go rename to cmd/gen-commands/main.go diff --git a/internal/cmd/gen-docs/README.md b/cmd/gen-docs/README.md similarity index 100% rename from internal/cmd/gen-docs/README.md rename to cmd/gen-docs/README.md diff --git a/internal/cmd/gen-docs/main.go b/cmd/gen-docs/main.go similarity index 100% rename from internal/cmd/gen-docs/main.go rename to cmd/gen-docs/main.go From e21bd3479af5f563a99ba374514c819943aa0c4c Mon Sep 17 00:00:00 2001 From: Adrien Barreau Date: Wed, 17 Dec 2025 18:30:37 +0100 Subject: [PATCH 26/28] fix: wrong examples for config delete(-profile)? (#888) ## What was changed - replace `temporal env delete` by `temporal config delete` in `temporal config` subcommands helpers - fix CONTRIBUTING.md for code generation ## Why? Helpers suggested `temporal env` instead of `temporal config`; feels like a wrong copy/paste. ## Checklist 1. Any docs updates needed? https://docs.temporal.io/cli/config --- CONTRIBUTING.md | 4 ++-- internal/temporalcli/commands.gen.go | 8 ++++---- internal/temporalcli/commands.yaml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 54db65aeb..f0630164e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ Example to run a single test case: First, update [commands.yaml](internal/temporalcli/commands.yaml) following the rules in that file. Then to regenerate the [commands.gen.go](internal/temporalcli/commands.gen.go) file from code, run: - go run ./internal/cmd/gen-commands -input internal/temporalcli/commands.yaml -pkg temporalcli -context "*CommandContext" > internal/temporalcli/commands.gen.go + go run ./cmd/gen-commands -input internal/temporalcli/commands.yaml -pkg temporalcli -context "*CommandContext" > internal/temporalcli/commands.gen.go This will expect every non-parent command to have a `run` method, so for new commands developers will have to implement `run` on the new command in a separate file before it will compile. @@ -31,7 +31,7 @@ This will expect every non-parent command to have a `run` method, so for new com Once a command is updated, the CI will automatically generate new docs and create a PR in the Documentation repo with the corresponding updates. To generate these docs locally, run: - go run ./internal/cmd/gen-docs -input internal/temporalcli/commands.yaml -output dist/docs + go run ./cmd/gen-docs -input internal/temporalcli/commands.yaml -output dist/docs This will auto-generate a new set of docs to `dist/docs/`. If a new root command is added, a new file will be automatically generated, like `temporal activity` and `activity.mdx`. diff --git a/internal/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go index 6cdd717f7..5a0df2037 100644 --- a/internal/temporalcli/commands.gen.go +++ b/internal/temporalcli/commands.gen.go @@ -835,9 +835,9 @@ func NewTemporalConfigDeleteCommand(cctx *CommandContext, parent *TemporalConfig s.Command.Use = "delete [flags]" s.Command.Short = "Delete a config file property (EXPERIMENTAL)\n" if hasHighlighting { - s.Command.Long = "Remove a property within a profile.\n\n\x1b[1mtemporal env delete \\\n --prop tls.client_cert_path\x1b[0m" + s.Command.Long = "Remove a property within a profile.\n\n\x1b[1mtemporal config delete \\\n --prop tls.client_cert_path\x1b[0m" } else { - s.Command.Long = "Remove a property within a profile.\n\n```\ntemporal env delete \\\n --prop tls.client_cert_path\n```" + s.Command.Long = "Remove a property within a profile.\n\n```\ntemporal config delete \\\n --prop tls.client_cert_path\n```" } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVarP(&s.Prop, "prop", "p", "", "Specific property to delete. If unset, deletes entire profile. Required.") @@ -862,9 +862,9 @@ func NewTemporalConfigDeleteProfileCommand(cctx *CommandContext, parent *Tempora s.Command.Use = "delete-profile [flags]" s.Command.Short = "Delete an entire config profile (EXPERIMENTAL)\n" if hasHighlighting { - s.Command.Long = "Remove a full profile entirely. The \x1b[1m--profile\x1b[0m must be set explicitly.\n\n\x1b[1mtemporal env delete-profile \\\n --profile my-profile\x1b[0m" + s.Command.Long = "Remove a full profile entirely. The \x1b[1m--profile\x1b[0m must be set explicitly.\n\n\x1b[1mtemporal config delete-profile \\\n --profile my-profile\x1b[0m" } else { - s.Command.Long = "Remove a full profile entirely. The `--profile` must be set explicitly.\n\n```\ntemporal env delete-profile \\\n --profile my-profile\n```" + s.Command.Long = "Remove a full profile entirely. The `--profile` must be set explicitly.\n\n```\ntemporal config delete-profile \\\n --profile my-profile\n```" } s.Command.Args = cobra.NoArgs s.Command.Run = func(c *cobra.Command, args []string) { diff --git a/internal/temporalcli/commands.yaml b/internal/temporalcli/commands.yaml index 7297d67fa..b428ccc45 100644 --- a/internal/temporalcli/commands.yaml +++ b/internal/temporalcli/commands.yaml @@ -787,7 +787,7 @@ commands: Remove a property within a profile. ``` - temporal env delete \ + temporal config delete \ --prop tls.client_cert_path ``` options: @@ -805,7 +805,7 @@ commands: Remove a full profile entirely. The `--profile` must be set explicitly. ``` - temporal env delete-profile \ + temporal config delete-profile \ --profile my-profile ``` From 90bebb4d3da373057641c2a6440ccc19142029aa Mon Sep 17 00:00:00 2001 From: Sean Kane Date: Wed, 17 Dec 2025 11:29:47 -0700 Subject: [PATCH 27/28] Fix envconfig namespace respected bug (#891) ## What was changed Call `dialClient` before accessing namespace. `dialClient` will use the envconfig files provided to find the namespace, without this change the `--namespace` flag has to be provided. ## Why? Properly respects the envconfig namespace. ## Checklist 1. Closes #890 2. How was this tested: Added tests 3. Any docs updates needed? No --- .../commands.operator_namespace.go | 36 ++++---- .../commands.operator_namespace_test.go | 84 +++++++++++++++++++ .../temporalcli/commands.taskqueue_config.go | 20 ++--- .../commands.taskqueue_config_test.go | 71 ++++++++++++++++ 4 files changed, 183 insertions(+), 28 deletions(-) diff --git a/internal/temporalcli/commands.operator_namespace.go b/internal/temporalcli/commands.operator_namespace.go index a46b851d2..041955e00 100644 --- a/internal/temporalcli/commands.operator_namespace.go +++ b/internal/temporalcli/commands.operator_namespace.go @@ -26,16 +26,16 @@ func (c *TemporalOperatorCommand) getNSFromFlagOrArg0(cctx *CommandContext, args } func (c *TemporalOperatorNamespaceCreateCommand) run(cctx *CommandContext, args []string) error { - nsName, err := c.Parent.Parent.getNSFromFlagOrArg0(cctx, args) + cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) if err != nil { return err } + defer cl.Close() - cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) + nsName, err := c.Parent.Parent.getNSFromFlagOrArg0(cctx, args) if err != nil { return err } - defer cl.Close() var clusters []*replication.ClusterReplicationConfig for _, clusterName := range c.Cluster { @@ -74,6 +74,12 @@ func (c *TemporalOperatorNamespaceCreateCommand) run(cctx *CommandContext, args } func (c *TemporalOperatorNamespaceDeleteCommand) run(cctx *CommandContext, args []string) error { + cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) + if err != nil { + return err + } + defer cl.Close() + nsName, err := c.Parent.Parent.getNSFromFlagOrArg0(cctx, args) if err != nil { return err @@ -91,12 +97,6 @@ func (c *TemporalOperatorNamespaceDeleteCommand) run(cctx *CommandContext, args return fmt.Errorf("user denied confirmation or mistyped the namespace name") } - cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) - if err != nil { - return err - } - defer cl.Close() - resp, err := cl.OperatorService().DeleteNamespace(cctx, &operatorservice.DeleteNamespaceRequest{ Namespace: nsName, }) @@ -114,6 +114,12 @@ func (c *TemporalOperatorNamespaceDeleteCommand) run(cctx *CommandContext, args func (c *TemporalOperatorNamespaceDescribeCommand) run(cctx *CommandContext, args []string) error { nsID := c.NamespaceId + cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) + if err != nil { + return err + } + defer cl.Close() + nsName, err := c.Parent.Parent.getNSFromFlagOrArg0(cctx, args) if err != nil { return err @@ -131,12 +137,6 @@ func (c *TemporalOperatorNamespaceDescribeCommand) run(cctx *CommandContext, arg return fmt.Errorf("provide one of --namespace-id= or -n name, but not both") } - cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) - if err != nil { - return err - } - defer cl.Close() - resp, err := cl.WorkflowService().DescribeNamespace(cctx, &workflowservice.DescribeNamespaceRequest{ Namespace: nsName, Id: nsID, @@ -193,16 +193,16 @@ func (c *TemporalOperatorNamespaceListCommand) run(cctx *CommandContext, args [] } func (c *TemporalOperatorNamespaceUpdateCommand) run(cctx *CommandContext, args []string) error { - nsName, err := c.Parent.Parent.getNSFromFlagOrArg0(cctx, args) + cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) if err != nil { return err } + defer cl.Close() - cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) + nsName, err := c.Parent.Parent.getNSFromFlagOrArg0(cctx, args) if err != nil { return err } - defer cl.Close() var updateRequest *workflowservice.UpdateNamespaceRequest diff --git a/internal/temporalcli/commands.operator_namespace_test.go b/internal/temporalcli/commands.operator_namespace_test.go index 6effed480..261fe07a0 100644 --- a/internal/temporalcli/commands.operator_namespace_test.go +++ b/internal/temporalcli/commands.operator_namespace_test.go @@ -2,6 +2,7 @@ package temporalcli_test import ( "fmt" + "os" "time" "github.com/temporalio/cli/internal/temporalcli" @@ -206,3 +207,86 @@ func (s *SharedServerSuite) TestUpdateOldAndNewNSArgs() { s.Error(res.Err) s.ContainsOnSameLine(res.Err.Error(), "namespace was provided as both an argument", "and a flag") } + +func (s *SharedServerSuite) TestOperatorNamespace_EnvConfigResolution() { + // Create a test namespace to use in envconfig + testNS := "envconfig-test-namespace" + res := s.Execute( + "operator", "namespace", "create", + "--address", s.Address(), + "-n", testNS, + ) + s.NoError(res.Err) + + // Create temp config file with namespace + f, err := os.CreateTemp("", "temporal-test-*.toml") + s.NoError(err) + defer os.Remove(f.Name()) + + _, err = fmt.Fprintf(f, ` +[profile.default] +address = "%s" +namespace = "%s" +`, s.Address(), testNS) + s.NoError(err) + f.Close() + + // Set environment to use config file + s.CommandHarness.Options.EnvLookup = EnvLookupMap{ + "TEMPORAL_CONFIG_FILE": f.Name(), + } + + // Test 1: Describe should use envconfig namespace (no -n flag) + res = s.Execute( + "operator", "namespace", "describe", + "--output", "json", + ) + s.NoError(res.Err) + var descResp workflowservice.DescribeNamespaceResponse + s.NoError(temporalcli.UnmarshalProtoJSONWithOptions(res.Stdout.Bytes(), &descResp, true)) + s.Equal(testNS, descResp.NamespaceInfo.Name, "Should use namespace from envconfig") + + // Test 2: Update should use envconfig namespace + res = s.Execute( + "operator", "namespace", "update", + "--description", "Updated via envconfig", + "--output", "json", + ) + s.NoError(res.Err) + + // Verify update was applied to correct namespace + res = s.Execute( + "operator", "namespace", "describe", + "--output", "json", + ) + s.NoError(res.Err) + s.NoError(temporalcli.UnmarshalProtoJSONWithOptions(res.Stdout.Bytes(), &descResp, true)) + s.Equal("Updated via envconfig", descResp.NamespaceInfo.Description) + s.Equal(testNS, descResp.NamespaceInfo.Name) + + // Test 3: CLI flag should override envconfig + res = s.Execute( + "operator", "namespace", "describe", + "--output", "json", + "-n", "default", + ) + s.NoError(res.Err) + s.NoError(temporalcli.UnmarshalProtoJSONWithOptions(res.Stdout.Bytes(), &descResp, true)) + s.Equal("default", descResp.NamespaceInfo.Name, "Explicit -n flag should override envconfig") + + // Test 4: Delete should use envconfig namespace + res = s.Execute( + "operator", "namespace", "delete", + "--yes", + ) + s.NoError(res.Err) + + // Verify namespace was deleted + res = s.Execute( + "operator", "namespace", "describe", + "--output", "json", + "-n", testNS, + ) + s.Error(res.Err) + s.Contains(res.Err.Error(), "is not found") +} diff --git a/internal/temporalcli/commands.taskqueue_config.go b/internal/temporalcli/commands.taskqueue_config.go index bd2948ca7..18f330d51 100644 --- a/internal/temporalcli/commands.taskqueue_config.go +++ b/internal/temporalcli/commands.taskqueue_config.go @@ -24,17 +24,17 @@ func (c *TemporalTaskQueueConfigGetCommand) run(cctx *CommandContext, args []str return err } - namespace := c.Parent.Parent.Namespace - if namespace == "" { - return fmt.Errorf("namespace is required") - } - cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) if err != nil { return err } defer cl.Close() + namespace := c.Parent.Parent.Namespace + if namespace == "" { + return fmt.Errorf("namespace is required") + } + // Get the task queue configuration resp, err := cl.WorkflowService().DescribeTaskQueue(cctx, &workflowservice.DescribeTaskQueueRequest{ Namespace: namespace, @@ -69,11 +69,6 @@ func (c *TemporalTaskQueueConfigSetCommand) run(cctx *CommandContext, args []str return err } - namespace := c.Parent.Parent.Namespace - if namespace == "" { - return fmt.Errorf("namespace is required") - } - // Check workflow task queue restrictions if taskQueueType == enums.TASK_QUEUE_TYPE_WORKFLOW { if c.Command.Flags().Changed("queue-rps-limit") || @@ -129,6 +124,11 @@ func (c *TemporalTaskQueueConfigSetCommand) run(cctx *CommandContext, args []str } defer cl.Close() + namespace := c.Parent.Parent.Namespace + if namespace == "" { + return fmt.Errorf("namespace is required") + } + request := &workflowservice.UpdateTaskQueueConfigRequest{ Namespace: namespace, Identity: c.Parent.Parent.Identity, diff --git a/internal/temporalcli/commands.taskqueue_config_test.go b/internal/temporalcli/commands.taskqueue_config_test.go index d6fac4b71..9939844dc 100644 --- a/internal/temporalcli/commands.taskqueue_config_test.go +++ b/internal/temporalcli/commands.taskqueue_config_test.go @@ -2,6 +2,8 @@ package temporalcli_test import ( "encoding/json" + "fmt" + "os" ) type taskQueueConfigType struct { @@ -254,3 +256,72 @@ func (s *SharedServerSuite) TestTaskQueue_Config_Describe_With_Report_Config() { updTime, _ := md["update_time"].(map[string]any) s.NotEmpty(updTime) } + +func (s *SharedServerSuite) TestTaskQueueConfig_EnvConfigNamespace() { + // Create test namespace + testNS := "tq-config-envconfig-test" + res := s.Execute( + "operator", "namespace", "create", + "--address", s.Address(), + "-n", testNS, + ) + s.NoError(res.Err) + + // Create temp config file + f, err := os.CreateTemp("", "temporal-test-*.toml") + s.NoError(err) + defer os.Remove(f.Name()) + + _, err = fmt.Fprintf(f, ` +[profile.default] +address = "%s" +namespace = "%s" +`, s.Address(), testNS) + s.NoError(err) + f.Close() + + // Set environment + s.CommandHarness.Options.EnvLookup = EnvLookupMap{ + "TEMPORAL_CONFIG_FILE": f.Name(), + } + + taskQueue := "test-tq-envconfig-" + s.T().Name() + + // Test 1: Set config without -n flag (should use envconfig) + res = s.Execute( + "task-queue", "config", "set", + "--task-queue", taskQueue, + "--task-queue-type", "activity", + "--queue-rps-limit", "15.0", + "--queue-rps-limit-reason", "envconfig test", + ) + s.NoError(res.Err) + + // Test 2: Get config without -n flag (should use envconfig) + res = s.Execute( + "task-queue", "config", "get", + "--task-queue", taskQueue, + "--task-queue-type", "activity", + "-o", "json", + ) + s.NoError(res.Err) + + var config taskQueueConfigType + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &config)) + s.NotNil(config.QueueRateLimit) + s.NotNil(config.QueueRateLimit.RateLimit) + s.Equal(float32(15.0), config.QueueRateLimit.RateLimit.RequestsPerSecond) + s.Equal("envconfig test", config.QueueRateLimit.Metadata.Reason) + + // Test 3: CLI flag should override envconfig - verify config doesn't exist in default namespace + res = s.Execute( + "task-queue", "config", "get", + "--task-queue", taskQueue, + "--task-queue-type", "activity", + "-n", "default", + ) + s.NoError(res.Err) + // In default namespace, no config was set for this task queue + s.Contains(res.Stdout.String(), "No configuration found for task queue", + "CLI flag should override envconfig and query default namespace") +} From 168728bf8ee61b341096a81bb9ec2f1fe15fef50 Mon Sep 17 00:00:00 2001 From: Stephan Behnke Date: Thu, 18 Dec 2025 10:57:22 -0800 Subject: [PATCH 28/28] Invoke CLI extension (#889) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What was changed CLI now invokes extensions in accordance with [CLI Extensions proposal](https://github.com/temporalio/proposals/blob/master/cli/cli-extensions.md). Note that the help command integration part is going to be in a follow-up PR. ## Why? Allow Temporal CLI Extensions. ## Checklist 1. Closes 2. How was this tested: 3. Any docs updates needed? --- go.mod | 14 +- go.sum | 32 +- internal/temporalcli/commands.extension.go | 179 +++++++++++ .../temporalcli/commands.extension_test.go | 300 ++++++++++++++++++ internal/temporalcli/commands.go | 30 +- .../temporalcli/commands.schedule_test.go | 6 +- 6 files changed, 530 insertions(+), 31 deletions(-) create mode 100644 internal/temporalcli/commands.extension.go create mode 100644 internal/temporalcli/commands.extension_test.go diff --git a/go.mod b/go.mod index cf4032f74..4339f8442 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,8 @@ require ( go.temporal.io/sdk v1.37.0 go.temporal.io/sdk/contrib/envconfig v0.1.0 go.temporal.io/server v1.29.1 - golang.org/x/term v0.32.0 + golang.org/x/term v0.38.0 + golang.org/x/tools v0.40.0 google.golang.org/grpc v1.72.2 google.golang.org/protobuf v1.36.6 gopkg.in/yaml.v3 v3.0.1 @@ -153,13 +154,14 @@ require ( go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.38.0 // indirect + golang.org/x/crypto v0.46.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/net v0.40.0 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.14.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.11.0 // indirect google.golang.org/api v0.228.0 // indirect google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 // indirect diff --git a/go.sum b/go.sum index 6926371e2..56f43b870 100644 --- a/go.sum +++ b/go.sum @@ -417,8 +417,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.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 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= @@ -438,8 +438,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -451,8 +451,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.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -461,8 +461,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -481,15 +481,15 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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= @@ -497,8 +497,8 @@ 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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -515,8 +515,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/temporalcli/commands.extension.go b/internal/temporalcli/commands.extension.go new file mode 100644 index 000000000..870573034 --- /dev/null +++ b/internal/temporalcli/commands.extension.go @@ -0,0 +1,179 @@ +package temporalcli + +import ( + "context" + "fmt" + "os/exec" + "slices" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +const ( + extensionPrefix = "temporal-" + extensionSeparator = "-" // separates command parts in extension name + argDashReplacement = "_" // dashes in args are replaced to avoid ambiguity +) + +// cliArgsToParseForExtension lists CLI flags that should be parsed (validated). +var cliArgsToParseForExtension = map[string]bool{ + "command-timeout": true, +} + +// tryExecuteExtension tries to execute an extension command if the command is not a built-in command. +// It returns an error if the extension command fails, and a boolean indicating whether an extension was executed. +func tryExecuteExtension(cctx *CommandContext, tcmd *TemporalCommand) (error, bool) { + // Find the deepest matching built-in command and remaining args. + foundCmd, remainingArgs, findErr := tcmd.Command.Find(cctx.Options.Args) + + // Check if remaining args include positional args (not just flags). + // If not, a built-in command fully handles this - no extension needed. + hasPosArgs := slices.ContainsFunc(remainingArgs, isPosArg) + if findErr == nil && !hasPosArgs { + return nil, false + } + + // Group args into these lists: + // - cliParseArgs: args to validate (subset of cliPassArgs) + // - cliPassArgs: known CLI args to pass to extension + // - extArgs: args to pass to extension and use for extension lookup + cliParseArgs, cliPassArgs, extArgs := groupArgs(foundCmd, remainingArgs) + + // Search for an extension executable. + cmdPrefix := strings.Split(foundCmd.CommandPath(), " ")[1:] + extPath, extArgs := lookupExtension(cmdPrefix, extArgs) + + // Parse CLI args that need validation. + if len(cliParseArgs) > 0 { + if err := foundCmd.Flags().Parse(cliParseArgs); err != nil { + return err, false + } + } + + if extPath == "" { + return nil, false + } + + // Apply --command-timeout if set. + ctx := cctx.Context + if timeout := tcmd.CommandTimeout.Duration(); timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() + } + + cmd := exec.CommandContext(ctx, extPath, append(cliPassArgs, extArgs...)...) + cmd.Stdin, cmd.Stdout, cmd.Stderr = cctx.Options.Stdin, cctx.Options.Stdout, cctx.Options.Stderr + if err := cmd.Run(); err != nil { + if ctx.Err() != nil { + return fmt.Errorf("program interrupted"), true + } + if _, ok := err.(*exec.ExitError); ok { + return nil, true + } + return fmt.Errorf("extension %s failed: %w", extPath, err), true + } + + return nil, true +} + +func groupArgs(foundCmd *cobra.Command, args []string) (cliParseArgs, cliPassArgs, extArgs []string) { + seenPos := false + for i := 0; i < len(args); i++ { + arg := args[i] + + if isPosArg(arg) { + seenPos = true + extArgs = append(extArgs, arg) + continue + } + + name, hasInline := parseFlagArg(arg) + if f, takesValue := lookupFlag(foundCmd, name); f != nil { + // Known CLI flag: goes to cliPassArgs. + // Flags in cliArgsToParseForExtension also go to cliParseArgs. + shouldParse := cliArgsToParseForExtension[f.Name] + cliPassArgs = append(cliPassArgs, arg) + if shouldParse { + cliParseArgs = append(cliParseArgs, arg) + } + if takesValue && !hasInline && i+1 < len(args) { + i++ + cliPassArgs = append(cliPassArgs, args[i]) + if shouldParse { + cliParseArgs = append(cliParseArgs, args[i]) + } + } + } else { + // Unknown flag: before first positional goes to cliParseArgs (to fail validation), + // after first positional goes to extArgs (passed to extension). + if seenPos { + extArgs = append(extArgs, arg) + } else { + cliParseArgs = append(cliParseArgs, arg) + } + } + } + return +} + +func isPosArg(arg string) bool { + return !strings.HasPrefix(arg, "-") +} + +// parseFlagArg extracts the flag name from a flag argument. +// Handles both --flag=value and --flag forms, returning the name and whether it has an inline value. +func parseFlagArg(arg string) (name string, hasInline bool) { + name, _, hasInline = strings.Cut(strings.TrimLeft(arg, "-"), "=") + return +} + +// lookupFlag finds a flag by name on cmd and all parents. +// It resolves aliases and considers shorthand flags. +func lookupFlag(cmd *cobra.Command, name string) (*pflag.Flag, bool) { + if normalize := cmd.Flags().GetNormalizeFunc(); normalize != nil { + name = string(normalize(cmd.Flags(), name)) + } + for c := cmd; c != nil; c = c.Parent() { + if f := c.Flags().Lookup(name); f != nil { + return f, f.NoOptDefVal == "" + } + if len(name) == 1 { + if f := c.Flags().ShorthandLookup(name); f != nil { + return f, f.NoOptDefVal == "" + } + } + } + return nil, false +} + +// lookupExtension finds an extension executable and returns its path along with +// extArgs with matched positional args removed. +func lookupExtension(cmdPrefix, extArgs []string) (string, []string) { + // Extract positional args from extArgs until we hit an unknown flag. + // We stop at unknown flags because we can't tell if subsequent args are flag values or positionals. + var posArgs []string + for _, arg := range extArgs { + if !isPosArg(arg) { + break + } + // Dashes are converted to underscores so "foo bar-baz" finds "temporal-foo-bar_baz". + posArgs = append(posArgs, strings.ReplaceAll(arg, extensionSeparator, argDashReplacement)) + } + + // Try most-specific to least-specific. + parts := append(cmdPrefix, posArgs...) + for n := len(parts); n > len(cmdPrefix); n-- { + path, err := exec.LookPath(extensionPrefix + strings.Join(parts[:n], extensionSeparator)) + if err != nil { + continue + } + // Remove matched positionals from extArgs (they come first). + matched := n - len(cmdPrefix) + return path, extArgs[matched:] + } + + return "", extArgs +} diff --git a/internal/temporalcli/commands.extension_test.go b/internal/temporalcli/commands.extension_test.go new file mode 100644 index 000000000..46ab15c49 --- /dev/null +++ b/internal/temporalcli/commands.extension_test.go @@ -0,0 +1,300 @@ +package temporalcli_test + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/tools/imports" +) + +// Go code snippets for cross-platform test extensions. +var ( + codeEchoArgs = `fmt.Println("Args:", strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe"), strings.Join(os.Args[1:], " "))` + codeEchoStderr = func(msg string) string { + return fmt.Sprintf(`fmt.Fprintln(os.Stderr, %q)`, msg) + } + codeExit = func(code int) string { + return fmt.Sprintf(`os.Exit(%d)`, code) + } + codeSleep = func(d time.Duration) string { + return fmt.Sprintf(`time.Sleep(%d)`, d) + } + codeCat = `io.Copy(os.Stdout, os.Stdin)` + codeEchoEnv = func(name string) string { + return fmt.Sprintf(`fmt.Println(os.Getenv(%q))`, name) + } +) + +func TestExtension_InvokesRootExtension(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeEchoArgs) + + res := h.Execute("foo") + + assert.Equal(t, "Args: temporal-foo \n", res.Stdout.String()) +} + +func TestExtension_InvokesSubcommandExtension(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo-bar", codeEchoArgs) + + res := h.Execute("foo", "bar") + + assert.Equal(t, "Args: temporal-foo-bar \n", res.Stdout.String()) +} + +func TestExtension_PrefersMostSpecificExtension(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeEchoArgs) + h.createExtension("temporal-foo-bar", codeEchoArgs) + + res := h.Execute("foo", "bar") + + assert.Equal(t, "Args: temporal-foo-bar \n", res.Stdout.String()) +} + +func TestExtension_ConvertsDashToUnderscoreInLookup(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo-bar_baz", codeEchoArgs) + + // Dash in arg is converted to underscore when looking up extension. + res := h.Execute("foo", "bar-baz") + assert.Equal(t, "Args: temporal-foo-bar_baz \n", res.Stdout.String()) + + // Underscore in arg stays as underscore. + res = h.Execute("foo", "bar_baz") + assert.Equal(t, "Args: temporal-foo-bar_baz \n", res.Stdout.String()) +} + +func TestExtension_DoesNotOverrideBuiltinCommand(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-workflow", codeEchoArgs) + h.createExtension("temporal-workflow-list", codeEchoArgs) + + t.Run("root command", func(t *testing.T) { + res := h.Execute("workflow", "--help") + assert.Contains(t, res.Stdout.String(), "Workflow commands perform operations on Workflow Executions") + }) + + t.Run("subcommand", func(t *testing.T) { + res := h.Execute("workflow", "list", "--help") + assert.Contains(t, res.Stdout.String(), "List Workflow Executions") + }) +} + +func TestExtension_Flags(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeEchoArgs) + h.createExtension("temporal-foo-bar", codeEchoArgs) // should never be called + h.createExtension("temporal-foo-json", codeEchoArgs) // should never be called + h.createExtension("temporal-workflow-diagram", codeEchoArgs) + h.createExtension("temporal-workflow-diagram-foo", codeEchoArgs) // should never be called + h.createExtension("temporal-workflow-diagram-json", codeEchoArgs) // should never be called + + cases := []struct { + args string + want string + err string + }{ + // Root extension + + {args: "--no-json-shorthand-payloads foo", want: "temporal-foo --no-json-shorthand-payloads"}, // boolean flag + {args: "--output json foo", want: "temporal-foo --output json"}, + {args: "--output=json foo", want: "temporal-foo --output=json"}, + {args: "-o json foo", want: "temporal-foo -o json"}, // shorthand + {args: "-o=json foo", want: "temporal-foo -o=json"}, + {args: "--unknown-flag value foo", err: "unknown flag"}, // unknown flags before extension fail + {args: "--output invalid foo", want: "temporal-foo --output invalid"}, // invalid value passed through + {args: "--command-timeout 1s foo", want: "temporal-foo --command-timeout 1s"}, // --command-timeout passed through + {args: "--command-timeout invalid foo", err: "invalid argument"}, // --command-timeout invalid + + {args: "foo --output json", want: "temporal-foo --output json"}, // not temporal-foo-json + {args: "foo --output=json", want: "temporal-foo --output=json"}, + {args: "foo -o json", want: "temporal-foo -o json"}, + {args: "foo -o=json", want: "temporal-foo -o=json"}, + {args: "foo -x bar", want: "temporal-foo -x bar"}, // not temporal-foo-x + {args: "foo --output invalid", want: "temporal-foo --output invalid"}, // invalid value passed through + {args: "foo arg1 -x value arg2", want: "temporal-foo arg1 -x value arg2"}, // order preserved + + // Subcommand extension + + {args: "--output json workflow diagram", want: "temporal-workflow-diagram --output json"}, + {args: "--output=json workflow diagram", want: "temporal-workflow-diagram --output=json"}, + {args: "-o json workflow diagram", want: "temporal-workflow-diagram -o json"}, // shorthand + {args: "-o=json workflow diagram", want: "temporal-workflow-diagram -o=json"}, + {args: "--unknown-flag value workflow diagram", err: "unknown flag"}, // unknown flags before extension fail + + {args: "workflow --tls diagram", want: "temporal-workflow-diagram --tls"}, // boolean flag + {args: "workflow --namespace my-ns diagram", want: "temporal-workflow-diagram --namespace my-ns"}, + {args: "workflow --namespace=my-ns diagram", want: "temporal-workflow-diagram --namespace=my-ns"}, + {args: "workflow -n my-ns diagram", want: "temporal-workflow-diagram -n my-ns"}, // shorthand + {args: "workflow -n=my-ns diagram", want: "temporal-workflow-diagram -n=my-ns"}, + {args: "workflow --unknown-flag diagram", err: "unknown flag"}, // unknown flags before extension fail + {args: "workflow --output invalid diagram", want: "temporal-workflow-diagram --output invalid"}, // invalid value passed through + + {args: "workflow diagram --output json", want: "temporal-workflow-diagram --output json"}, // not temporal-workflow-diagram-json + {args: "workflow diagram --output=json", want: "temporal-workflow-diagram --output=json"}, + {args: "workflow diagram -o json", want: "temporal-workflow-diagram -o json"}, // shorthand + {args: "workflow diagram -o=json", want: "temporal-workflow-diagram -o=json"}, + {args: "workflow diagram -x foo", want: "temporal-workflow-diagram -x foo"}, // not temporal-workflow-diagram-foo + {args: "workflow diagram arg1 -x value arg2", want: "temporal-workflow-diagram arg1 -x value arg2"}, // order preserved + {args: "workflow diagram foo --flag value", want: "temporal-workflow-diagram-foo --flag value"}, // nested commands + {args: "workflow diagram --output invalid", want: "temporal-workflow-diagram --output invalid"}, // invalid value passed through + + // Note: Flag aliases are already implicitly tested via other command-specific tests. + } + + for _, c := range cases { + res := h.Execute(strings.Split(c.args, " ")...) + if c.err != "" { + assert.ErrorContains(t, res.Err, c.err) + } else { + assert.Equal(t, "Args: "+c.want+"\n", res.Stdout.String()) + assert.NoError(t, res.Err) + } + } +} + +func TestExtension_PassesStdin(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeCat) + h.Stdin.WriteString("hello from stdin") + + res := h.Execute("foo") + + assert.Equal(t, "hello from stdin", res.Stdout.String()) +} + +func TestExtension_InheritsEnvironmentVariables(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeEchoEnv("TEST_EXT_VAR")) + os.Setenv("TEST_EXT_VAR", "test_value_123") + t.Cleanup(func() { os.Unsetenv("TEST_EXT_VAR") }) + + res := h.Execute("foo") + + assert.Equal(t, "test_value_123\n", res.Stdout.String()) +} + +func TestExtension_RelaysStderr(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeEchoStderr("stderr output")) + + res := h.Execute("foo") + + assert.Empty(t, res.Stdout.String()) + assert.Equal(t, "stderr output\n", res.Stderr.String()) +} + +func TestExtension_RelaysStdoutAndStderr(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", ` + fmt.Fprintln(os.Stdout, "stdout line") + fmt.Fprintln(os.Stderr, "stderr line") + `) + + res := h.Execute("foo") + + assert.Equal(t, "stdout line\n", res.Stdout.String()) + assert.Equal(t, "stderr line\n", res.Stderr.String()) +} + +func TestExtension_FailsOnNonExecutableCommand(t *testing.T) { + h := newExtensionHarness(t) + // Create file without execute permission. + path := filepath.Join(h.binDir, "temporal-foo") + err := os.WriteFile(path, []byte("a text file"), 0644) + require.NoError(t, err) + + res := h.Execute("foo") + + assert.Contains(t, res.Stdout.String(), "Usage:") // help text is shown + assert.EqualError(t, res.Err, "unknown command") +} + +func TestExtension_PassesThroughNonZeroExit(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeEchoArgs, codeExit(42)) + + res := h.Execute("foo") + + assert.Equal(t, "Args: temporal-foo \n", res.Stdout.String()) + assert.NoError(t, res.Err) +} + +func TestExtension_FailsOnCommandTimeout(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeSleep(10*time.Second)) + + res := h.Execute("foo", "--command-timeout", "100ms") + assert.EqualError(t, res.Err, "program interrupted") + + res = h.Execute("foo", "--command-timeout", "invalid") + assert.ErrorContains(t, res.Err, "invalid argument \"invalid\"") +} + +func TestExtension_FailsOnCommandCancellation(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeSleep(10*time.Second)) + go func() { + time.Sleep(100 * time.Millisecond) + h.CancelContext() + }() + + res := h.Execute("foo") + + assert.EqualError(t, res.Err, "program interrupted") +} + +type extensionHarness struct { + *CommandHarness + binDir string +} + +func newExtensionHarness(t *testing.T) *extensionHarness { + t.Helper() + + binDir := t.TempDir() + oldPath := os.Getenv("PATH") + os.Setenv("PATH", binDir+string(os.PathListSeparator)+oldPath) + t.Cleanup(func() { os.Setenv("PATH", oldPath) }) + + return &extensionHarness{ + CommandHarness: NewCommandHarness(t), + binDir: binDir, + } +} + +func (h *extensionHarness) createExtension(name string, code ...string) string { + h.t.Helper() + + // Wrap code in main function. + source := fmt.Sprintf("package main\n\nfunc main() {\n%s\n}\n", strings.Join(code, "\n")) + + // Run goimports to resolve imports. + formatted, err := imports.Process("main.go", []byte(source), nil) + require.NoError(h.t, err, "Failed to process imports for %s:\n%s", name, source) + + // Write source file. + srcPath := filepath.Join(h.binDir, name+".go") + require.NoError(h.t, os.WriteFile(srcPath, formatted, 0644)) + + // Build executable. + binPath := filepath.Join(h.binDir, name) + if runtime.GOOS == "windows" { + binPath += ".exe" + } + cmd := exec.Command("go", "build", "-o", binPath, srcPath) + output, err := cmd.CombinedOutput() + require.NoError(h.t, err, "Failed to compile %s: %s\nSource:\n%s", name, output, formatted) + + return binPath +} diff --git a/internal/temporalcli/commands.go b/internal/temporalcli/commands.go index 135cb113f..acf98142b 100644 --- a/internal/temporalcli/commands.go +++ b/internal/temporalcli/commands.go @@ -61,7 +61,16 @@ type CommandContext struct { CurrentCommand *cobra.Command } +type IOStreams struct { + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer +} + type CommandOptions struct { + // IOStreams defaults to OS values. + IOStreams + // If empty, assumed to be os.Args[1:] Args []string // Deprecated `--env` and `--env-file` approach @@ -69,11 +78,6 @@ type CommandOptions struct { // If nil, [envconfig.EnvLookupOS] is used. EnvLookup envconfig.EnvLookup - // These three fields below default to OS values - Stdin io.Reader - Stdout io.Writer - Stderr io.Writer - // Defaults to logging error then os.Exit(1) Fail func(error) @@ -344,10 +348,22 @@ func Execute(ctx context.Context, options CommandOptions) { defer cancel() if err == nil { - // We have a context; let's actually run the command. cmd := NewTemporalCommand(cctx) cmd.Command.SetArgs(cctx.Options.Args) - err = cmd.Command.ExecuteContext(cctx) + cmd.Command.SetOut(cctx.Options.Stdout) + cmd.Command.SetErr(cctx.Options.Stderr) + + // Try extension first. + err, cctx.ActuallyRanCommand = tryExecuteExtension(cctx, cmd) + if err != nil { + cctx.Options.Fail(err) + return + } + + // Run builtin command if no extension handled the command. + if !cctx.ActuallyRanCommand { + err = cmd.Command.ExecuteContext(cctx) + } } if err != nil { diff --git a/internal/temporalcli/commands.schedule_test.go b/internal/temporalcli/commands.schedule_test.go index d5ec1baf9..d828e55ea 100644 --- a/internal/temporalcli/commands.schedule_test.go +++ b/internal/temporalcli/commands.schedule_test.go @@ -28,8 +28,10 @@ func (s *SharedServerSuite) createSchedule(args ...string) (schedId, schedWfId s ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() options := temporalcli.CommandOptions{ - Stdout: io.Discard, - Stderr: io.Discard, + IOStreams: temporalcli.IOStreams{ + Stdout: io.Discard, + Stderr: io.Discard, + }, Args: []string{ "schedule", "delete", "--address", s.Address(),