relay-flow 0.2.8-alpha → 0.2.10-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -35
- package/cmd/relay-flow/commands_test.go +117 -8
- package/cmd/relay-flow/main.go +391 -83
- package/cmd/relay-flow/observability_test.go +204 -0
- package/cmd/relay-flow/onboarding.go +344 -0
- package/cmd/relay-flow/onboarding_test.go +515 -0
- package/cmd/relay-flow/render.go +629 -0
- package/cmd/relay-flow/repo_registration.go +219 -0
- package/cmd/relay-flow/serve.go +35 -1
- package/go.mod +12 -8
- package/go.sum +24 -17
- package/internal/execution/goworkflows/activities.go +9 -0
- package/internal/execution/goworkflows/engine.go +15 -0
- package/internal/execution/goworkflows/engine_test.go +3 -3
- package/internal/execution/goworkflows/interpreter.go +86 -3
- package/internal/execution/goworkflows/projection.go +16 -0
- package/internal/execution/projection/detail_test.go +259 -0
- package/internal/execution/projection/projection.go +290 -6
- package/internal/execution/projection/projection_test.go +3 -2
- package/internal/execution/temporal/activities.go +9 -0
- package/internal/execution/temporal/engine.go +13 -0
- package/internal/execution/temporal/interpreter.go +131 -3
- package/internal/execution/temporal/operations.go +174 -0
- package/internal/execution/temporal/operations_test.go +36 -0
- package/internal/harness/opencode/opencode_test.go +4 -4
- package/internal/harness/opencode/repo_setup.go +1 -1
- package/internal/harness/pi/prompt_test.go +3 -3
- package/internal/repo/service.go +34 -3
- package/internal/repo/service_test.go +22 -0
- package/internal/run/detail.go +277 -0
- package/internal/run/detail_test.go +71 -0
- package/internal/run/run.go +4 -0
- package/internal/runner/herdr/herdr.go +103 -4
- package/internal/runner/herdr/herdr_test.go +72 -2
- package/internal/runner/herdr/herdrcli/contract.go +12 -3
- package/internal/runner/herdr/herdrcli/herdrcli_test.go +17 -2
- package/internal/runner/herdr/herdrcli/operations.go +16 -0
- package/internal/runner/herdr/herdrcli/testdata/strict-herdr.sh +8 -0
- package/internal/runner/herdr/herdrcli/testdata/workspace-create.json +1 -0
- package/internal/runner/orca/orca.go +68 -9
- package/internal/runner/orca/orca_test.go +134 -2
- package/internal/runner/orca/orcacli/orcacli.go +21 -1
- package/internal/runner/orca/orcacli/orcacli_test.go +22 -0
- package/internal/runner/orca/orcacli/testdata/strict-orca.sh +5 -0
- package/internal/runner/runner.go +9 -0
- package/internal/server/api_test.go +19 -0
- package/internal/server/client.go +49 -0
- package/internal/server/fixture_test.go +7 -0
- package/internal/server/observability.go +109 -0
- package/internal/server/observability_test.go +60 -0
- package/internal/server/server.go +77 -0
- package/internal/task/jira/testdata/jira_search_issues.json +4 -4
- package/internal/workflow/workflow.go +4 -1
- package/internal/workflow/workflow_test.go +4 -4
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ package main
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
5
|
"fmt"
|
|
6
|
+
"path/filepath"
|
|
6
7
|
"strings"
|
|
7
8
|
|
|
8
9
|
"github.com/charmbracelet/huh"
|
|
@@ -13,6 +14,224 @@ import (
|
|
|
13
14
|
"github.com/rajpopat27/relay-flow/internal/task"
|
|
14
15
|
)
|
|
15
16
|
|
|
17
|
+
const addRepositorySelection = -1
|
|
18
|
+
|
|
19
|
+
func repoSelectionOptions(candidates []runner.RepoCandidate) []huh.Option[int] {
|
|
20
|
+
options := make([]huh.Option[int], 0, len(candidates)+1)
|
|
21
|
+
for i, candidate := range candidates {
|
|
22
|
+
label := strings.TrimSpace(candidate.Name)
|
|
23
|
+
if label == "" {
|
|
24
|
+
label = candidate.Path
|
|
25
|
+
}
|
|
26
|
+
options = append(options, huh.NewOption(label+" ("+candidate.Path+")", i))
|
|
27
|
+
}
|
|
28
|
+
options = append(options, huh.NewOption("[+] Add repository", addRepositorySelection))
|
|
29
|
+
return options
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func canonicalRepoPath(path string) string {
|
|
33
|
+
path = strings.TrimSpace(path)
|
|
34
|
+
if path == "" {
|
|
35
|
+
return ""
|
|
36
|
+
}
|
|
37
|
+
absolute, err := filepath.Abs(path)
|
|
38
|
+
if err != nil {
|
|
39
|
+
return filepath.Clean(path)
|
|
40
|
+
}
|
|
41
|
+
absolute = filepath.Clean(absolute)
|
|
42
|
+
if resolved, err := filepath.EvalSymlinks(absolute); err == nil {
|
|
43
|
+
return filepath.Clean(resolved)
|
|
44
|
+
}
|
|
45
|
+
return absolute
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func selectedRepoPaths(candidates []runner.RepoCandidate, selected []int) []string {
|
|
49
|
+
paths := make([]string, 0, len(selected))
|
|
50
|
+
seen := map[string]bool{}
|
|
51
|
+
for _, index := range selected {
|
|
52
|
+
if index < 0 || index >= len(candidates) {
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
path := canonicalRepoPath(candidates[index].Path)
|
|
56
|
+
if path == "" || seen[path] {
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
seen[path] = true
|
|
60
|
+
paths = append(paths, path)
|
|
61
|
+
}
|
|
62
|
+
return paths
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func selectedRepoIndices(candidates []runner.RepoCandidate, paths []string) []int {
|
|
66
|
+
wanted := map[string]bool{}
|
|
67
|
+
for _, path := range paths {
|
|
68
|
+
if path != "" {
|
|
69
|
+
wanted[path] = true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
selected := make([]int, 0, len(wanted))
|
|
73
|
+
for i, candidate := range candidates {
|
|
74
|
+
if wanted[canonicalRepoPath(candidate.Path)] {
|
|
75
|
+
selected = append(selected, i)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return selected
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func candidateAtPath(candidates []runner.RepoCandidate, path string) bool {
|
|
82
|
+
path = canonicalRepoPath(path)
|
|
83
|
+
for _, candidate := range candidates {
|
|
84
|
+
if canonicalRepoPath(candidate.Path) == path {
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func mergeRepoCandidateNames(candidates []runner.RepoCandidate, names map[string]string) []runner.RepoCandidate {
|
|
92
|
+
out := make([]runner.RepoCandidate, len(candidates))
|
|
93
|
+
copy(out, candidates)
|
|
94
|
+
for i := range out {
|
|
95
|
+
if name := names[canonicalRepoPath(out[i].Path)]; name != "" {
|
|
96
|
+
out[i].Name = name
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func validateAddedRepo(candidate runner.RepoCandidate, candidates []runner.RepoCandidate, registered []repo.Info) error {
|
|
103
|
+
candidate.Name = strings.TrimSpace(candidate.Name)
|
|
104
|
+
candidate.Path = canonicalRepoPath(candidate.Path)
|
|
105
|
+
if candidate.Path == "" {
|
|
106
|
+
return fmt.Errorf("repository path is required")
|
|
107
|
+
}
|
|
108
|
+
if candidate.Name == "" {
|
|
109
|
+
return fmt.Errorf("repository name is required")
|
|
110
|
+
}
|
|
111
|
+
for _, existing := range candidates {
|
|
112
|
+
if canonicalRepoPath(existing.Path) == candidate.Path {
|
|
113
|
+
return fmt.Errorf("repository path %q is already discovered as %q", candidate.Path, existing.Name)
|
|
114
|
+
}
|
|
115
|
+
if strings.TrimSpace(existing.Name) == candidate.Name {
|
|
116
|
+
return fmt.Errorf("repository name %q is already discovered", candidate.Name)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for _, existing := range registered {
|
|
120
|
+
if canonicalRepoPath(existing.Path) == candidate.Path {
|
|
121
|
+
return fmt.Errorf("repository path %q is already registered as %q", candidate.Path, existing.Name)
|
|
122
|
+
}
|
|
123
|
+
if strings.TrimSpace(existing.Name) == candidate.Name {
|
|
124
|
+
return fmt.Errorf("repository name %q is already registered", candidate.Name)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return nil
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func promptAddedRepo() (runner.RepoCandidate, error) {
|
|
131
|
+
var path, name string
|
|
132
|
+
pathInput := huh.NewInput().Title("Repository path").Value(&path).Validate(func(value string) error {
|
|
133
|
+
if strings.TrimSpace(value) == "" {
|
|
134
|
+
return fmt.Errorf("repository path is required")
|
|
135
|
+
}
|
|
136
|
+
return nil
|
|
137
|
+
})
|
|
138
|
+
nameInput := huh.NewInput().Title("Registered name").Value(&name).Validate(func(value string) error {
|
|
139
|
+
if strings.TrimSpace(value) == "" {
|
|
140
|
+
return fmt.Errorf("repository name is required")
|
|
141
|
+
}
|
|
142
|
+
return nil
|
|
143
|
+
})
|
|
144
|
+
if err := huh.NewForm(huh.NewGroup(pathInput, nameInput)).Run(); err != nil {
|
|
145
|
+
return runner.RepoCandidate{}, err
|
|
146
|
+
}
|
|
147
|
+
return runner.RepoCandidate{Name: strings.TrimSpace(name), Path: canonicalRepoPath(path)}, nil
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
type repoSelectionPrompt func([]runner.RepoCandidate, []int) ([]int, error)
|
|
151
|
+
type repoAddPrompt func() (runner.RepoCandidate, error)
|
|
152
|
+
|
|
153
|
+
func selectReposInteractive(ctx context.Context, c *server.Client, candidates []runner.RepoCandidate, registered []repo.Info) ([]runner.RepoCandidate, []int, error) {
|
|
154
|
+
return selectReposInteractiveWithPrompts(ctx, c, candidates, registered, promptRepoSelection, promptAddedRepo)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func promptRepoSelection(candidates []runner.RepoCandidate, selected []int) ([]int, error) {
|
|
158
|
+
pick := huh.NewForm(huh.NewGroup(repoMultiSelect(repoSelectionOptions(candidates), &selected)))
|
|
159
|
+
if err := pick.Run(); err != nil {
|
|
160
|
+
return nil, err
|
|
161
|
+
}
|
|
162
|
+
return selected, nil
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
func selectReposInteractiveWithPrompts(ctx context.Context, c *server.Client, candidates []runner.RepoCandidate, registered []repo.Info, selectPrompt repoSelectionPrompt, addPrompt repoAddPrompt) ([]runner.RepoCandidate, []int, error) {
|
|
166
|
+
selectedPaths := []string{}
|
|
167
|
+
nameOverrides := map[string]string{}
|
|
168
|
+
addedCandidates := []runner.RepoCandidate{}
|
|
169
|
+
for {
|
|
170
|
+
candidates = mergeRepoCandidateNames(candidates, nameOverrides)
|
|
171
|
+
selected, err := selectPrompt(candidates, selectedRepoIndices(candidates, selectedPaths))
|
|
172
|
+
if err != nil {
|
|
173
|
+
return nil, nil, err
|
|
174
|
+
}
|
|
175
|
+
selectedPaths = selectedRepoPaths(candidates, selected)
|
|
176
|
+
add := false
|
|
177
|
+
for _, value := range selected {
|
|
178
|
+
if value == addRepositorySelection {
|
|
179
|
+
add = true
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if !add {
|
|
184
|
+
if len(selectedPaths) == 0 {
|
|
185
|
+
return nil, nil, fmt.Errorf("select at least one repository")
|
|
186
|
+
}
|
|
187
|
+
return candidates, selectedRepoIndices(candidates, selectedPaths), nil
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
added, err := addPrompt()
|
|
191
|
+
if err != nil {
|
|
192
|
+
return nil, nil, err
|
|
193
|
+
}
|
|
194
|
+
knownCandidates := append(append([]runner.RepoCandidate(nil), candidates...), addedCandidates...)
|
|
195
|
+
if err := validateAddedRepo(added, knownCandidates, registered); err != nil {
|
|
196
|
+
return nil, nil, err
|
|
197
|
+
}
|
|
198
|
+
if err := c.EnsureRepo(ctx, added); err != nil {
|
|
199
|
+
return nil, nil, fmt.Errorf("ensure repository %q: %w", added.Name, err)
|
|
200
|
+
}
|
|
201
|
+
path := canonicalRepoPath(added.Path)
|
|
202
|
+
nameOverrides[path] = added.Name
|
|
203
|
+
addedCandidates = append(addedCandidates, added)
|
|
204
|
+
selectedPaths = append(selectedPaths, path)
|
|
205
|
+
|
|
206
|
+
refreshed, err := c.DiscoverRepos(ctx)
|
|
207
|
+
if err != nil {
|
|
208
|
+
return nil, nil, err
|
|
209
|
+
}
|
|
210
|
+
refreshed = mergeRepoCandidateNames(refreshed, nameOverrides)
|
|
211
|
+
// Preserve selected entries across a runner refresh even if the
|
|
212
|
+
// external runner's listing is briefly eventually consistent.
|
|
213
|
+
for _, old := range candidates {
|
|
214
|
+
oldPath := canonicalRepoPath(old.Path)
|
|
215
|
+
if containsPath(selectedPaths, oldPath) && !candidateAtPath(refreshed, oldPath) {
|
|
216
|
+
refreshed = append(refreshed, old)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if !candidateAtPath(refreshed, path) {
|
|
220
|
+
refreshed = append(refreshed, added)
|
|
221
|
+
}
|
|
222
|
+
candidates = refreshed
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
func containsPath(paths []string, want string) bool {
|
|
227
|
+
for _, path := range paths {
|
|
228
|
+
if path == want {
|
|
229
|
+
return true
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
|
|
16
235
|
// loadRepoRegistration asks the selected task plugin for the fields needed by
|
|
17
236
|
// registration. A second pass lets a plugin expose dependent choices after a
|
|
18
237
|
// value such as a project has been supplied.
|
package/cmd/relay-flow/serve.go
CHANGED
|
@@ -504,6 +504,7 @@ func (r repoExists) Exists(name string) bool {
|
|
|
504
504
|
type durableEngine interface {
|
|
505
505
|
runsvc.Executor
|
|
506
506
|
runsvc.RunQueries
|
|
507
|
+
GetRunDetail(context.Context, runsvc.ID) (runsvc.RunDetail, error)
|
|
507
508
|
Start(context.Context) error
|
|
508
509
|
Shutdown(context.Context) error
|
|
509
510
|
HasProcessedReport(context.Context, runsvc.ID, string) (bool, error)
|
|
@@ -523,11 +524,34 @@ func (d *serveDeps) SubmitWorkflow(ctx context.Context, yaml []byte) (*workflow.
|
|
|
523
524
|
return d.wf.Submit(ctx, yaml)
|
|
524
525
|
}
|
|
525
526
|
func (d *serveDeps) GetWorkflow(_ context.Context, name string) (*workflow.Workflow, error) {
|
|
526
|
-
|
|
527
|
+
wf, err := d.wf.Get(name)
|
|
528
|
+
if err != nil {
|
|
529
|
+
return nil, fmt.Errorf("%w: %v", server.ErrNotFound, err)
|
|
530
|
+
}
|
|
531
|
+
return wf, nil
|
|
527
532
|
}
|
|
528
533
|
func (d *serveDeps) ListWorkflows(context.Context) ([]*workflow.Workflow, error) {
|
|
529
534
|
return d.wf.List(), nil
|
|
530
535
|
}
|
|
536
|
+
func (d *serveDeps) ListWorkflowSummaries(ctx context.Context) ([]server.WorkflowSummary, error) {
|
|
537
|
+
workflows := d.wf.List()
|
|
538
|
+
runs, err := d.engine.ListRuns(ctx, runsvc.Filter{})
|
|
539
|
+
if err != nil {
|
|
540
|
+
return nil, err
|
|
541
|
+
}
|
|
542
|
+
return server.BuildWorkflowSummaries(workflows, runs), nil
|
|
543
|
+
}
|
|
544
|
+
func (d *serveDeps) GetWorkflowDetail(ctx context.Context, name string) (server.WorkflowDetail, error) {
|
|
545
|
+
wf, err := d.wf.Get(name)
|
|
546
|
+
if err != nil {
|
|
547
|
+
return server.WorkflowDetail{}, fmt.Errorf("%w: %v", server.ErrNotFound, err)
|
|
548
|
+
}
|
|
549
|
+
runs, err := d.engine.ListRuns(ctx, runsvc.Filter{Workflow: name})
|
|
550
|
+
if err != nil {
|
|
551
|
+
return server.WorkflowDetail{}, err
|
|
552
|
+
}
|
|
553
|
+
return server.BuildWorkflowDetail(wf, runs), nil
|
|
554
|
+
}
|
|
531
555
|
func (d *serveDeps) RemoveWorkflow(ctx context.Context, name string) error {
|
|
532
556
|
return d.wf.Remove(ctx, name)
|
|
533
557
|
}
|
|
@@ -538,6 +562,13 @@ func (d *serveDeps) ListRuns(ctx context.Context, filter runsvc.Filter) ([]runsv
|
|
|
538
562
|
func (d *serveDeps) GetRunByTicket(ctx context.Context, ticket string) (runsvc.Run, error) {
|
|
539
563
|
return d.engine.FindRunByTicket(ctx, ticket)
|
|
540
564
|
}
|
|
565
|
+
func (d *serveDeps) GetRunDetail(ctx context.Context, ticket string) (runsvc.RunDetail, error) {
|
|
566
|
+
base, err := d.engine.FindRunByTicket(ctx, ticket)
|
|
567
|
+
if err != nil {
|
|
568
|
+
return runsvc.RunDetail{}, err
|
|
569
|
+
}
|
|
570
|
+
return d.engine.GetRunDetail(ctx, base.ID)
|
|
571
|
+
}
|
|
541
572
|
func (d *serveDeps) RestartRun(ctx context.Context, ticket string) (runsvc.Run, error) {
|
|
542
573
|
rn, err := d.runManager.RestartByTicket(ctx, ticket)
|
|
543
574
|
if err != nil {
|
|
@@ -565,6 +596,9 @@ func (d *serveDeps) RegisterNodeSession(ctx context.Context, registration runsvc
|
|
|
565
596
|
func (d *serveDeps) DiscoverRepos(ctx context.Context) ([]runner.RepoCandidate, error) {
|
|
566
597
|
return d.repos.Discover(ctx)
|
|
567
598
|
}
|
|
599
|
+
func (d *serveDeps) EnsureRepo(ctx context.Context, candidate runner.RepoCandidate) error {
|
|
600
|
+
return d.repos.EnsureRepo(ctx, candidate.Name, candidate.Path)
|
|
601
|
+
}
|
|
568
602
|
func (d *serveDeps) TaskRegistrationFields(ctx context.Context, values config.RawValues) ([]task.RegistrationField, error) {
|
|
569
603
|
return d.repos.RegistrationFields(ctx, values)
|
|
570
604
|
}
|
package/go.mod
CHANGED
|
@@ -3,6 +3,7 @@ module github.com/rajpopat27/relay-flow
|
|
|
3
3
|
go 1.26.0
|
|
4
4
|
|
|
5
5
|
require (
|
|
6
|
+
github.com/charmbracelet/huh/spinner v0.0.0-20260223110133-9dc45e34a40b
|
|
6
7
|
github.com/cschleiden/go-workflows v1.4.2
|
|
7
8
|
github.com/google/renameio/v2 v2.0.1
|
|
8
9
|
github.com/google/uuid v1.6.0
|
|
@@ -16,6 +17,9 @@ require (
|
|
|
16
17
|
|
|
17
18
|
require (
|
|
18
19
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
|
20
|
+
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
|
21
|
+
github.com/clipperhouse/stringish v0.1.1 // indirect
|
|
22
|
+
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
|
19
23
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
|
20
24
|
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
|
|
21
25
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
|
|
@@ -27,15 +31,15 @@ require (
|
|
|
27
31
|
github.com/benbjohnson/clock v1.3.0 // indirect
|
|
28
32
|
github.com/catppuccin/go v0.3.0 // indirect
|
|
29
33
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
|
30
|
-
github.com/charmbracelet/bubbles
|
|
31
|
-
github.com/charmbracelet/bubbletea v1.3.
|
|
32
|
-
github.com/charmbracelet/colorprofile v0.
|
|
34
|
+
github.com/charmbracelet/bubbles v1.0.0 // indirect
|
|
35
|
+
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
|
36
|
+
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
|
33
37
|
github.com/charmbracelet/huh v1.0.0
|
|
34
38
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
|
35
|
-
github.com/charmbracelet/x/ansi v0.
|
|
36
|
-
github.com/charmbracelet/x/cellbuf v0.0.
|
|
39
|
+
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
|
40
|
+
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
|
37
41
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
|
38
|
-
github.com/charmbracelet/x/term v0.2.
|
|
42
|
+
github.com/charmbracelet/x/term v0.2.2 // indirect
|
|
39
43
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
40
44
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
41
45
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
|
@@ -52,10 +56,10 @@ require (
|
|
|
52
56
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
|
53
57
|
github.com/jellydator/ttlcache/v3 v3.0.0 // indirect
|
|
54
58
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
|
55
|
-
github.com/lucasb-eyer/go-colorful v1.
|
|
59
|
+
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
|
56
60
|
github.com/mattn/go-isatty v0.0.20
|
|
57
61
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
|
58
|
-
github.com/mattn/go-runewidth v0.0.
|
|
62
|
+
github.com/mattn/go-runewidth v0.0.19 // indirect
|
|
59
63
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
|
60
64
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
|
61
65
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
package/go.sum
CHANGED
|
@@ -14,20 +14,22 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
|
|
|
14
14
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
|
15
15
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
|
16
16
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
17
|
-
github.com/charmbracelet/bubbles
|
|
18
|
-
github.com/charmbracelet/bubbles
|
|
19
|
-
github.com/charmbracelet/bubbletea v1.3.
|
|
20
|
-
github.com/charmbracelet/bubbletea v1.3.
|
|
21
|
-
github.com/charmbracelet/colorprofile v0.
|
|
22
|
-
github.com/charmbracelet/colorprofile v0.
|
|
17
|
+
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
|
18
|
+
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
|
19
|
+
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
|
20
|
+
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
|
21
|
+
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
|
22
|
+
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
|
23
23
|
github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw=
|
|
24
24
|
github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
|
|
25
|
+
github.com/charmbracelet/huh/spinner v0.0.0-20260223110133-9dc45e34a40b h1:deQbW7eR/gYwkXonGX6a1now6H6f8v4kfv0OIKECu0I=
|
|
26
|
+
github.com/charmbracelet/huh/spinner v0.0.0-20260223110133-9dc45e34a40b/go.mod h1:Y68nuKJuC/Q2lmiq18EkHWkVWi2VGLrwaOfOyPKLkkE=
|
|
25
27
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
|
26
28
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
|
27
|
-
github.com/charmbracelet/x/ansi v0.
|
|
28
|
-
github.com/charmbracelet/x/ansi v0.
|
|
29
|
-
github.com/charmbracelet/x/cellbuf v0.0.
|
|
30
|
-
github.com/charmbracelet/x/cellbuf v0.0.
|
|
29
|
+
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
|
30
|
+
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
|
31
|
+
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
|
32
|
+
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
|
31
33
|
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
|
32
34
|
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
|
33
35
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
|
@@ -36,12 +38,18 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payR
|
|
|
36
38
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
|
37
39
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
|
38
40
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
|
39
|
-
github.com/charmbracelet/x/term v0.2.
|
|
40
|
-
github.com/charmbracelet/x/term v0.2.
|
|
41
|
+
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
|
42
|
+
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
|
41
43
|
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
|
42
44
|
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
|
43
45
|
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
|
|
44
46
|
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
|
|
47
|
+
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
|
48
|
+
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
|
49
|
+
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
|
50
|
+
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
|
51
|
+
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
|
52
|
+
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
|
45
53
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
|
46
54
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
|
47
55
|
github.com/cschleiden/go-workflows v1.4.2 h1:s7wgx3iKvFmwJVzB7wSuUFqbplcpCUGrn8u1J4/T+2c=
|
|
@@ -98,14 +106,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
|
98
106
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
99
107
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
|
100
108
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
|
101
|
-
github.com/lucasb-eyer/go-colorful v1.
|
|
102
|
-
github.com/lucasb-eyer/go-colorful v1.
|
|
109
|
+
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
|
110
|
+
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
103
111
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
104
112
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
105
113
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
|
106
114
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
|
107
|
-
github.com/mattn/go-runewidth v0.0.
|
|
108
|
-
github.com/mattn/go-runewidth v0.0.
|
|
115
|
+
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
|
116
|
+
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
|
109
117
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
|
110
118
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
|
111
119
|
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
|
@@ -124,7 +132,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|
|
124
132
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
125
133
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
|
126
134
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
127
|
-
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
128
135
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|
129
136
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
|
130
137
|
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
|
@@ -402,6 +402,15 @@ func (a *Activities) CompleteMailbox(ctx context.Context, w run.Work, mailbox ta
|
|
|
402
402
|
|
|
403
403
|
// Projection activities: idempotent read-model updates.
|
|
404
404
|
|
|
405
|
+
func (a *Activities) ProjectionUpsertStep(ctx context.Context, step run.StepEntry) error {
|
|
406
|
+
if err := a.Runs.upsertStep(ctx, step); err != nil {
|
|
407
|
+
// Step detail is a cache. Never let a display-projection failure block
|
|
408
|
+
// route selection, report acceptance, or durable graph progression.
|
|
409
|
+
slog.Warn("step projection unavailable", "runID", string(step.RunID), "node", step.Node, "sequence", step.Sequence, "error", err)
|
|
410
|
+
}
|
|
411
|
+
return nil
|
|
412
|
+
}
|
|
413
|
+
|
|
405
414
|
func (a *Activities) ProjectionUpdateNode(ctx context.Context, id run.ID, state run.State, node string, visit run.NodeVisitID) error {
|
|
406
415
|
return a.Runs.updateNode(ctx, id, state, node, visit)
|
|
407
416
|
}
|
|
@@ -211,6 +211,7 @@ func (e *Engine) registerActivities() error {
|
|
|
211
211
|
a.CompleteMailbox,
|
|
212
212
|
a.ProjectionUpdateNodeRuntimeVisit,
|
|
213
213
|
a.ProjectionRecordProcessedReport,
|
|
214
|
+
a.ProjectionUpsertStep,
|
|
214
215
|
a.ProjectionUpdateNode,
|
|
215
216
|
a.ProjectionUpdateState,
|
|
216
217
|
a.ProjectionUpdateRetry,
|
|
@@ -502,6 +503,20 @@ func (e *Engine) FindRunByTicket(ctx context.Context, ticket string) (run.Run, e
|
|
|
502
503
|
return e.runs.findByTicket(ctx, ticket)
|
|
503
504
|
}
|
|
504
505
|
|
|
506
|
+
func (e *Engine) GetRunDetail(ctx context.Context, id run.ID) (run.RunDetail, error) {
|
|
507
|
+
detail, err := e.runs.getDetail(ctx, id)
|
|
508
|
+
if err != nil {
|
|
509
|
+
return run.RunDetail{}, err
|
|
510
|
+
}
|
|
511
|
+
// Static definitions are used only to add explicit pending display rows;
|
|
512
|
+
// the projection remains a cache and never influences execution.
|
|
513
|
+
if wf, wfErr := e.workflowOf(ctx, id); wfErr == nil {
|
|
514
|
+
detail.AddPendingNodes(*wf)
|
|
515
|
+
detail.DeriveInspectionFields(time.Now().UTC())
|
|
516
|
+
}
|
|
517
|
+
return detail, nil
|
|
518
|
+
}
|
|
519
|
+
|
|
505
520
|
func (e *Engine) ListRuns(ctx context.Context, filter run.Filter) ([]run.Run, error) {
|
|
506
521
|
return e.runs.list(ctx, filter)
|
|
507
522
|
}
|
|
@@ -187,7 +187,7 @@ func TestRunBeginsAtStartAndFollowsEntryEdge(t *testing.T) {
|
|
|
187
187
|
|
|
188
188
|
wf := linearWorkflow(false)
|
|
189
189
|
node := wf.Nodes["coding"]
|
|
190
|
-
node.NudgePrompt = "Continue {{ticket}} at {{node}}. Valid next steps: {{nextSteps}}."
|
|
190
|
+
node.NudgePrompt = "Continue {{ticket}} at {{node}} in {{mailbox}}. Valid next steps: {{nextSteps}}."
|
|
191
191
|
wf.Nodes["coding"] = node
|
|
192
192
|
rid, err := startRun(engine, wf)
|
|
193
193
|
if err != nil {
|
|
@@ -221,8 +221,8 @@ func TestRunBeginsAtStartAndFollowsEntryEdge(t *testing.T) {
|
|
|
221
221
|
if call.NudgeTemplate != node.NudgePrompt {
|
|
222
222
|
t.Fatalf("nudge passed to harness = %q, want raw template %q", call.NudgeTemplate, node.NudgePrompt)
|
|
223
223
|
}
|
|
224
|
-
if call.Data.Ticket != "PAY-101" || call.Data.Workflow != wf.Name || call.Data.Repo != "payments" || call.Data.Node != "coding" || call.Data.NextSteps == "" {
|
|
225
|
-
t.Fatalf("nudge prompt data = %+v, want current workflow values", call.Data)
|
|
224
|
+
if call.Data.Ticket != "PAY-101" || call.Data.Workflow != wf.Name || call.Data.Repo != "payments" || call.Data.Node != "coding" || call.Data.Mailbox != "PAY-101-coding" || call.Data.NextSteps == "" {
|
|
225
|
+
t.Fatalf("nudge prompt data = %+v, want current workflow and mailbox values", call.Data)
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
// Pre-edge gate: before following the start edge the run ensures the
|
|
@@ -142,6 +142,23 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
142
142
|
return err
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// The step timeline is a derived display projection. Its sequence is
|
|
146
|
+
// deterministic for replay and is never consulted for routing.
|
|
147
|
+
stepSequence := int64(0)
|
|
148
|
+
target, err := wf.StartTarget()
|
|
149
|
+
if err != nil {
|
|
150
|
+
return err
|
|
151
|
+
}
|
|
152
|
+
startStarted := goworkflow.Now(ctx).UTC()
|
|
153
|
+
if _, err := retryLoop(ctx, start.ID, a, work, "start",
|
|
154
|
+
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
155
|
+
return goworkflow.ExecuteActivity[struct{}](ctx2, noNativeRetries, a.ProjectionUpsertStep,
|
|
156
|
+
run.StepEntry{RunID: start.ID, Sequence: stepSequence, Node: "start", NodeType: "lifecycle",
|
|
157
|
+
Status: run.StepRunning, StartedAt: &startStarted, Depth: 1})
|
|
158
|
+
}); err != nil {
|
|
159
|
+
return err
|
|
160
|
+
}
|
|
161
|
+
|
|
145
162
|
// Process the reserved start taskConfig (parent target).
|
|
146
163
|
startNode := wf.Nodes["start"]
|
|
147
164
|
if _, err := retryLoop(ctx, start.ID, a, work, "start",
|
|
@@ -160,13 +177,21 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
160
177
|
return err
|
|
161
178
|
}
|
|
162
179
|
|
|
163
|
-
|
|
164
|
-
if err
|
|
180
|
+
startFinished := goworkflow.Now(ctx).UTC()
|
|
181
|
+
if _, err := retryLoop(ctx, start.ID, a, work, "start",
|
|
182
|
+
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
183
|
+
return goworkflow.ExecuteActivity[struct{}](ctx2, noNativeRetries, a.ProjectionUpsertStep,
|
|
184
|
+
run.StepEntry{RunID: start.ID, Sequence: stepSequence, Node: "start", NodeType: "lifecycle",
|
|
185
|
+
Status: run.StepSucceeded, StartedAt: &startStarted, FinishedAt: &startFinished,
|
|
186
|
+
Route: target, Depth: 1})
|
|
187
|
+
}); err != nil {
|
|
165
188
|
return err
|
|
166
189
|
}
|
|
167
190
|
|
|
168
191
|
current := target
|
|
169
192
|
seenReportIDs := map[string]bool{}
|
|
193
|
+
lastStepByNode := map[string]int64{}
|
|
194
|
+
lastDepthByNode := map[string]int{}
|
|
170
195
|
for current != "end" {
|
|
171
196
|
node := wf.Nodes[current]
|
|
172
197
|
|
|
@@ -178,6 +203,27 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
178
203
|
return err
|
|
179
204
|
}
|
|
180
205
|
visitID := run.NodeVisitID(visit)
|
|
206
|
+
stepSequence++
|
|
207
|
+
stepParent := int64(0)
|
|
208
|
+
stepDepth := 1
|
|
209
|
+
if previous, ok := lastStepByNode[current]; ok {
|
|
210
|
+
stepParent = previous
|
|
211
|
+
stepDepth = lastDepthByNode[current] + 1
|
|
212
|
+
}
|
|
213
|
+
stepStarted := goworkflow.Now(ctx).UTC()
|
|
214
|
+
if _, err := retryLoop(ctx, start.ID, a, work, current,
|
|
215
|
+
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
216
|
+
return goworkflow.ExecuteActivity[struct{}](ctx2, noNativeRetries, a.ProjectionUpsertStep,
|
|
217
|
+
run.StepEntry{RunID: start.ID, Sequence: stepSequence, Node: current,
|
|
218
|
+
NodeVisitID: visitID, NodeType: string(node.Type), Status: run.StepRunning,
|
|
219
|
+
StartedAt: &stepStarted, ParentSequence: stepParent, Depth: stepDepth,
|
|
220
|
+
Runtime: node.Agent})
|
|
221
|
+
}); err != nil {
|
|
222
|
+
return err
|
|
223
|
+
}
|
|
224
|
+
lastStepByNode[current] = stepSequence
|
|
225
|
+
lastDepthByNode[current] = stepDepth
|
|
226
|
+
|
|
181
227
|
runtime, err := retryLoop(ctx, start.ID, a, work, current,
|
|
182
228
|
func(ctx2 goworkflow.Context) goworkflow.Future[NodeRuntime] {
|
|
183
229
|
return goworkflow.ExecuteActivity[NodeRuntime](ctx2, noNativeRetries,
|
|
@@ -328,6 +374,23 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
328
374
|
}); err != nil {
|
|
329
375
|
return err
|
|
330
376
|
}
|
|
377
|
+
stepStatus := run.StepSucceeded
|
|
378
|
+
stepMessage := report.Summary.Completed
|
|
379
|
+
if report.Status == workflow.OutcomeFailure {
|
|
380
|
+
stepStatus = run.StepFailed
|
|
381
|
+
stepMessage = report.Summary.IssuesDiscovered
|
|
382
|
+
}
|
|
383
|
+
stepFinished := goworkflow.Now(ctx).UTC()
|
|
384
|
+
if _, err := retryLoop(ctx, start.ID, a, work, current,
|
|
385
|
+
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
386
|
+
return goworkflow.ExecuteActivity[struct{}](ctx2, noNativeRetries, a.ProjectionUpsertStep,
|
|
387
|
+
run.StepEntry{RunID: start.ID, Sequence: stepSequence, Node: current,
|
|
388
|
+
NodeVisitID: visitID, NodeType: string(node.Type), Status: stepStatus,
|
|
389
|
+
StartedAt: &stepStarted, FinishedAt: &stepFinished, Message: stepMessage,
|
|
390
|
+
Route: report.NextStep, Runtime: node.Agent, ParentSequence: stepParent, Depth: stepDepth})
|
|
391
|
+
}); err != nil {
|
|
392
|
+
return err
|
|
393
|
+
}
|
|
331
394
|
|
|
332
395
|
// Ordered transition: summary -> feedback (selected next only) ->
|
|
333
396
|
// complete current -> process next node.
|
|
@@ -389,7 +452,19 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
389
452
|
}
|
|
390
453
|
|
|
391
454
|
// end: apply end task config, then optional runner cleanup, then mark
|
|
392
|
-
// the run completed.
|
|
455
|
+
// the run completed. Record the lifecycle row before setup so a lost
|
|
456
|
+
// terminal upsert can still be finalized by the authoritative completed
|
|
457
|
+
// state update.
|
|
458
|
+
stepSequence++
|
|
459
|
+
endStarted := goworkflow.Now(ctx).UTC()
|
|
460
|
+
endStep := run.StepEntry{RunID: start.ID, Sequence: stepSequence, Node: "end", NodeType: "lifecycle",
|
|
461
|
+
Status: run.StepRunning, StartedAt: &endStarted, Depth: 1}
|
|
462
|
+
if _, err := retryLoop(ctx, start.ID, a, work, "end",
|
|
463
|
+
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
464
|
+
return goworkflow.ExecuteActivity[struct{}](ctx2, noNativeRetries, a.ProjectionUpsertStep, endStep)
|
|
465
|
+
}); err != nil {
|
|
466
|
+
return err
|
|
467
|
+
}
|
|
393
468
|
endNode := wf.Nodes["end"]
|
|
394
469
|
if _, err := retryLoop(ctx, start.ID, a, work, "end",
|
|
395
470
|
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
@@ -424,6 +499,14 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
424
499
|
return err
|
|
425
500
|
}
|
|
426
501
|
}
|
|
502
|
+
endFinished := goworkflow.Now(ctx).UTC()
|
|
503
|
+
endStep.Status, endStep.FinishedAt = run.StepSucceeded, &endFinished
|
|
504
|
+
if _, err := retryLoop(ctx, start.ID, a, work, "end",
|
|
505
|
+
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
506
|
+
return goworkflow.ExecuteActivity[struct{}](ctx2, noNativeRetries, a.ProjectionUpsertStep, endStep)
|
|
507
|
+
}); err != nil {
|
|
508
|
+
return err
|
|
509
|
+
}
|
|
427
510
|
if _, err := retryLoop(ctx, start.ID, a, work, "",
|
|
428
511
|
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
429
512
|
now := goworkflow.Now(ctx).UTC()
|
|
@@ -54,6 +54,18 @@ func (p *RunProjection) updateNode(ctx context.Context, id run.ID, state run.Sta
|
|
|
54
54
|
return p.shared().UpdateNode(ctx, id, state, node, visit)
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
func (p *RunProjection) upsertStep(ctx context.Context, step run.StepEntry) error {
|
|
58
|
+
return p.shared().UpsertStep(ctx, step)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func (p *RunProjection) listSteps(ctx context.Context, id run.ID) ([]run.StepEntry, error) {
|
|
62
|
+
return p.shared().ListSteps(ctx, id)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func (p *RunProjection) getDetail(ctx context.Context, id run.ID) (run.RunDetail, error) {
|
|
66
|
+
return p.shared().GetDetail(ctx, id)
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
func (p *RunProjection) getNodeRuntime(ctx context.Context, id run.ID, node string) (NodeRuntime, error) {
|
|
58
70
|
return p.shared().GetNodeRuntime(ctx, id, node)
|
|
59
71
|
}
|
|
@@ -102,6 +114,10 @@ func (p *RunProjection) get(ctx context.Context, id run.ID) (run.Run, error) {
|
|
|
102
114
|
return p.shared().Get(ctx, id)
|
|
103
115
|
}
|
|
104
116
|
|
|
117
|
+
func (p *RunProjection) detail(ctx context.Context, id run.ID) (run.RunDetail, error) {
|
|
118
|
+
return p.getDetail(ctx, id)
|
|
119
|
+
}
|
|
120
|
+
|
|
105
121
|
func (p *RunProjection) findByTicket(ctx context.Context, ticket string) (run.Run, error) {
|
|
106
122
|
return p.shared().FindByTicket(ctx, ticket)
|
|
107
123
|
}
|