From b46cd2dbce8eb2dc0fa22c6234abe46fd2533c3c Mon Sep 17 00:00:00 2001 From: Veeral Patel Date: Mon, 20 Apr 2026 14:45:47 -0700 Subject: [PATCH] feat: add --reverse flag to `temporal workflow show` Fetches Event History newest-event-first via WorkflowService.GetWorkflowExecutionHistoryReverse, useful for inspecting recent activity on long-running workflows without pulling the full history. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/temporalcli/commands.gen.go | 2 + .../temporalcli/commands.workflow_exec.go | 56 ++++++++++-- .../temporalcli/commands.workflow_view.go | 6 ++ .../commands.workflow_view_test.go | 87 +++++++++++++++++++ internal/temporalcli/commands.yaml | 4 + 5 files changed, 150 insertions(+), 5 deletions(-) diff --git a/internal/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go index ad01d3083..cc15f83bf 100644 --- a/internal/temporalcli/commands.gen.go +++ b/internal/temporalcli/commands.gen.go @@ -3773,6 +3773,7 @@ type TemporalWorkflowShowCommand struct { WorkflowReferenceOptions Follow bool Detailed bool + Reverse bool } func NewTemporalWorkflowShowCommand(cctx *CommandContext, parent *TemporalWorkflowCommand) *TemporalWorkflowShowCommand { @@ -3789,6 +3790,7 @@ func NewTemporalWorkflowShowCommand(cctx *CommandContext, parent *TemporalWorkfl s.Command.Args = cobra.NoArgs s.Command.Flags().BoolVarP(&s.Follow, "follow", "f", false, "Follow the Workflow Execution progress in real time. Does not apply to JSON output.") s.Command.Flags().BoolVar(&s.Detailed, "detailed", false, "Display events as detailed sections instead of table. Does not apply to JSON output.") + s.Command.Flags().BoolVar(&s.Reverse, "reverse", false, "Fetch Event History newest-event-first. Cannot be combined with --follow.") s.WorkflowReferenceOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { diff --git a/internal/temporalcli/commands.workflow_exec.go b/internal/temporalcli/commands.workflow_exec.go index 935504b96..e115a5303 100644 --- a/internal/temporalcli/commands.workflow_exec.go +++ b/internal/temporalcli/commands.workflow_exec.go @@ -707,16 +707,24 @@ func coloredEventType(e enums.EventType) string { type structuredHistoryIter struct { ctx context.Context client client.Client + namespace string workflowID string runID string includeDetails bool // If set true, long poll the history for updates follow bool + // If set true, fetch history newest-event-first via GetWorkflowExecutionHistoryReverse + reverse bool // If and when the iterator encounters a workflow-terminating event, it will store it here wfResult *history.HistoryEvent - // Internal + // Internal (forward) iter client.HistoryEventIterator + + // Internal (reverse) + reverseBuf []*history.HistoryEvent + reverseNextToken []byte + reverseStarted bool } func (s *structuredHistoryIter) print(cctx *CommandContext) error { @@ -784,15 +792,20 @@ func (s *structuredHistoryIter) Next() (any, error) { Type: coloredEventType(event.EventType), } - // Follow continue as new - if attr := event.GetWorkflowExecutionContinuedAsNewEventAttributes(); attr != nil { - s.runID = attr.NewExecutionRunId - s.iter = nil + // Follow continue as new (forward only; reverse traversal stays within the requested run) + if !s.reverse { + if attr := event.GetWorkflowExecutionContinuedAsNewEventAttributes(); attr != nil { + s.runID = attr.NewExecutionRunId + s.iter = nil + } } return data, nil } func (s *structuredHistoryIter) NextRawEvent() (*history.HistoryEvent, error) { + if s.reverse { + return s.nextRawEventReverse() + } // Load iter if s.iter == nil { s.iter = s.client.GetWorkflowHistory( @@ -811,6 +824,39 @@ func (s *structuredHistoryIter) NextRawEvent() (*history.HistoryEvent, error) { return event, nil } +func (s *structuredHistoryIter) nextRawEventReverse() (*history.HistoryEvent, error) { + for len(s.reverseBuf) == 0 { + if s.reverseStarted && len(s.reverseNextToken) == 0 { + return nil, nil + } + s.reverseStarted = true + resp, err := s.client.WorkflowService().GetWorkflowExecutionHistoryReverse( + s.ctx, + &workflowservice.GetWorkflowExecutionHistoryReverseRequest{ + Namespace: s.namespace, + Execution: &common.WorkflowExecution{ + WorkflowId: s.workflowID, + RunId: s.runID, + }, + NextPageToken: s.reverseNextToken, + }, + ) + if err != nil { + return nil, err + } + s.reverseNextToken = resp.GetNextPageToken() + if h := resp.GetHistory(); h != nil { + s.reverseBuf = h.GetEvents() + } + } + event := s.reverseBuf[0] + s.reverseBuf = s.reverseBuf[1:] + if isWorkflowTerminatingEvent(event.EventType) { + s.wfResult = event + } + return event, nil +} + type eventFieldValue struct { field string value string diff --git a/internal/temporalcli/commands.workflow_view.go b/internal/temporalcli/commands.workflow_view.go index 278fcad35..4fad13055 100644 --- a/internal/temporalcli/commands.workflow_view.go +++ b/internal/temporalcli/commands.workflow_view.go @@ -572,6 +572,10 @@ func (c *TemporalWorkflowResultCommand) run(cctx *CommandContext, _ []string) er } func (c *TemporalWorkflowShowCommand) run(cctx *CommandContext, _ []string) error { + if c.Reverse && c.Follow { + return fmt.Errorf("--reverse cannot be combined with --follow") + } + // Call describe cl, err := dialClient(cctx, &c.Parent.ClientOptions) if err != nil { @@ -583,10 +587,12 @@ func (c *TemporalWorkflowShowCommand) run(cctx *CommandContext, _ []string) erro iter := &structuredHistoryIter{ ctx: cctx, client: cl, + namespace: c.Parent.Namespace, workflowID: c.WorkflowId, runID: c.RunId, includeDetails: c.Detailed, follow: c.Follow, + reverse: c.Reverse, } if !cctx.JSONOutput { cctx.Printer.Println(color.MagentaString("Progress:")) diff --git a/internal/temporalcli/commands.workflow_view_test.go b/internal/temporalcli/commands.workflow_view_test.go index 5abb30068..31cf22b33 100644 --- a/internal/temporalcli/commands.workflow_view_test.go +++ b/internal/temporalcli/commands.workflow_view_test.go @@ -420,6 +420,93 @@ func (s *SharedServerSuite) TestWorkflow_Show_JSON() { s.NotContains(out, "Results:") } +func (s *SharedServerSuite) TestWorkflow_Show_Reverse() { + s.Worker().OnDevWorkflow(func(ctx workflow.Context, a any) (any, error) { + workflow.GetSignalChannel(ctx, "my-signal").Receive(ctx, nil) + return "hi!", nil + }) + + run, err := s.Client.ExecuteWorkflow( + s.Context, + client.StartWorkflowOptions{TaskQueue: s.Worker().Options.TaskQueue}, + DevWorkflow, + "ignored", + ) + s.NoError(err) + s.NoError(s.Client.SignalWorkflow(s.Context, run.GetID(), "", "my-signal", nil)) + s.NoError(run.Get(s.Context, nil)) + + res := s.Execute( + "workflow", "show", + "--address", s.Address(), + "-w", run.GetID(), + "--reverse", + ) + s.NoError(res.Err) + out := res.Stdout.String() + completedIdx := strings.Index(out, "WorkflowExecutionCompleted") + startedIdx := strings.Index(out, "WorkflowExecutionStarted") + s.Greater(completedIdx, -1, "output should include the completed event") + s.Greater(startedIdx, -1, "output should include the started event") + s.Less(completedIdx, startedIdx, "completed event should appear before started event in reverse order") +} + +func (s *SharedServerSuite) TestWorkflow_Show_Reverse_JSON() { + s.Worker().OnDevWorkflow(func(ctx workflow.Context, a any) (any, error) { + workflow.GetSignalChannel(ctx, "my-signal").Receive(ctx, nil) + return "hi!", nil + }) + + run, err := s.Client.ExecuteWorkflow( + s.Context, + client.StartWorkflowOptions{TaskQueue: s.Worker().Options.TaskQueue}, + DevWorkflow, + "workflow-param", + ) + s.NoError(err) + s.NoError(s.Client.SignalWorkflow(s.Context, run.GetID(), "", "my-signal", nil)) + s.NoError(run.Get(s.Context, nil)) + + res := s.Execute( + "workflow", "show", + "--address", s.Address(), + "-w", run.GetID(), + "--reverse", + "-o", "json", + ) + s.NoError(res.Err) + out := res.Stdout.String() + + var parsed struct { + Events []struct { + EventId string `json:"eventId"` + } `json:"events"` + } + s.NoError(json.Unmarshal([]byte(out), &parsed)) + s.GreaterOrEqual(len(parsed.Events), 2) + prev := int64(-1) + for i, e := range parsed.Events { + id, err := strconv.ParseInt(e.EventId, 10, 64) + s.NoError(err) + if i > 0 { + s.Less(id, prev, "event %d (id=%d) should have smaller eventId than previous (%d) in reverse order", i, id, prev) + } + prev = id + } +} + +func (s *SharedServerSuite) TestWorkflow_Show_Reverse_RejectsFollow() { + res := s.Execute( + "workflow", "show", + "--address", s.Address(), + "-w", "does-not-matter", + "--reverse", + "--follow", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "--reverse cannot be combined with --follow") +} + func (s *SharedServerSuite) TestWorkflow_List() { s.Worker().OnDevWorkflow(func(ctx workflow.Context, a any) (any, error) { return a, nil diff --git a/internal/temporalcli/commands.yaml b/internal/temporalcli/commands.yaml index 5f4294f8f..52fa4982a 100644 --- a/internal/temporalcli/commands.yaml +++ b/internal/temporalcli/commands.yaml @@ -3759,6 +3759,10 @@ commands: description: | Display events as detailed sections instead of table. Does not apply to JSON output. + - name: reverse + type: bool + description: | + Fetch Event History newest-event-first. Cannot be combined with --follow. option-sets: - workflow-reference