From f7d51449fec7a47d077a8e28362e8e5524fa8864 Mon Sep 17 00:00:00 2001 From: Sean Kane Date: Wed, 23 Jul 2025 21:26:09 +0200 Subject: [PATCH 1/5] Update next-server branch with changes in main (#834) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What was changed Updating `next-server` branch with changes to `main` ## Why? Consistency ## Checklist 1. Closes: NA 2. How was this tested: NA 3. Any docs updates needed? No --------- Co-authored-by: Andrew Yuan Co-authored-by: Chetan Gowda Co-authored-by: Maciej Dudkowski Co-authored-by: Carly de Frondeville --- .github/workflows/trigger-docs.yml | 2 +- .gitignore | 3 + CONTRIBUTING.md | 15 + Dockerfile | 33 +- README.md | 11 + temporalcli/commands.activity.go | 12 + temporalcli/commands.activity_test.go | 10 + temporalcli/commands.gen.go | 111 +++-- temporalcli/commands.workflow_reset.go | 14 +- .../commands.workflow_reset_update_options.go | 75 ++++ ...ands.workflow_reset_update_options_test.go | 391 ++++++++++++++++++ temporalcli/commandsgen/code.go | 4 +- temporalcli/commandsgen/commands.yml | 155 ++++--- temporalcli/commandsgen/docs.go | 9 +- temporalcli/commandsgen/parse.go | 1 + 15 files changed, 729 insertions(+), 117 deletions(-) create mode 100644 temporalcli/commands.workflow_reset_update_options.go create mode 100644 temporalcli/commands.workflow_reset_update_options_test.go diff --git a/.github/workflows/trigger-docs.yml b/.github/workflows/trigger-docs.yml index 2013a0177..8b62778bf 100644 --- a/.github/workflows/trigger-docs.yml +++ b/.github/workflows/trigger-docs.yml @@ -45,7 +45,7 @@ jobs: run: | gh workflow run update-cli-docs.yml \ -R temporalio/documentation \ - -r cli-docs-autoupdate \ + -r main \ -f cli_release_tag="${{ github.ref_name }}" \ -f commit_author="${{ steps.get_user.outputs.GIT_NAME }}" \ -f commit_author_email="${{ steps.get_user.outputs.GIT_EMAIL }}" \ diff --git a/.gitignore b/.gitignore index 67040f2e7..14b4adaac 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ /temporal /temporal.exe +# Goreleaser output +/dist + # Used by IDE /.idea /.vscode diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a35a33514..122c7140f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,3 +46,18 @@ Note that inclusion of space characters in the value supplied via `-ldflags` is 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 + +## Building Docker image + +Docker image build requires [Goreleaser](https://goreleaser.com/) to build the binaries first, although it doesn't use +Goreleaser for the Docker image itself. + +First, run the Goreleaser build: + + goreleaser build --snapshot --clean + +Then, run the Docker build using the following command: + + docker build --tag temporalio/temporal:snapshot --platform= . + +Currently only `linux/amd64` and `linux/arm64` platforms are supported. diff --git a/Dockerfile b/Dockerfile index 898552833..05c3938a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,12 @@ -FROM golang:1.24-bookworm AS builder - -WORKDIR /app - -# Copy everything -COPY . ./ - -# Build -RUN go build ./cmd/temporal - -# Use slim container for running -FROM debian:bookworm-slim -RUN set -x && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ - ca-certificates && \ - rm -rf /var/lib/apt/lists/* - -# Copy binary -COPY --from=builder /app/temporal /app/temporal - -# Set CLI as primary entrypoint -ENTRYPOINT ["/app/temporal"] +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 +ARG TARGETARCH +RUN apk add --no-cache ca-certificates +COPY --from=dist /dist/$TARGETARCH/temporal /usr/local/bin/temporal +RUN adduser -u 1000 -D temporal +USER temporal + +ENTRYPOINT ["temporal"] diff --git a/README.md b/README.md index fdf4f7903..79608ce3f 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,17 @@ Reference [the documentation](https://docs.temporal.io/cli) for detailed install 2. Extract the downloaded archive. 3. Add the `temporal` binary to your `PATH` (`temporal.exe` for Windows). +### Run via Docker + +[Temporal CLI on DockerHub](https://hub.docker.com/r/temporalio/temporal) + + docker run --rm temporalio/temporal --help + +Note that for dev server to be accessible from host system, it needs to listen on external IP and the ports need to be forwarded: + + docker run --rm -p 7233:7233 -p 8233:8233 temporalio/temporal:latest server start-dev --ip 0.0.0.0 + # UI is now accessible from host at http://localhost:8233/ + ### Build 1. Install [Go](https://go.dev/) diff --git a/temporalcli/commands.activity.go b/temporalcli/commands.activity.go index b63561691..f2c26b9ca 100644 --- a/temporalcli/commands.activity.go +++ b/temporalcli/commands.activity.go @@ -208,6 +208,10 @@ func (c *TemporalActivityPauseCommand) run(cctx *CommandContext, args []string) Identity: c.Identity, } + if c.ActivityId != "" && c.ActivityType != "" { + return fmt.Errorf("either Activity Type or Activity Id, but not both") + } + if c.ActivityType != "" { request.Activity = &workflowservice.PauseActivityRequest_Type{Type: c.ActivityType} } else if c.ActivityId != "" { @@ -261,6 +265,10 @@ func (c *TemporalActivityUnpauseCommand) run(cctx *CommandContext, args []string Identity: c.Identity, } + if c.ActivityId != "" && c.ActivityType != "" { + return fmt.Errorf("either Activity Type or Activity Id, but not both") + } + if c.ActivityType != "" { request.Activity = &workflowservice.UnpauseActivityRequest_Type{Type: c.ActivityType} } else if c.ActivityId != "" { @@ -318,6 +326,10 @@ func (c *TemporalActivityResetCommand) run(cctx *CommandContext, args []string) ResetHeartbeat: c.ResetHeartbeats, } + if c.ActivityId != "" && c.ActivityType != "" { + return fmt.Errorf("either Activity Type or Activity Id, but not both") + } + if c.ActivityType != "" { request.Activity = &workflowservice.ResetActivityRequest_Type{Type: c.ActivityType} } else if c.ActivityId != "" { diff --git a/temporalcli/commands.activity_test.go b/temporalcli/commands.activity_test.go index 2c258d19f..b56dc3130 100644 --- a/temporalcli/commands.activity_test.go +++ b/temporalcli/commands.activity_test.go @@ -247,6 +247,16 @@ func (s *SharedServerSuite) TestActivityCommandFailed_NoActivityTpeOrId() { } } +func (s *SharedServerSuite) TestActivityCommandFailed_BothActivityTpeOrId() { + run := s.waitActivityStarted() + + commands := []string{"pause", "unpause", "reset"} + for _, command := range commands { + res := sendActivityCommand(command, run, s, "--activity-id", activityId, "--activity-type", activityType) + s.Error(res.Err) + } +} + func (s *SharedServerSuite) TestActivityReset() { run := s.waitActivityStarted() diff --git a/temporalcli/commands.gen.go b/temporalcli/commands.gen.go index a79941694..ef89a3b87 100644 --- a/temporalcli/commands.gen.go +++ b/temporalcli/commands.gen.go @@ -314,6 +314,20 @@ func (v *QueryModifiersOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSe f.Var(&v.RejectCondition, "reject-condition", "Optional flag for rejecting Queries based on Workflow state. Accepted values: not_open, not_completed_cleanly.") } +type WorkflowUpdateOptionsOptions struct { + VersioningOverrideBehavior StringEnum + VersioningOverrideDeploymentName string + VersioningOverrideBuildId string +} + +func (v *WorkflowUpdateOptionsOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSet) { + v.VersioningOverrideBehavior = NewStringEnum([]string{"pinned", "auto_upgrade"}, "") + f.Var(&v.VersioningOverrideBehavior, "versioning-override-behavior", "Override the versioning behavior of a Workflow. Accepted values: pinned, auto_upgrade. Required.") + _ = cobra.MarkFlagRequired(f, "versioning-override-behavior") + f.StringVar(&v.VersioningOverrideDeploymentName, "versioning-override-deployment-name", "", "When overriding to a `pinned` behavior, specifies the Deployment Name of the version to target.") + f.StringVar(&v.VersioningOverrideBuildId, "versioning-override-build-id", "", "When overriding to a `pinned` behavior, specifies the Build ID of the version to target.") +} + type TemporalCommand struct { Command cobra.Command Env string @@ -489,14 +503,14 @@ func NewTemporalActivityPauseCommand(cctx *CommandContext, parent *TemporalActiv s.Command.Use = "pause [flags]" s.Command.Short = "Pause an Activity" if hasHighlighting { - s.Command.Long = "Pause an Activity.\n\nIf the Activity is not currently running (e.g. because it previously\nfailed), it will not be run again until it is unpaused.\n\nHowever, if the Activity is currently running, it will run to completion.\nIf the Activity is on its last retry attempt and fails, the failure will\nbe returned to the caller, just as if the Activity had not been paused.\n\nActivities can be specified by their Activity ID or Activity Type.\nOne of those parameters must be provided. If both are provided - Activity\nType will be used, and Activity ID will be ignored.\n\nSpecify the Activity and Workflow IDs:\n\n\x1b[1mtemporal activity pause \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId\x1b[0m" + s.Command.Long = "Pause an Activity.\n\nIf the Activity is not currently running (e.g. because it previously\nfailed), it will not be run again until it is unpaused.\n\nHowever, if the Activity is currently running, it will run until the next \ntime it fails, completes, or times out, at which point the pause will kick in.\n\nIf the Activity is on its last retry attempt and fails, the failure will\nbe returned to the caller, just as if the Activity had not been paused.\n\nActivities should be specified either by their Activity ID or Activity Type.\n\nFor example, specify the Activity and Workflow IDs like this:\n\n\x1b[1mtemporal activity pause \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId\x1b[0m\n\nTo later unpause the activity, see unpause. You may also want to \nreset the activity to unpause it while also starting it from the beginning." } else { - s.Command.Long = "Pause an Activity.\n\nIf the Activity is not currently running (e.g. because it previously\nfailed), it will not be run again until it is unpaused.\n\nHowever, if the Activity is currently running, it will run to completion.\nIf the Activity is on its last retry attempt and fails, the failure will\nbe returned to the caller, just as if the Activity had not been paused.\n\nActivities can be specified by their Activity ID or Activity Type.\nOne of those parameters must be provided. If both are provided - Activity\nType will be used, and Activity ID will be ignored.\n\nSpecify the Activity and Workflow IDs:\n\n```\ntemporal activity pause \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId\n```" + s.Command.Long = "Pause an Activity.\n\nIf the Activity is not currently running (e.g. because it previously\nfailed), it will not be run again until it is unpaused.\n\nHowever, if the Activity is currently running, it will run until the next \ntime it fails, completes, or times out, at which point the pause will kick in.\n\nIf the Activity is on its last retry attempt and fails, the failure will\nbe returned to the caller, just as if the Activity had not been paused.\n\nActivities should be specified either by their Activity ID or Activity Type.\n\nFor example, specify the Activity and Workflow IDs like this:\n\n```\ntemporal activity pause \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId\n```\n\nTo later unpause the activity, see unpause. You may also want to \nreset the activity to unpause it while also starting it from the beginning." } s.Command.Args = cobra.NoArgs - s.Command.Flags().StringVarP(&s.ActivityId, "activity-id", "a", "", "Activity ID to pause.") - s.Command.Flags().StringVarP(&s.ActivityType, "activity-type", "g", "", "Activity Type to pause.") - s.Command.Flags().StringVar(&s.Identity, "identity", "", "Identity of the user submitting this request.") + 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.") + 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) { if err := s.run(cctx, args); err != nil { @@ -524,16 +538,16 @@ func NewTemporalActivityResetCommand(cctx *CommandContext, parent *TemporalActiv s.Command.Use = "reset [flags]" s.Command.Short = "Reset an Activity" if hasHighlighting { - s.Command.Long = "Resetting an activity resets both the number of attempts and the activity\ntimeout.\n\nIf activity is paused and 'keep_paused' flag is not provided - it will be\nunpaused.\nIf activity is paused and 'keep_paused' flag is provided - it will stay\npaused.\nIf activity is waiting for the retry, is will be rescheduled immediately.\nIf the 'reset_heartbeats' flag is set, the activity heartbeat timer and\nheartbeats will be reset.\n\nActivities can be specified by their Activity ID or Activity Type.\nOne of those parameters must be provided. If both are provided - Activity\nType will be used, and Activity ID will be ignored.\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" } else { - s.Command.Long = "Resetting an activity resets both the number of attempts and the activity\ntimeout.\n\nIf activity is paused and 'keep_paused' flag is not provided - it will be\nunpaused.\nIf activity is paused and 'keep_paused' flag is provided - it will stay\npaused.\nIf activity is waiting for the retry, is will be rescheduled immediately.\nIf the 'reset_heartbeats' flag is set, the activity heartbeat timer and\nheartbeats will be reset.\n\nActivities can be specified by their Activity ID or Activity Type.\nOne of those parameters must be provided. If both are provided - Activity\nType will be used, and Activity ID will be ignored.\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```" } s.Command.Args = cobra.NoArgs - s.Command.Flags().StringVarP(&s.ActivityId, "activity-id", "a", "", "Activity ID to pause.") - s.Command.Flags().StringVarP(&s.ActivityType, "activity-type", "g", "", "Activity Type to pause.") - s.Command.Flags().StringVar(&s.Identity, "identity", "", "Identity of the user submitting this request.") - s.Command.Flags().BoolVar(&s.KeepPaused, "keep-paused", false, "If activity was paused - it will stay paused.") - s.Command.Flags().BoolVar(&s.ResetHeartbeats, "reset-heartbeats", false, "Reset the Activity's heartbeat.") + 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().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.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { @@ -563,14 +577,14 @@ func NewTemporalActivityUnpauseCommand(cctx *CommandContext, parent *TemporalAct s.Command.Use = "unpause [flags]" s.Command.Short = "Unpause an Activity" if hasHighlighting { - 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 \x1b[1m--reset-attempts\x1b[0m 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, \x1b[1m--reset-attempts\x1b[0m will allow the\nActivity to be retried another N times after unpausing.\n\nUse \x1b[1m--reset-heartbeat\x1b[0m 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. If both are provided - Activity\nType will be used, and Activity ID will be ignored.\n\nActivities can be unpaused in bulk via a visibility Query list filter:\n\n\x1b[1mtemporal activity unpause \\\n --query YourQuery \\\n --reason YourReasonForTermination\x1b[0m\n\n\nSpecify the Activity ID or Type and Workflow IDs:\n\n\x1b[1mtemporal activity unpause \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId\n --reset-attempts\n --reset-heartbeats\x1b[0m" + 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 \x1b[1m--reset-attempts\x1b[0m 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, \x1b[1m--reset-attempts\x1b[0m will allow the\nActivity to be retried another N times after unpausing.\n\nUse \x1b[1m--reset-heartbeat\x1b[0m 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\x1b[1mtemporal activity unpause \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId\n --reset-attempts\n --reset-heartbeats\x1b[0m\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\x1b[1mtemporal activity unpause \\\n --query 'TemporalPauseInfo=\"property:activityType=Foo\"'\x1b[0m" } else { - 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. If both are provided - Activity\nType will be used, and Activity ID will be ignored.\n\nActivities can be unpaused in bulk via a visibility Query list filter:\n\n```\ntemporal activity unpause \\\n --query YourQuery \\\n --reason YourReasonForTermination\n```\n\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```" + 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", "", "Activity ID to unpause. Can only be used without --query.") - s.Command.Flags().StringVarP(&s.ActivityType, "activity-type", "g", "", "Activity Type to unpause.") - s.Command.Flags().StringVar(&s.Identity, "identity", "", "Identity of the user submitting this request.") + 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().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.") @@ -609,9 +623,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 Activity options. Specify the Activity and Workflow IDs, and\noptions you want to update.\nUpdates are incremental, only changing the specified options.\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" + 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." } else { - s.Command.Long = "Update Activity options. Specify the Activity and Workflow IDs, and\noptions you want to update.\nUpdates are incremental, only changing the specified options.\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```" + 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.Args = cobra.NoArgs s.Command.Flags().StringVar(&s.ActivityId, "activity-id", "", "Activity ID. Required.") @@ -762,9 +776,9 @@ func NewTemporalConfigCommand(cctx *CommandContext, parent *TemporalCommand) *Te s.Command.Use = "config" s.Command.Short = "Manage config files (EXPERIMENTAL)" if hasHighlighting { - s.Command.Long = "Config files are TOML files that contain profiles, with each profile\ncontaining configuration for connecting to Temporal. \n\n\x1b[1mtemporal config set \\\n --prop address \\\n --value us-west-2.aws.api.temporal.io:7233\x1b[0m\n\nThe default config file path is \x1b[1m$CONFIG_PATH/temporalio/temporal.toml\x1b[0m where\n\x1b[1m$CONFIG_PATH\x1b[0m is defined as \x1b[1m$HOME/.config\x1b[0m on Unix,\n\x1b[1m$HOME/Library/Application Support\x1b[0m on macOS, and \x1b[1m%AppData%\x1b[0m on Windows.\nThis can be overridden with the \x1b[1mTEMPORAL_CONFIG_FILE\x1b[0m environment\nvariable or \x1b[1m--config-file\x1b[0m.\n\nThe default profile is \x1b[1mdefault\x1b[0m. This can be overridden with the\n\x1b[1mTEMPORAL_PROFILE\x1b[0m environment variable or \x1b[1m--profile\x1b[0m." + s.Command.Long = "Config files are TOML files that contain profiles, with each profile\ncontaining configuration for connecting to Temporal.\n\n\x1b[1mtemporal config set \\\n --prop address \\\n --value us-west-2.aws.api.temporal.io:7233\x1b[0m\n\nThe default config file path is \x1b[1m$CONFIG_PATH/temporalio/temporal.toml\x1b[0m where\n\x1b[1m$CONFIG_PATH\x1b[0m is defined as \x1b[1m$HOME/.config\x1b[0m on Unix,\n\x1b[1m$HOME/Library/Application Support\x1b[0m on macOS, and \x1b[1m%AppData%\x1b[0m on Windows.\nThis can be overridden with the \x1b[1mTEMPORAL_CONFIG_FILE\x1b[0m environment\nvariable or \x1b[1m--config-file\x1b[0m.\n\nThe default profile is \x1b[1mdefault\x1b[0m. This can be overridden with the\n\x1b[1mTEMPORAL_PROFILE\x1b[0m environment variable or \x1b[1m--profile\x1b[0m." } else { - s.Command.Long = "Config files are TOML files that contain profiles, with each profile\ncontaining configuration for connecting to Temporal. \n\n```\ntemporal config set \\\n --prop address \\\n --value us-west-2.aws.api.temporal.io:7233\n```\n\nThe default config file path is `$CONFIG_PATH/temporalio/temporal.toml` where\n`$CONFIG_PATH` is defined as `$HOME/.config` on Unix,\n`$HOME/Library/Application Support` on macOS, and `%AppData%` on Windows.\nThis can be overridden with the `TEMPORAL_CONFIG_FILE` environment\nvariable or `--config-file`.\n\nThe default profile is `default`. This can be overridden with the\n`TEMPORAL_PROFILE` environment variable or `--profile`." + s.Command.Long = "Config files are TOML files that contain profiles, with each profile\ncontaining configuration for connecting to Temporal.\n\n```\ntemporal config set \\\n --prop address \\\n --value us-west-2.aws.api.temporal.io:7233\n```\n\nThe default config file path is `$CONFIG_PATH/temporalio/temporal.toml` where\n`$CONFIG_PATH` is defined as `$HOME/.config` on Unix,\n`$HOME/Library/Application Support` on macOS, and `%AppData%` on Windows.\nThis can be overridden with the `TEMPORAL_CONFIG_FILE` environment\nvariable or `--config-file`.\n\nThe default profile is `default`. This can be overridden with the\n`TEMPORAL_PROFILE` environment variable or `--profile`." } s.Command.Args = cobra.NoArgs s.Command.AddCommand(&NewTemporalConfigDeleteCommand(cctx, &s).Command) @@ -3395,8 +3409,7 @@ type TemporalWorkflowResetCommand struct { func NewTemporalWorkflowResetCommand(cctx *CommandContext, parent *TemporalWorkflowCommand) *TemporalWorkflowResetCommand { var s TemporalWorkflowResetCommand s.Parent = parent - s.Command.DisableFlagsInUseLine = true - s.Command.Use = "reset [flags]" + s.Command.Use = "reset" s.Command.Short = "Move Workflow Execution history point" if hasHighlighting { s.Command.Long = "Reset a Workflow Execution so it can resume from a point in its Event History\nwithout losing its progress up to that point:\n\n\x1b[1mtemporal workflow reset \\\n --workflow-id YourWorkflowId \\\n --event-id YourLastEvent\x1b[0m\n\nStart from where the Workflow Execution last continued as new:\n\n\x1b[1mtemporal workflow reset \\\n --workflow-id YourWorkflowId \\\n --type LastContinuedAsNew\x1b[0m\n\nFor batch resets, limit your resets to FirstWorkflowTask, LastWorkflowTask, or\nBuildId. Do not use Workflow IDs, run IDs, or event IDs with this command.\n\nVisit https://docs.temporal.io/visibility to read more about Search\nAttributes and Query creation." @@ -3404,21 +3417,45 @@ func NewTemporalWorkflowResetCommand(cctx *CommandContext, parent *TemporalWorkf s.Command.Long = "Reset a Workflow Execution so it can resume from a point in its Event History\nwithout losing its progress up to that point:\n\n```\ntemporal workflow reset \\\n --workflow-id YourWorkflowId \\\n --event-id YourLastEvent\n```\n\nStart from where the Workflow Execution last continued as new:\n\n```\ntemporal workflow reset \\\n --workflow-id YourWorkflowId \\\n --type LastContinuedAsNew\n```\n\nFor batch resets, limit your resets to FirstWorkflowTask, LastWorkflowTask, or\nBuildId. Do not use Workflow IDs, run IDs, or event IDs with this command.\n\nVisit https://docs.temporal.io/visibility to read more about Search\nAttributes and Query creation." } s.Command.Args = cobra.NoArgs - s.Command.Flags().StringVarP(&s.WorkflowId, "workflow-id", "w", "", "Workflow ID. Required for non-batch reset operations.") - s.Command.Flags().StringVarP(&s.RunId, "run-id", "r", "", "Run ID.") - s.Command.Flags().IntVarP(&s.EventId, "event-id", "e", 0, "Event ID to reset to. Event must occur after `WorkflowTaskStarted`. `WorkflowTaskCompleted`, `WorkflowTaskFailed`, etc. are valid.") - s.Command.Flags().StringVar(&s.Reason, "reason", "", "Reason for reset. Required.") - _ = cobra.MarkFlagRequired(s.Command.Flags(), "reason") + s.Command.AddCommand(&NewTemporalWorkflowResetWithWorkflowUpdateOptionsCommand(cctx, &s).Command) + s.Command.PersistentFlags().StringVarP(&s.WorkflowId, "workflow-id", "w", "", "Workflow ID. Required for non-batch reset operations.") + s.Command.PersistentFlags().StringVarP(&s.RunId, "run-id", "r", "", "Run ID.") + s.Command.PersistentFlags().IntVarP(&s.EventId, "event-id", "e", 0, "Event ID to reset to. Event must occur after `WorkflowTaskStarted`. `WorkflowTaskCompleted`, `WorkflowTaskFailed`, etc. are valid.") + s.Command.PersistentFlags().StringVar(&s.Reason, "reason", "", "Reason for reset. Required.") + _ = cobra.MarkFlagRequired(s.Command.PersistentFlags(), "reason") s.ReapplyType = NewStringEnum([]string{"All", "Signal", "None"}, "All") - s.Command.Flags().Var(&s.ReapplyType, "reapply-type", "Types of events to re-apply after reset point. Accepted values: All, Signal, None.") - _ = s.Command.Flags().MarkDeprecated("reapply-type", "Use --reapply-exclude instead.") + s.Command.PersistentFlags().Var(&s.ReapplyType, "reapply-type", "Types of events to re-apply after reset point. Accepted values: All, Signal, None.") + _ = s.Command.PersistentFlags().MarkDeprecated("reapply-type", "Use --reapply-exclude instead.") s.ReapplyExclude = NewStringEnumArray([]string{"All", "Signal", "Update"}, []string{}) - s.Command.Flags().Var(&s.ReapplyExclude, "reapply-exclude", "Exclude these event types from re-application. Accepted values: All, Signal, Update.") + s.Command.PersistentFlags().Var(&s.ReapplyExclude, "reapply-exclude", "Exclude these event types from re-application. Accepted values: All, Signal, Update.") s.Type = NewStringEnum([]string{"FirstWorkflowTask", "LastWorkflowTask", "LastContinuedAsNew", "BuildId"}, "") - s.Command.Flags().VarP(&s.Type, "type", "t", "The event type for the reset. Accepted values: FirstWorkflowTask, LastWorkflowTask, LastContinuedAsNew, BuildId.") - s.Command.Flags().StringVar(&s.BuildId, "build-id", "", "A Build ID. Use only with the BuildId `--type`. Resets the first Workflow task processed by this ID. By default, this reset may be in a prior run, earlier than a Continue as New point.") - s.Command.Flags().StringVarP(&s.Query, "query", "q", "", "Content for an SQL-like `QUERY` List Filter.") - s.Command.Flags().BoolVarP(&s.Yes, "yes", "y", false, "Don't prompt to confirm. Only allowed when `--query` is present.") + s.Command.PersistentFlags().VarP(&s.Type, "type", "t", "The event type for the reset. Accepted values: FirstWorkflowTask, LastWorkflowTask, LastContinuedAsNew, BuildId.") + s.Command.PersistentFlags().StringVar(&s.BuildId, "build-id", "", "A Build ID. Use only with the BuildId `--type`. Resets the first Workflow task processed by this ID. By default, this reset may be in a prior run, earlier than a Continue as New point.") + s.Command.PersistentFlags().StringVarP(&s.Query, "query", "q", "", "Content for an SQL-like `QUERY` List Filter.") + s.Command.PersistentFlags().BoolVarP(&s.Yes, "yes", "y", false, "Don't prompt to confirm. Only allowed when `--query` is present.") + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + +type TemporalWorkflowResetWithWorkflowUpdateOptionsCommand struct { + Parent *TemporalWorkflowResetCommand + Command cobra.Command + WorkflowUpdateOptionsOptions +} + +func NewTemporalWorkflowResetWithWorkflowUpdateOptionsCommand(cctx *CommandContext, parent *TemporalWorkflowResetCommand) *TemporalWorkflowResetWithWorkflowUpdateOptionsCommand { + var s TemporalWorkflowResetWithWorkflowUpdateOptionsCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "with-workflow-update-options [flags]" + s.Command.Short = "Update options on reset workflow" + s.Command.Long = "Run Workflow Update Options atomically after the Workflow is reset.\nWorkflows selected by the reset command are forwarded onto the subcommand." + s.Command.Args = cobra.NoArgs + s.WorkflowUpdateOptionsOptions.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) @@ -3654,9 +3691,9 @@ func NewTemporalWorkflowStartUpdateWithStartCommand(cctx *CommandContext, parent s.Command.Use = "start-update-with-start [flags]" s.Command.Short = "Send an Update-With-Start and wait for it to be accepted or rejected (Experimental)" if hasHighlighting { - s.Command.Long = "Send a message to a Workflow Execution to invoke an Update handler, and wait for\nthe update to be accepted or rejected. If the Workflow Execution is not running, \nthen a new workflow execution is started and the update is sent.\n\nExperimental.\n\n\x1b[1mtemporal workflow start-update-with-start \\\n --update-name YourUpdate \\\n --update-input '{\"update-key\": \"update-value\"}' \\\n --update-wait-for-stage accepted \\\n --workflow-id YourWorkflowId \\\n --type YourWorkflowType \\\n --task-queue YourTaskQueue \\\n --id-conflict-policy Fail \\\n --input '{\"wf-key\": \"wf-value\"}'\x1b[0m" + s.Command.Long = "Send a message to a Workflow Execution to invoke an Update handler, and wait for\nthe update to be accepted or rejected. If the Workflow Execution is not running,\nthen a new workflow execution is started and the update is sent.\n\nExperimental.\n\n\x1b[1mtemporal workflow start-update-with-start \\\n --update-name YourUpdate \\\n --update-input '{\"update-key\": \"update-value\"}' \\\n --update-wait-for-stage accepted \\\n --workflow-id YourWorkflowId \\\n --type YourWorkflowType \\\n --task-queue YourTaskQueue \\\n --id-conflict-policy Fail \\\n --input '{\"wf-key\": \"wf-value\"}'\x1b[0m" } else { - s.Command.Long = "Send a message to a Workflow Execution to invoke an Update handler, and wait for\nthe update to be accepted or rejected. If the Workflow Execution is not running, \nthen a new workflow execution is started and the update is sent.\n\nExperimental.\n\n```\ntemporal workflow start-update-with-start \\\n --update-name YourUpdate \\\n --update-input '{\"update-key\": \"update-value\"}' \\\n --update-wait-for-stage accepted \\\n --workflow-id YourWorkflowId \\\n --type YourWorkflowType \\\n --task-queue YourTaskQueue \\\n --id-conflict-policy Fail \\\n --input '{\"wf-key\": \"wf-value\"}'\n```" + s.Command.Long = "Send a message to a Workflow Execution to invoke an Update handler, and wait for\nthe update to be accepted or rejected. If the Workflow Execution is not running,\nthen a new workflow execution is started and the update is sent.\n\nExperimental.\n\n```\ntemporal workflow start-update-with-start \\\n --update-name YourUpdate \\\n --update-input '{\"update-key\": \"update-value\"}' \\\n --update-wait-for-stage accepted \\\n --workflow-id YourWorkflowId \\\n --type YourWorkflowType \\\n --task-queue YourTaskQueue \\\n --id-conflict-policy Fail \\\n --input '{\"wf-key\": \"wf-value\"}'\n```" } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVar(&s.UpdateName, "update-name", "", "Update name. Required. Aliased as \"--update-type\".") diff --git a/temporalcli/commands.workflow_reset.go b/temporalcli/commands.workflow_reset.go index a4284591b..47112a2c0 100644 --- a/temporalcli/commands.workflow_reset.go +++ b/temporalcli/commands.workflow_reset.go @@ -10,6 +10,7 @@ import ( "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" @@ -66,7 +67,10 @@ func (c *TemporalWorkflowResetCommand) validateBatchResetArguments() error { return nil } func (c *TemporalWorkflowResetCommand) doWorkflowReset(cctx *CommandContext, cl client.Client) error { + return c.doWorkflowResetWithPostOps(cctx, cl, nil) +} +func (c *TemporalWorkflowResetCommand) doWorkflowResetWithPostOps(cctx *CommandContext, cl client.Client, postOps []*workflow.PostResetOperation) error { var err error resetBaseRunID := c.RunId eventID := int64(c.EventId) @@ -94,6 +98,7 @@ func (c *TemporalWorkflowResetCommand) doWorkflowReset(cctx *CommandContext, cl WorkflowTaskFinishEventId: eventID, ResetReapplyType: reapplyType, ResetReapplyExcludeTypes: reapplyExcludes, + PostResetOperations: postOps, }) if err != nil { return fmt.Errorf("failed to reset workflow: %w", err) @@ -108,6 +113,10 @@ func (c *TemporalWorkflowResetCommand) doWorkflowReset(cctx *CommandContext, cl } func (c *TemporalWorkflowResetCommand) runBatchReset(cctx *CommandContext, cl client.Client) error { + return c.runBatchResetWithPostOps(cctx, cl, nil) +} + +func (c *TemporalWorkflowResetCommand) runBatchResetWithPostOps(cctx *CommandContext, cl client.Client, postOps []*workflow.PostResetOperation) error { request := workflowservice.StartBatchOperationRequest{ Namespace: c.Parent.Namespace, JobId: uuid.NewString(), @@ -121,8 +130,9 @@ func (c *TemporalWorkflowResetCommand) runBatchReset(cctx *CommandContext, cl cl } request.Operation = &workflowservice.StartBatchOperationRequest_ResetOperation{ ResetOperation: &batch.BatchOperationReset{ - Identity: clientIdentity(), - Options: batchResetOptions, + Identity: clientIdentity(), + Options: batchResetOptions, + PostResetOperations: postOps, }, } count, err := cl.CountWorkflow(cctx, &workflowservice.CountWorkflowExecutionsRequest{Query: c.Query}) diff --git a/temporalcli/commands.workflow_reset_update_options.go b/temporalcli/commands.workflow_reset_update_options.go new file mode 100644 index 000000000..3c24a3218 --- /dev/null +++ b/temporalcli/commands.workflow_reset_update_options.go @@ -0,0 +1,75 @@ +package temporalcli + +import ( + "fmt" + + deploymentpb "go.temporal.io/api/deployment/v1" + workflowpb "go.temporal.io/api/workflow/v1" + "google.golang.org/protobuf/types/known/fieldmaskpb" +) + +func (c *TemporalWorkflowResetWithWorkflowUpdateOptionsCommand) run(cctx *CommandContext, args []string) error { + validate, _ := c.Parent.getResetOperations() + if err := validate(); err != nil { + return err + } + + if c.VersioningOverrideBehavior.Value == "pinned" { + if c.VersioningOverrideDeploymentName == "" || c.VersioningOverrideBuildId == "" { + return fmt.Errorf("deployment name and build id are required with 'pinned' behavior") + } + } + if c.VersioningOverrideBehavior.Value != "pinned" { + if c.VersioningOverrideDeploymentName != "" || c.VersioningOverrideBuildId != "" { + return fmt.Errorf("cannot set deployment name or build id with %v behavior", c.VersioningOverrideBehavior.Value) + } + } + + cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) + if err != nil { + return err + } + defer cl.Close() + + VersioningOverride := &workflowpb.VersioningOverride{} + switch c.VersioningOverrideBehavior.Value { + case "pinned": + VersioningOverride.Override = &workflowpb.VersioningOverride_Pinned{ + Pinned: &workflowpb.VersioningOverride_PinnedOverride{ + Behavior: workflowpb.VersioningOverride_PINNED_OVERRIDE_BEHAVIOR_PINNED, + Version: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: c.VersioningOverrideDeploymentName, + BuildId: c.VersioningOverrideBuildId, + }, + }, + } + case "auto_upgrade": + VersioningOverride.Override = &workflowpb.VersioningOverride_AutoUpgrade{ + AutoUpgrade: true, + } + default: + return fmt.Errorf("invalid deployment behavior: %v, valid values are: 'pinned', and 'auto_upgrade'", c.VersioningOverrideBehavior.Value) + } + + var workflowExecutionOptions *workflowpb.WorkflowExecutionOptions + protoMask, err := fieldmaskpb.New(workflowExecutionOptions, "versioning_override") + if err != nil { + return fmt.Errorf("invalid field mask: %w", err) + } + + postOp := &workflowpb.PostResetOperation{ + Variant: &workflowpb.PostResetOperation_UpdateWorkflowOptions_{ + UpdateWorkflowOptions: &workflowpb.PostResetOperation_UpdateWorkflowOptions{ + WorkflowExecutionOptions: &workflowpb.WorkflowExecutionOptions{ + VersioningOverride: VersioningOverride, + }, + UpdateMask: protoMask, + }, + }, + } + + if c.Parent.WorkflowId != "" { + return c.Parent.doWorkflowResetWithPostOps(cctx, cl, []*workflowpb.PostResetOperation{postOp}) + } + return c.Parent.runBatchResetWithPostOps(cctx, cl, []*workflowpb.PostResetOperation{postOp}) +} diff --git a/temporalcli/commands.workflow_reset_update_options_test.go b/temporalcli/commands.workflow_reset_update_options_test.go new file mode 100644 index 000000000..d031110fb --- /dev/null +++ b/temporalcli/commands.workflow_reset_update_options_test.go @@ -0,0 +1,391 @@ +package temporalcli_test + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.temporal.io/api/enums/v1" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/workflow" +) + +func (s *SharedServerSuite) TestWorkflow_ResetWithWorkflowUpdateOptions_ValidatesArguments_MissingRequiredFlag() { + res := s.Execute( + "workflow", "reset", "with-workflow-update-options", + "--address", s.Address(), + "-w", "test-workflow", + "-t", "FirstWorkflowTask", + "--reason", "test-reset", + ) + require.Error(s.T(), res.Err) + require.Contains(s.T(), res.Err.Error(), "required flag") +} + +func (s *SharedServerSuite) TestWorkflow_ResetWithWorkflowUpdateOptions_ValidatesArguments_PinnedWithoutVersion() { + res := s.Execute( + "workflow", "reset", "with-workflow-update-options", + "--address", s.Address(), + "-w", "test-workflow", + "-t", "FirstWorkflowTask", + "--reason", "test-reset", + "--versioning-override-behavior", "pinned", + ) + require.Error(s.T(), res.Err) + require.Contains(s.T(), res.Err.Error(), "deployment name and build id are required with 'pinned' behavior") +} + +func (s *SharedServerSuite) TestWorkflow_ResetWithWorkflowUpdateOptions_ValidatesArguments_AutoUpgradeWithVersion() { + res := s.Execute( + "workflow", "reset", "with-workflow-update-options", + "--address", s.Address(), + "-w", "test-workflow", + "-t", "FirstWorkflowTask", + "--reason", "test-reset", + "--versioning-override-behavior", "auto_upgrade", + "--versioning-override-deployment-name", "some-deployment", + ) + require.Error(s.T(), res.Err) + require.Contains(s.T(), res.Err.Error(), "cannot set deployment name or build id with auto_upgrade behavior") +} + +func (s *SharedServerSuite) TestWorkflow_ResetWithWorkflowUpdateOptions_Single_AutoUpgradeBehavior() { + var wfExecutions int + s.Worker().OnDevWorkflow(func(ctx workflow.Context, a any) (any, error) { + wfExecutions++ + return "result", nil + }) + + // Start the workflow + searchAttr := "keyword-" + uuid.NewString() + run, err := s.Client.ExecuteWorkflow( + s.Context, + client.StartWorkflowOptions{ + TaskQueue: s.Worker().Options.TaskQueue, + SearchAttributes: map[string]any{"CustomKeywordField": searchAttr}, + }, + DevWorkflow, + "test-input", + ) + s.NoError(err) + var result any + s.NoError(run.Get(s.Context, &result)) + s.Equal(1, wfExecutions) + + // Reset with auto upgrade versioning behavior + res := s.Execute( + "workflow", "reset", "with-workflow-update-options", + "--address", s.Address(), + "-w", run.GetID(), + "-t", "FirstWorkflowTask", + "--reason", "test-reset-with-auto-upgrade", + "--versioning-override-behavior", "auto_upgrade", + ) + require.NoError(s.T(), res.Err) + + // Wait for reset to complete + s.Eventually(func() bool { + resp, err := s.Client.ListWorkflow(s.Context, &workflowservice.ListWorkflowExecutionsRequest{ + Query: "CustomKeywordField = '" + searchAttr + "'", + }) + s.NoError(err) + return len(resp.Executions) == 2 && resp.Executions[0].Status == enums.WORKFLOW_EXECUTION_STATUS_COMPLETED + }, 3*time.Second, 100*time.Millisecond) + + s.Equal(2, wfExecutions, "Should have re-executed the workflow") +} + +func (s *SharedServerSuite) TestWorkflow_ResetWithWorkflowUpdateOptions_Single_PinnedBehavior() { + var wfExecutions int + s.Worker().OnDevWorkflow(func(ctx workflow.Context, a any) (any, error) { + wfExecutions++ + return "result", nil + }) + + // Start the workflow + searchAttr := "keyword-" + uuid.NewString() + run, err := s.Client.ExecuteWorkflow( + s.Context, + client.StartWorkflowOptions{ + TaskQueue: s.Worker().Options.TaskQueue, + SearchAttributes: map[string]any{"CustomKeywordField": searchAttr}, + }, + DevWorkflow, + "test-input", + ) + s.NoError(err) + var result any + s.NoError(run.Get(s.Context, &result)) + s.Equal(1, wfExecutions) + + // Reset with pinned versioning behavior and properly formatted version + pinnedDeploymentName := "test-deployment" + pinnedBuildId := "v1.0" + res := s.Execute( + "workflow", "reset", "with-workflow-update-options", + "--address", s.Address(), + "-w", run.GetID(), + "-t", "FirstWorkflowTask", + "--reason", "test-reset-with-pinned-version", + "--versioning-override-behavior", "pinned", + "--versioning-override-deployment-name", pinnedDeploymentName, + "--versioning-override-build-id", pinnedBuildId, + ) + require.NoError(s.T(), res.Err) + + // Wait for reset to complete + s.Eventually(func() bool { + resp, err := s.Client.ListWorkflow(s.Context, &workflowservice.ListWorkflowExecutionsRequest{ + Query: "CustomKeywordField = '" + searchAttr + "'", + }) + s.NoError(err) + if len(resp.Executions) < 2 { // there should be two executions. + return false + } + resetRunID := resp.Executions[0].Execution.RunId // the first result is the reset execution. + descResult, err := s.Client.DescribeWorkflowExecution(s.Context, run.GetID(), resetRunID) + s.NoError(err) + s.NotNil(descResult) + + info := descResult.GetWorkflowExecutionInfo() + pinnedVersionOverride := info.VersioningInfo.VersioningOverride.GetPinned().GetVersion() + return pinnedVersionOverride.GetDeploymentName() == pinnedDeploymentName && pinnedVersionOverride.GetBuildId() == pinnedBuildId + }, 5*time.Second, 100*time.Millisecond) +} + +func (s *SharedServerSuite) TestWorkflow_ResetBatchWithWorkflowUpdateOptions_AutoUpgradeBehavior() { + var wfExecutions int + s.Worker().OnDevWorkflow(func(ctx workflow.Context, a any) (any, error) { + wfExecutions++ + return "result", nil + }) + + // Start the workflow + searchAttr := "keyword-" + uuid.NewString() + run, err := s.Client.ExecuteWorkflow( + s.Context, + client.StartWorkflowOptions{ + TaskQueue: s.Worker().Options.TaskQueue, + SearchAttributes: map[string]any{"CustomKeywordField": searchAttr}, + }, + DevWorkflow, + "test-input", + ) + s.NoError(err) + var result any + s.NoError(run.Get(s.Context, &result)) + s.Equal(1, wfExecutions) + + // Reset batch with auto_upgrade versioning behavior + s.CommandHarness.Stdin.WriteString("y\n") + res := s.Execute( + "workflow", "reset", "with-workflow-update-options", + "--address", s.Address(), + "--query", fmt.Sprintf("CustomKeywordField = '%s'", searchAttr), + "-t", "FirstWorkflowTask", + "--reason", "test-batch-reset-with-update-options", + "--versioning-override-behavior", "auto_upgrade", + ) + require.NoError(s.T(), res.Err) + + // Wait for batch reset to complete + s.Eventually(func() bool { + resp, err := s.Client.ListWorkflow(s.Context, &workflowservice.ListWorkflowExecutionsRequest{ + Query: "CustomKeywordField = '" + searchAttr + "'", + }) + s.NoError(err) + return len(resp.Executions) == 2 && resp.Executions[0].Status == enums.WORKFLOW_EXECUTION_STATUS_COMPLETED + }, 3*time.Second, 100*time.Millisecond) + + s.Equal(2, wfExecutions, "Should have re-executed the workflow from batch reset") +} + +func (s *SharedServerSuite) TestWorkflow_ResetBatchWithWorkflowUpdateOptions_PinnedBehavior() { + var wfExecutions int + s.Worker().OnDevWorkflow(func(ctx workflow.Context, a any) (any, error) { + wfExecutions++ + return "result", nil + }) + + // Start the workflow + searchAttr := "keyword-" + uuid.NewString() + run, err := s.Client.ExecuteWorkflow( + s.Context, + client.StartWorkflowOptions{ + TaskQueue: s.Worker().Options.TaskQueue, + SearchAttributes: map[string]any{"CustomKeywordField": searchAttr}, + }, + DevWorkflow, + "test-input", + ) + s.NoError(err) + var result any + s.NoError(run.Get(s.Context, &result)) + s.Equal(1, wfExecutions) + + // Reset batch with pinned versioning behavior and properly formatted version + pinnedDeploymentName := "batch-deployment" + pinnedBuildId := "v1.0" + s.CommandHarness.Stdin.WriteString("y\n") + res := s.Execute( + "workflow", "reset", "with-workflow-update-options", + "--address", s.Address(), + "--query", fmt.Sprintf("CustomKeywordField = '%s'", searchAttr), + "-t", "FirstWorkflowTask", + "--reason", "test-batch-reset-with-pinned-version", + "--versioning-override-behavior", "pinned", + "--versioning-override-deployment-name", pinnedDeploymentName, + "--versioning-override-build-id", pinnedBuildId, + ) + require.NoError(s.T(), res.Err) + + // Wait for batch reset to complete + s.Eventually(func() bool { + resp, err := s.Client.ListWorkflow(s.Context, &workflowservice.ListWorkflowExecutionsRequest{ + Query: "CustomKeywordField = '" + searchAttr + "'", + }) + s.NoError(err) + if len(resp.Executions) < 2 { // there should be two executions. + return false + } + resetRunID := resp.Executions[0].Execution.RunId // the first result is the reset execution. + descResult, err := s.Client.DescribeWorkflowExecution(s.Context, run.GetID(), resetRunID) + s.NoError(err) + s.NotNil(descResult) + + info := descResult.GetWorkflowExecutionInfo() + pinnedVersionOverride := info.VersioningInfo.VersioningOverride.GetPinned().GetVersion() + return pinnedVersionOverride.GetDeploymentName() == pinnedDeploymentName && pinnedVersionOverride.GetBuildId() == pinnedBuildId + }, 5*time.Second, 100*time.Millisecond) +} + +func (s *SharedServerSuite) TestWorkflow_ResetWithWorkflowUpdateOptions_InheritsParentFlags() { + // Test that the subcommand inherits parent flags correctly + res := s.Execute( + "workflow", "reset", "with-workflow-update-options", + "--address", s.Address(), + "-w", "test-workflow", + "-r", "test-run-id", + "-e", "10", + "--reason", "test-reset-with-inherited-flags", + "--versioning-override-behavior", "auto_upgrade", + "--reapply-exclude", "Signal", + ) + + // The command should fail because the workflow doesn't exist, but the error + // should be about the missing workflow, not about invalid flags + require.Error(s.T(), res.Err) + require.NotContains(s.T(), res.Err.Error(), "required flag") + require.NotContains(s.T(), res.Err.Error(), "invalid argument") +} + +func (s *SharedServerSuite) TestWorkflow_ResetWithWorkflowUpdateOptions_WithLastWorkflowTask() { + var wfExecutions, activityExecutions int + s.Worker().OnDevActivity(func(ctx context.Context, a any) (any, error) { + activityExecutions++ + return nil, nil + }) + s.Worker().OnDevWorkflow(func(ctx workflow.Context, a any) (any, error) { + workflow.ExecuteActivity(ctx, DevActivity, 1).Get(ctx, nil) + wfExecutions++ + return nil, nil + }) + + // Start the workflow + searchAttr := "keyword-" + uuid.NewString() + run, err := s.Client.ExecuteWorkflow( + s.Context, + client.StartWorkflowOptions{ + TaskQueue: s.Worker().Options.TaskQueue, + SearchAttributes: map[string]any{"CustomKeywordField": searchAttr}, + }, + DevWorkflow, + "ignored", + ) + s.NoError(err) + var junk any + s.NoError(run.Get(s.Context, &junk)) + s.Equal(1, wfExecutions) + + // Reset to the last workflow task with update options + res := s.Execute( + "workflow", "reset", "with-workflow-update-options", + "--address", s.Address(), + "-w", run.GetID(), + "-t", "LastWorkflowTask", + "--reason", "test-reset-last-workflow-task-with-options", + "--versioning-override-behavior", "auto_upgrade", + ) + require.NoError(s.T(), res.Err) + + // Wait for reset to complete + s.Eventually(func() bool { + resp, err := s.Client.ListWorkflow(s.Context, &workflowservice.ListWorkflowExecutionsRequest{ + Query: "CustomKeywordField = '" + searchAttr + "'", + }) + s.NoError(err) + return len(resp.Executions) == 2 && resp.Executions[0].Status == enums.WORKFLOW_EXECUTION_STATUS_COMPLETED + }, 3*time.Second, 100*time.Millisecond) + + s.Equal(2, wfExecutions, "Should re-executed the workflow") + s.Equal(1, activityExecutions, "Should not have re-executed the activity") +} + +func (s *SharedServerSuite) TestWorkflow_ResetWithWorkflowUpdateOptions_WithEventID() { + // Test that the new subcommand works with event ID reset type + var activityCount int + s.Worker().OnDevActivity(func(ctx context.Context, a any) (any, error) { + activityCount++ + return a, nil + }) + + s.Worker().OnDevWorkflow(func(ctx workflow.Context, a any) (any, error) { + var res any + if err := workflow.ExecuteActivity(ctx, DevActivity, 1).Get(ctx, &res); err != nil { + return res, err + } + err := workflow.ExecuteActivity(ctx, DevActivity, 2).Get(ctx, &res) + return res, err + }) + + // Start the workflow + searchAttr := "keyword-" + uuid.NewString() + run, err := s.Client.ExecuteWorkflow( + s.Context, + client.StartWorkflowOptions{ + TaskQueue: s.Worker().Options.TaskQueue, + SearchAttributes: map[string]any{"CustomKeywordField": searchAttr}, + }, + DevWorkflow, + "ignored", + ) + require.NoError(s.T(), err) + var ignored any + s.NoError(run.Get(s.Context, &ignored)) + s.Equal(2, activityCount) + + // Reset with event ID and update options + res := s.Execute( + "workflow", "reset", "with-workflow-update-options", + "--address", s.Address(), + "-w", run.GetID(), + "-e", "3", // Use a known early event ID + "--reason", "test-reset-event-id-with-options", + "--versioning-override-behavior", "auto_upgrade", + ) + require.NoError(s.T(), res.Err) + + // Wait for reset to complete + s.Eventually(func() bool { + resp, err := s.Client.ListWorkflow(s.Context, &workflowservice.ListWorkflowExecutionsRequest{ + Query: "CustomKeywordField = '" + searchAttr + "'", + }) + s.NoError(err) + return len(resp.Executions) == 2 && resp.Executions[0].Status == enums.WORKFLOW_EXECUTION_STATUS_COMPLETED + }, 5*time.Second, 100*time.Millisecond) + + s.Greater(activityCount, 2, "Should have re-executed activities after reset") +} diff --git a/temporalcli/commandsgen/code.go b/temporalcli/commandsgen/code.go index e51296d46..e2784f90f 100644 --- a/temporalcli/commandsgen/code.go +++ b/temporalcli/commandsgen/code.go @@ -261,8 +261,8 @@ func (c *Command) writeCode(w *codeWriter) error { } w.writeLinef("}))") } - // If there are no subcommands, we need a run function - if len(subCommands) == 0 { + // If there are no subcommands, or if subcommands are optional, we need a run function + if len(subCommands) == 0 || c.SubcommandsOptional { w.writeLinef("s.Command.Run = func(c *%v.Command, args []string) {", w.importCobra()) w.writeLinef("if err := s.run(cctx, args); err != nil {") w.writeLinef("cctx.Options.Fail(err)") diff --git a/temporalcli/commandsgen/commands.yml b/temporalcli/commandsgen/commands.yml index db5207c5a..ff27aa71b 100644 --- a/temporalcli/commandsgen/commands.yml +++ b/temporalcli/commandsgen/commands.yml @@ -51,7 +51,8 @@ # * When commands have a single command-level option, include it the mandatory example. # * Use square bracket overviews to present how complex commands will be used. # * Yes: temporal operator [command] [subcommand] [options] -# Commands with subcommands can't be run on their own. +# Commands with subcommands can't be run on their own unless +# subcommands-optional is set to true. # Because of this, always use full command examples. # * Use square brackets to highlight optional elements, especially when long # descriptions would suffer from two very similar command invocations. @@ -94,6 +95,7 @@ # exact-args: Require this exact number of args. (int) # maximum-args: Require this maximum number of args. (int) # ignores-missing-env: Ignore missing environment variables. (bool) +# subcommands-optional: Allow command to be run even when it has subcommands. (bool) # options: A list of options. (Option[]) # - name: The option name. (string) # type: The option type. (string) @@ -302,7 +304,7 @@ commands: type: string description: Identity of the user submitting this request. option-sets: - - workflow reference + - workflow-reference - name: temporal activity fail summary: Fail an Activity @@ -330,14 +332,16 @@ commands: type: string description: Reason for failing the Activity. option-sets: - - workflow reference + - workflow-reference - name: temporal activity update-options summary: Update Activity options description: | - Update Activity options. Specify the Activity and Workflow IDs, and - options you want to update. - Updates are incremental, only changing the specified options. + Update the options of a running Activity that were passed into it from + a Workflow. Updates are incremental, only changing the specified + options. + + For example: ``` temporal activity update-options \ @@ -352,8 +356,9 @@ commands: --retry-maximum-interval DURATION \ --retry-backoff-coefficient NewBackoffCoefficient \ --retry-maximum-attempts NewMaximumAttempts - ``` + + You may follow this command with `temporal activity reset`, and the new values will apply after the reset. options: - name: activity-id type: string @@ -415,7 +420,7 @@ commands: type: string description: Identity of the user submitting this request. option-sets: - - workflow reference + - workflow-reference - name: temporal activity pause summary: Pause an Activity @@ -425,35 +430,38 @@ commands: If the Activity is not currently running (e.g. because it previously failed), it will not be run again until it is unpaused. - However, if the Activity is currently running, it will run to completion. + However, if the Activity is currently running, it will run until the next + time it fails, completes, or times out, at which point the pause will kick in. + If the Activity is on its last retry attempt and fails, the failure will be returned to the caller, just as if the Activity had not been paused. - Activities can be specified by their Activity ID or Activity Type. - One of those parameters must be provided. If both are provided - Activity - Type will be used, and Activity ID will be ignored. + Activities should be specified either by their Activity ID or Activity Type. - Specify the Activity and Workflow IDs: + For example, specify the Activity and Workflow IDs like this: ``` temporal activity pause \ --activity-id YourActivityId \ --workflow-id YourWorkflowId ``` + + To later unpause the activity, see [unpause](#unpause). You may also want to + [reset](#reset) the activity to unpause it while also starting it from the beginning. options: - name: activity-id short: a type: string - description: Activity ID to pause. + 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: Activity Type to pause. + description: All activities of the Activity Type will be paused. Either `activity-id` or `activity-type` must be provided, but not both. - name: identity type: string - description: Identity of the user submitting this request. + description: The identity of the user or client submitting this request. option-sets: - - workflow reference + - workflow-reference - name: temporal activity unpause summary: Unpause an Activity @@ -472,18 +480,8 @@ commands: Use `--reset-heartbeat` to reset the Activity's heartbeats. Activities can be specified by their Activity ID or Activity Type. - One of those parameters must be provided. If both are provided - Activity - Type will be used, and Activity ID will be ignored. + One of those parameters must be provided. - Activities can be unpaused in bulk via a visibility Query list filter: - - ``` - temporal activity unpause \ - --query YourQuery \ - --reason YourReasonForTermination - ``` - - Specify the Activity ID or Type and Workflow IDs: ``` @@ -493,19 +491,29 @@ commands: --reset-attempts --reset-heartbeats ``` + + Activities can be unpaused in bulk via a visibility Query list filter. + For example, if you want to unpause activities of type Foo that you + previously paused, do: + + ``` + temporal activity unpause \ + --query 'TemporalPauseInfo="property:activityType=Foo"' + ``` options: - name: activity-id short: a type: string description: | - Activity ID to unpause. Can only be used without --query. + 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. - name: activity-type short: g type: string - description: Activity Type to unpause. + 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: Identity of the user submitting this request. + description: The identity of the user or client submitting this request. - name: reset-attempts type: bool description: Also reset the activity attempts. @@ -530,20 +538,34 @@ commands: - name: temporal activity reset summary: Reset an Activity description: | - Resetting an activity resets both the number of attempts and the activity - timeout. + Reset an activity. This restarts the activity as if it were first being + scheduled. That is, it will reset both the number of attempts and the + activity timeout, as well as, optionally, the + [heartbeat details](#reset-heartbeats). - If activity is paused and 'keep_paused' flag is not provided - it will be - unpaused. - If activity is paused and 'keep_paused' flag is provided - it will stay - paused. - If activity is waiting for the retry, is will be rescheduled immediately. - If the 'reset_heartbeats' flag is set, the activity heartbeat timer and - heartbeats will be reset. + If the activity may be executing (i.e. it has not yet timed out), the + reset will take effect the next time it fails, heartbeats, or times out. + If is waiting for a retry (i.e. has failed or timed out), the reset + will apply immediately. + + If the activity is already paused, it will be unpaused by default. + You can specify `keep_paused` to prevent this. + + If the activity is paused and the `keep_paused` flag is not provided, + it will be unpaused. If the activity is paused and `keep_paused` flag + is provided - it will stay paused. Activities can be specified by their Activity ID or Activity Type. - One of those parameters must be provided. If both are provided - Activity - Type will be used, and Activity ID will be ignored. + + ### Resetting activities that heartbeat {#reset-heartbeats} + + Activities that heartbeat will receive a [Canceled failure](/references/failures#cancelled-failure) + the next time they heartbeat after a reset. + + If, in your Activity, you need to do any cleanup when an Activity is + reset, handle this error and then re-throw it when you've cleaned up. + + If the `reset_heartbeats` flag is set, the heartbeat details will also be cleared. Specify the Activity Type of ID and Workflow IDs: @@ -558,22 +580,22 @@ commands: - name: activity-id short: a type: string - description: Activity ID to pause. + description: The Activity ID to reset. Either `activity-id` or `activity-type` must be provided, but not both. - name: activity-type short: g type: string - description: Activity Type to pause. + description: The Activity Type to reset. Either `activity-id` or `activity-type` must be provided, but not both. - name: identity type: string - description: Identity of the user submitting this request. + description: The identity of the user or client submitting this request. - name: keep-paused type: bool - description: If activity was paused - it will stay paused. + description: If the activity was paused, it will stay paused. - name: reset-heartbeats type: bool - description: Reset the Activity's heartbeat. + description: Clear the Activity's heartbeat details. option-sets: - - workflow reference + - workflow-reference - name: temporal batch summary: Manage running batch jobs @@ -688,7 +710,7 @@ commands: summary: Manage config files (EXPERIMENTAL) description: | Config files are TOML files that contain profiles, with each profile - containing configuration for connecting to Temporal. + containing configuration for connecting to Temporal. ``` temporal config set \ @@ -2016,8 +2038,8 @@ commands: description: Backfill start time. required: true option-sets: - - overlap policy - - schedule id + - overlap-policy + - schedule-id - name: temporal schedule create summary: Create a new Schedule @@ -3416,6 +3438,7 @@ commands: - name: temporal workflow reset summary: Move Workflow Execution history point + subcommands-optional: true description: | Reset a Workflow Execution so it can resume from a point in its Event History without losing its progress up to that point: @@ -3505,6 +3528,14 @@ commands: Don't prompt to confirm. Only allowed when `--query` is present. + - name: temporal workflow reset with-workflow-update-options + summary: Update options on reset workflow + description: | + Run Workflow Update Options atomically after the Workflow is reset. + Workflows selected by the reset command are forwarded onto the subcommand. + option-sets: + - workflow-update-options + - name: temporal workflow result summary: Wait for and show the result of a Workflow Execution description: | @@ -3849,7 +3880,7 @@ commands: summary: Send an Update-With-Start and wait for it to be accepted or rejected (Experimental) description: | Send a message to a Workflow Execution to invoke an Update handler, and wait for - the update to be accepted or rejected. If the Workflow Execution is not running, + the update to be accepted or rejected. If the Workflow Execution is not running, then a new workflow execution is started and the update is sent. Experimental. @@ -4496,3 +4527,23 @@ option-sets: enum-values: - not_open - not_completed_cleanly + + - name: workflow-update-options + options: + - name: versioning-override-behavior + type: string-enum + description: | + Override the versioning behavior of a Workflow. + required: true + enum-values: + - pinned + - auto_upgrade + - name: versioning-override-deployment-name + type: string + 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 + version to target. + diff --git a/temporalcli/commandsgen/docs.go b/temporalcli/commandsgen/docs.go index d26aa2456..4cca69f9f 100644 --- a/temporalcli/commandsgen/docs.go +++ b/temporalcli/commandsgen/docs.go @@ -9,7 +9,6 @@ import ( ) func GenerateDocsFiles(commands Commands) (map[string][]byte, error) { - optionSetMap := make(map[string]OptionSets) for i, optionSet := range commands.OptionSets { optionSetMap[optionSet.Name] = commands.OptionSets[i] @@ -59,6 +58,8 @@ func (c *Command) writeDoc(w *docWriter) error { func (w *docWriter) writeCommand(c *Command) { fileName := c.fileName() w.fileMap[fileName] = &bytes.Buffer{} + 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("---\n") w.fileMap[fileName].WriteString("id: " + fileName + "\n") w.fileMap[fileName].WriteString("title: Temporal CLI " + fileName + " command reference\n") @@ -160,7 +161,11 @@ func (w *docWriter) processOptions(c *Command) { // Maintain stack of options available from parent commands for _, set := range c.OptionSets { - optionSetOptions := w.optionSetMap[set].Options + optionSet, ok := w.optionSetMap[set] + if !ok { + panic(fmt.Sprintf("invalid option set %v used", set)) + } + optionSetOptions := optionSet.Options options = append(options, optionSetOptions...) } diff --git a/temporalcli/commandsgen/parse.go b/temporalcli/commandsgen/parse.go index a93c69482..568e38fd7 100644 --- a/temporalcli/commandsgen/parse.go +++ b/temporalcli/commandsgen/parse.go @@ -48,6 +48,7 @@ type ( ExactArgs int `yaml:"exact-args"` MaximumArgs int `yaml:"maximum-args"` IgnoreMissingEnv bool `yaml:"ignores-missing-env"` + SubcommandsOptional bool `yaml:"subcommands-optional"` Options []Option `yaml:"options"` OptionSets []string `yaml:"option-sets"` Docs Docs `yaml:"docs"` From 9786ca42b698bb4f1ee4f4da7db76628252fb697 Mon Sep 17 00:00:00 2001 From: Sean Kane Date: Tue, 29 Jul 2025 22:25:36 +0200 Subject: [PATCH 2/5] activity: support batch operations in reset and update-options (#831) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What was changed * The `temporal activity {pause,update-options}` support batch operations through `--query` * Consistently passing the `--identity` parameter to server requests ## Why? Improved developer experience, can batch these requests through a visibility query instead of sending multiple requests ## Checklist 1. Closes: N/A 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 | 82 ++++---- 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 | 113 ++++++----- 12 files changed, 383 insertions(+), 209 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 4b41bfc4a..069de1e65 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 { @@ -173,7 +185,6 @@ func (c *ClientOptions) dialClient(cctx *CommandContext) (client.Client, error) return nil, fmt.Errorf("failed creating client options: %w", err) } 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 @@ -255,18 +266,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 ef89a3b87..37e40a492 100644 --- a/temporalcli/commands.gen.go +++ b/temporalcli/commands.gen.go @@ -33,6 +33,7 @@ type ClientOptions struct { CodecEndpoint string CodecAuth string CodecHeader []string + Identity string } func (v *ClientOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSet) { @@ -52,6 +53,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 { @@ -422,7 +424,6 @@ type TemporalActivityCompleteCommand struct { WorkflowReferenceOptions ActivityId string Result string - Identity string } func NewTemporalActivityCompleteCommand(cctx *CommandContext, parent *TemporalActivityCommand) *TemporalActivityCompleteCommand { @@ -441,7 +442,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 { @@ -457,7 +457,6 @@ type TemporalActivityFailCommand struct { WorkflowReferenceOptions ActivityId string Detail string - Identity string Reason string } @@ -476,7 +475,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) { @@ -493,7 +491,6 @@ type TemporalActivityPauseCommand struct { WorkflowReferenceOptions ActivityId string ActivityType string - Identity string } func NewTemporalActivityPauseCommand(cctx *CommandContext, parent *TemporalActivityCommand) *TemporalActivityPauseCommand { @@ -509,8 +506,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.") - s.Command.Flags().StringVar(&s.Identity, "identity", "", "The identity of the user or client submitting this request.") + 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.") s.WorkflowReferenceOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { @@ -523,12 +519,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 { @@ -538,17 +537,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) @@ -563,7 +566,6 @@ type TemporalActivityUnpauseCommand struct { SingleWorkflowOrBatchOptions ActivityId string ActivityType string - Identity string ResetAttempts bool ResetHeartbeats bool MatchAll bool @@ -582,14 +584,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 { @@ -602,8 +603,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 @@ -613,7 +616,7 @@ type TemporalActivityUpdateOptionsCommand struct { RetryMaximumInterval Duration RetryBackoffCoefficient float32 RetryMaximumAttempts int - Identity string + RestoreOriginalOptions bool } func NewTemporalActivityUpdateOptionsCommand(cctx *CommandContext, parent *TemporalActivityCommand) *TemporalActivityUpdateOptionsCommand { @@ -623,13 +626,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.") @@ -645,8 +649,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) @@ -2783,7 +2787,6 @@ type TemporalWorkerDeploymentDeleteCommand struct { Parent *TemporalWorkerDeploymentCommand Command cobra.Command DeploymentNameOptions - Identity string } func NewTemporalWorkerDeploymentDeleteCommand(cctx *CommandContext, parent *TemporalWorkerDeploymentCommand) *TemporalWorkerDeploymentDeleteCommand { @@ -2798,7 +2801,6 @@ func NewTemporalWorkerDeploymentDeleteCommand(cctx *CommandContext, parent *Temp s.Command.Long = "+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\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 { @@ -2812,7 +2814,6 @@ type TemporalWorkerDeploymentDeleteVersionCommand struct { Parent *TemporalWorkerDeploymentCommand Command cobra.Command DeploymentVersionOptions - Identity string SkipDrainage bool } @@ -2828,7 +2829,6 @@ func NewTemporalWorkerDeploymentDeleteVersionCommand(cctx *CommandContext, paren s.Command.Long = "+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\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) { @@ -2922,7 +2922,6 @@ type TemporalWorkerDeploymentSetCurrentVersionCommand struct { Parent *TemporalWorkerDeploymentCommand Command cobra.Command DeploymentVersionOrUnversionedOptions - Identity string IgnoreMissingTaskQueues bool Yes bool } @@ -2939,7 +2938,6 @@ func NewTemporalWorkerDeploymentSetCurrentVersionCommand(cctx *CommandContext, p s.Command.Long = "+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\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()) @@ -2957,7 +2955,6 @@ type TemporalWorkerDeploymentSetRampingVersionCommand struct { DeploymentVersionOrUnversionedOptions Percentage float32 Delete bool - Identity string IgnoreMissingTaskQueues bool Yes bool } @@ -2976,7 +2973,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 ff27aa71b..e61557f87 100644 --- a/temporalcli/commandsgen/commands.yml +++ b/temporalcli/commandsgen/commands.yml @@ -300,9 +300,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 @@ -325,9 +322,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. @@ -338,8 +332,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: @@ -359,11 +353,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. @@ -416,11 +431,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 @@ -454,12 +469,8 @@ 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. - - name: identity - type: string - description: The identity of the user or client submitting this request. option-sets: - workflow-reference @@ -505,18 +516,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: | @@ -525,10 +532,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. @@ -576,26 +582,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 @@ -954,10 +983,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 @@ -1040,9 +1065,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. @@ -1088,9 +1110,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. @@ -1156,9 +1175,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. @@ -4124,6 +4140,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 975f91df0f128577a06baab545f57bd1c2886924 Mon Sep 17 00:00:00 2001 From: Sean Kane Date: Wed, 30 Jul 2025 15:46:58 -0600 Subject: [PATCH 3/5] pause: add pause activity information to workflow describe cmd --- go.mod | 2 ++ go.sum | 2 -- temporalcli/commands.workflow_view.go | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 26af6c26c..969d28179 100644 --- a/go.mod +++ b/go.mod @@ -175,3 +175,5 @@ require ( modernc.org/strutil v1.2.1 // indirect modernc.org/token v1.1.0 // indirect ) + +replace go.temporal.io/api => ../api-go diff --git a/go.sum b/go.sum index 63dc1f148..580e6c549 100644 --- a/go.sum +++ b/go.sum @@ -402,8 +402,6 @@ 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/sdk/contrib/envconfig v0.1.0 h1:s+G/Ujph+Xl2jzLiiIm2T1vuijDkUL4Kse49dgDVGBE= diff --git a/temporalcli/commands.workflow_view.go b/temporalcli/commands.workflow_view.go index 896bd76ef..38b066379 100644 --- a/temporalcli/commands.workflow_view.go +++ b/temporalcli/commands.workflow_view.go @@ -287,6 +287,9 @@ func (c *TemporalWorkflowDescribeCommand) run(cctx *CommandContext, args []strin LastFailure *failure.Failure `cli:",cardOmitEmpty"` LastWorkerIdentity string `cli:",cardOmitEmpty"` LastHeartbeatDetails []*common.Payload `cli:",cardOmitEmpty"` + Paused bool + PauseTime time.Time `cli:",cardOmitEmpty"` + PausedBy string `cli:",cardOmitEmpty"` }, len(resp.PendingActivities)) for i, a := range resp.PendingActivities { acts[i].ActivityId = a.ActivityId @@ -301,6 +304,18 @@ func (c *TemporalWorkflowDescribeCommand) run(cctx *CommandContext, args []strin acts[i].LastFailure = a.LastFailure acts[i].LastWorkerIdentity = a.LastWorkerIdentity acts[i].LastHeartbeatDetails = a.HeartbeatDetails.GetPayloads() + acts[i].Paused = a.Paused + + if pauseInfo := a.GetPauseInfo(); pauseInfo != nil { + acts[i].PauseTime = timestampToTime(pauseInfo.GetPauseTime()) + + switch pausedBy := pauseInfo.GetPausedBy().(type) { + case *workflow.PendingActivityInfo_PauseInfo_Manual_: + acts[i].PausedBy = pausedBy.Manual.Identity + case *workflow.PendingActivityInfo_PauseInfo_Rule_: + acts[i].PausedBy = pausedBy.Rule.Identity + } + } } _ = cctx.Printer.PrintStructured(acts, printer.StructuredOptions{}) cctx.Printer.Println() From fb3b502021c45434ac1ced30b3ea15da29b5439b Mon Sep 17 00:00:00 2001 From: Sean Kane Date: Wed, 30 Jul 2025 16:02:43 -0600 Subject: [PATCH 4/5] no go.mod or go.sum changes necessary --- go.mod | 2 -- go.sum | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 969d28179..26af6c26c 100644 --- a/go.mod +++ b/go.mod @@ -175,5 +175,3 @@ require ( modernc.org/strutil v1.2.1 // indirect modernc.org/token v1.1.0 // indirect ) - -replace go.temporal.io/api => ../api-go diff --git a/go.sum b/go.sum index 580e6c549..63dc1f148 100644 --- a/go.sum +++ b/go.sum @@ -402,6 +402,8 @@ 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/sdk/contrib/envconfig v0.1.0 h1:s+G/Ujph+Xl2jzLiiIm2T1vuijDkUL4Kse49dgDVGBE= From f7d0d327ff570f7dd71a8f075302e368f7a522fe Mon Sep 17 00:00:00 2001 From: Sean Kane Date: Tue, 12 Aug 2025 15:36:34 -0600 Subject: [PATCH 5/5] add paused activity section to temporal workflow describe command --- go.mod | 6 ++++-- go.sum | 6 ++---- temporalcli/commands.workflow_view.go | 22 ++++++++++++++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 26af6c26c..5bee62aae 100644 --- a/go.mod +++ b/go.mod @@ -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/api v1.51.1-0.20250725211336-3d6e39249ecf go.temporal.io/sdk v1.35.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-135.0.0.20250729005252-42e640c619f8 google.golang.org/grpc v1.72.2 google.golang.org/protobuf v1.36.6 gopkg.in/yaml.v3 v3.0.1 @@ -175,3 +175,5 @@ require ( modernc.org/strutil v1.2.1 // indirect modernc.org/token v1.1.0 // indirect ) + +replace go.temporal.io/api => ../api-go diff --git a/go.sum b/go.sum index 63dc1f148..ec988c8cc 100644 --- a/go.sum +++ b/go.sum @@ -402,14 +402,12 @@ 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/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-135.0.0.20250729005252-42e640c619f8 h1:/TKIH0T3HEicYLAgaNucr6xZPRh9JowLWbIhefRv8C4= +go.temporal.io/server v1.29.0-135.0.0.20250729005252-42e640c619f8/go.mod h1:RGikKuGPvR29D5YkZuJkRI1o9SD+ws6Zx4YsQezMmd0= 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.workflow_view.go b/temporalcli/commands.workflow_view.go index 38b066379..123339157 100644 --- a/temporalcli/commands.workflow_view.go +++ b/temporalcli/commands.workflow_view.go @@ -26,6 +26,7 @@ func (c *TemporalWorkflowDescribeCommand) run(cctx *CommandContext, args []strin return err } defer cl.Close() + resp, err := cl.DescribeWorkflowExecution(cctx, c.WorkflowId, c.RunId) if err != nil { return fmt.Errorf("failed describing workflow: %w", err) @@ -321,6 +322,27 @@ func (c *TemporalWorkflowDescribeCommand) run(cctx *CommandContext, args []strin cctx.Printer.Println() } + if pauseInfo := resp.GetWorkflowPauseInfo(); pauseInfo != nil { + cctx.Printer.Println(color.MagentaString("Paused Activities: %v", len(pauseInfo.GetActivityPauseInfos()))) + if len(pauseInfo.GetActivityPauseInfos()) > 0 { + cctx.Printer.Println() + acts := make([]struct { + UpdateTime time.Time + ActivityType string + Identity string + Reason string + }, len(pauseInfo.GetActivityPauseInfos())) + for i, a := range pauseInfo.GetActivityPauseInfos() { + acts[i].UpdateTime = timestampToTime(a.GetUpdateTime()) + acts[i].ActivityType = a.ActivityType + acts[i].Identity = a.Identity + acts[i].Reason = a.Reason + } + _ = cctx.Printer.PrintStructured(acts, printer.StructuredOptions{}) + cctx.Printer.Println() + } + } + cctx.Printer.Println(color.MagentaString("Pending Child Workflows: %v", len(resp.PendingChildren))) if len(resp.PendingChildren) > 0 { cctx.Printer.Println()