relay-flow 0.2.6-alpha → 0.2.8-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.
@@ -0,0 +1,235 @@
1
+ package main
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "strings"
7
+
8
+ "github.com/charmbracelet/huh"
9
+ "github.com/rajpopat27/relay-flow/internal/config"
10
+ "github.com/rajpopat27/relay-flow/internal/repo"
11
+ "github.com/rajpopat27/relay-flow/internal/runner"
12
+ "github.com/rajpopat27/relay-flow/internal/server"
13
+ "github.com/rajpopat27/relay-flow/internal/task"
14
+ )
15
+
16
+ // loadRepoRegistration asks the selected task plugin for the fields needed by
17
+ // registration. A second pass lets a plugin expose dependent choices after a
18
+ // value such as a project has been supplied.
19
+ func loadRepoRegistration(ctx context.Context, c *server.Client, supplied kvFlags) (task.Registration, error) {
20
+ initial, err := c.RepoRegistrationFields(ctx, nil)
21
+ if err != nil {
22
+ return task.Registration{}, err
23
+ }
24
+ if len(supplied) == 0 {
25
+ return initial, nil
26
+ }
27
+ dependent, err := c.RepoRegistrationFields(ctx, flatRegistrationValues(supplied))
28
+ if err != nil {
29
+ return task.Registration{}, err
30
+ }
31
+ return mergeRegistration(initial, dependent), nil
32
+ }
33
+
34
+ func flatRegistrationValues(values kvFlags) config.RawValues {
35
+ out := make(config.RawValues, len(values))
36
+ for key, value := range values {
37
+ out[key] = value
38
+ }
39
+ return out
40
+ }
41
+
42
+ func mergeRegistration(first, second task.Registration) task.Registration {
43
+ fields := make([]task.RegistrationField, 0, len(first.Fields)+len(second.Fields))
44
+ positions := map[string]int{}
45
+ for _, field := range append(append([]task.RegistrationField(nil), first.Fields...), second.Fields...) {
46
+ if index, ok := positions[field.Key]; ok {
47
+ fields[index] = field
48
+ continue
49
+ }
50
+ positions[field.Key] = len(fields)
51
+ fields = append(fields, field)
52
+ }
53
+ return task.Registration{Fields: fields}
54
+ }
55
+
56
+ // promptRepoRegistration renders only metadata returned by the task plugin.
57
+ // The core CLI does not know which values are Jira statuses or how they are
58
+ // validated.
59
+ func promptRepoRegistration(reg task.Registration, values kvFlags) error {
60
+ for _, field := range reg.Fields {
61
+ if field.Derived || values[field.Key] != "" {
62
+ continue
63
+ }
64
+ title := field.Title
65
+ if title == "" {
66
+ title = field.Key
67
+ }
68
+ if len(field.Options) > 0 {
69
+ selected := field.Default
70
+ if err := huh.NewForm(huh.NewGroup(
71
+ huh.NewSelect[string]().Title(title).Options(registrationSelectOptions(field)...).Value(&selected),
72
+ )).Run(); err != nil {
73
+ return err
74
+ }
75
+ if strings.TrimSpace(selected) == "" {
76
+ return fmt.Errorf("task field %q requires a value", field.Key)
77
+ }
78
+ values[field.Key] = selected
79
+ continue
80
+ }
81
+ value := field.Default
82
+ input := huh.NewInput().Title(title).Value(&value).Validate(func(value string) error {
83
+ if strings.TrimSpace(value) == "" {
84
+ return fmt.Errorf("%s is required", title)
85
+ }
86
+ return nil
87
+ })
88
+ if err := huh.NewForm(huh.NewGroup(input)).Run(); err != nil {
89
+ return err
90
+ }
91
+ values[field.Key] = value
92
+ }
93
+ return nil
94
+ }
95
+
96
+ func registrationSelectOptions(field task.RegistrationField) []huh.Option[string] {
97
+ options := make([]huh.Option[string], 0, len(field.Options)+1)
98
+ if field.Default == "" {
99
+ // Huh selects the first option when Enter is pressed. Keep an explicit
100
+ // empty choice first so a missing conventional default cannot silently
101
+ // become the first real Jira status.
102
+ options = append(options, huh.NewOption("Select a value...", ""))
103
+ }
104
+ for _, option := range field.Options {
105
+ options = append(options, huh.NewOption(option, option))
106
+ }
107
+ return options
108
+ }
109
+
110
+ func registrationTaskConfig(reg task.Registration, supplied kvFlags, repoName string) (config.RawValues, error) {
111
+ known := make(map[string]task.RegistrationField, len(reg.Fields))
112
+ for _, field := range reg.Fields {
113
+ if strings.TrimSpace(field.Key) == "" {
114
+ return nil, errorsForRegistration("task plugin returned an empty registration key")
115
+ }
116
+ if _, exists := known[field.Key]; exists {
117
+ return nil, errorsForRegistration(fmt.Sprintf("duplicate task registration key %q", field.Key))
118
+ }
119
+ known[field.Key] = field
120
+ }
121
+ out := config.RawValues{}
122
+ for key, value := range supplied {
123
+ field, ok := known[key]
124
+ if !ok {
125
+ return nil, errorsForRegistration(fmt.Sprintf("unknown task key %q", key))
126
+ }
127
+ if field.Derived {
128
+ return nil, errorsForRegistration(fmt.Sprintf("task key %q is derived and cannot be overridden", key))
129
+ }
130
+ if strings.TrimSpace(value) == "" {
131
+ return nil, errorsForRegistration(fmt.Sprintf("task key %q requires a non-empty value", key))
132
+ }
133
+ if len(field.Options) > 0 && !containsRegistrationOption(field.Options, value) {
134
+ return nil, errorsForRegistration(fmt.Sprintf("task key %q value %q is not one of the available choices", key, value))
135
+ }
136
+ if err := setRegistrationValue(out, key, value); err != nil {
137
+ return nil, err
138
+ }
139
+ }
140
+ for _, field := range reg.Fields {
141
+ if field.Derived {
142
+ if err := setRegistrationValue(out, field.Key, repoName); err != nil {
143
+ return nil, err
144
+ }
145
+ continue
146
+ }
147
+ if registrationValue(out, field.Key) == "" {
148
+ return nil, errorsForRegistration(fmt.Sprintf("missing required task key %q", field.Key))
149
+ }
150
+ }
151
+ return out, nil
152
+ }
153
+
154
+ func errorsForRegistration(message string) error { return fmt.Errorf("%s", message) }
155
+
156
+ func containsRegistrationOption(options []string, value string) bool {
157
+ for _, option := range options {
158
+ if option == value {
159
+ return true
160
+ }
161
+ }
162
+ return false
163
+ }
164
+
165
+ func registrationValue(values config.RawValues, key string) string {
166
+ parts := strings.Split(key, ".")
167
+ var current any = map[string]any(values)
168
+ for _, part := range parts {
169
+ var next map[string]any
170
+ switch value := current.(type) {
171
+ case map[string]any:
172
+ next = value
173
+ case config.RawValues:
174
+ next = map[string]any(value)
175
+ default:
176
+ return ""
177
+ }
178
+ value, ok := next[part]
179
+ if !ok {
180
+ return ""
181
+ }
182
+ current = value
183
+ }
184
+ value, _ := current.(string)
185
+ return strings.TrimSpace(value)
186
+ }
187
+
188
+ func setRegistrationValue(values config.RawValues, key, value string) error {
189
+ parts := strings.Split(key, ".")
190
+ if len(parts) == 0 || parts[0] == "" {
191
+ return errorsForRegistration("task plugin returned an invalid registration key")
192
+ }
193
+ current := values
194
+ for _, part := range parts[:len(parts)-1] {
195
+ if part == "" {
196
+ return errorsForRegistration(fmt.Sprintf("task plugin returned an invalid registration key %q", key))
197
+ }
198
+ existing, ok := current[part]
199
+ if !ok {
200
+ nested := map[string]any{}
201
+ current[part] = nested
202
+ current = nested
203
+ continue
204
+ }
205
+ var nested map[string]any
206
+ switch typed := existing.(type) {
207
+ case map[string]any:
208
+ nested = typed
209
+ case config.RawValues:
210
+ nested = map[string]any(typed)
211
+ current[part] = nested
212
+ default:
213
+ return errorsForRegistration(fmt.Sprintf("task registration key %q conflicts with another value", key))
214
+ }
215
+ current = nested
216
+ }
217
+ current[parts[len(parts)-1]] = value
218
+ return nil
219
+ }
220
+
221
+ func registerSelectedReposDynamic(ctx context.Context, c *server.Client, candidates []runner.RepoCandidate, selected []int, reg task.Registration, values kvFlags) error {
222
+ for _, index := range selected {
223
+ candidate := candidates[index]
224
+ taskCfg, err := registrationTaskConfig(reg, values, candidate.Name)
225
+ if err != nil {
226
+ return fmt.Errorf("%s: %w", candidate.Name, err)
227
+ }
228
+ info, err := c.RegisterRepo(ctx, repo.RegisterInput{Name: candidate.Name, Path: candidate.Path, TaskConfig: taskCfg})
229
+ if err != nil {
230
+ return fmt.Errorf("%s: %w", candidate.Name, err)
231
+ }
232
+ fmt.Println(info.Name)
233
+ }
234
+ return nil
235
+ }
@@ -565,8 +565,8 @@ func (d *serveDeps) RegisterNodeSession(ctx context.Context, registration runsvc
565
565
  func (d *serveDeps) DiscoverRepos(ctx context.Context) ([]runner.RepoCandidate, error) {
566
566
  return d.repos.Discover(ctx)
567
567
  }
568
- func (d *serveDeps) TaskFields(context.Context) ([]string, error) {
569
- return d.repos.RequiredRepoKeys(), nil
568
+ func (d *serveDeps) TaskRegistrationFields(ctx context.Context, values config.RawValues) ([]task.RegistrationField, error) {
569
+ return d.repos.RegistrationFields(ctx, values)
570
570
  }
571
571
  func (d *serveDeps) RegisterRepo(ctx context.Context, input repo.RegisterInput) (repo.Info, error) {
572
572
  info, err := d.repos.Register(ctx, input)
@@ -101,6 +101,13 @@ repos:
101
101
  # relay-flow repo name during repo registration.
102
102
  project: PAY
103
103
  component: payments
104
+ # These must be valid statuses in the PAY project. Interactive
105
+ # registration discovers them; scripted registration passes the same
106
+ # three keys with --set.
107
+ statusDefaults:
108
+ start: To Do
109
+ work: In Progress
110
+ end: Done
104
111
 
105
112
  # Optional repo-level overrides inherit into workflows and nodes.
106
113
  # assignee: relay-bot@example.com
@@ -11,10 +11,11 @@ taskConfig:
11
11
  - To Do
12
12
  issueTypes:
13
13
  - Story
14
- # labels:
15
- # - coding
16
- # assignees:
17
- # - owner@example.com
14
+ labels:
15
+ - "workflow:true"
16
+ assignees:
17
+ - currentUser()
18
+ # Jira resolves currentUser() to the authenticated Jira email.
18
19
  # assignee: default-node-owner@example.com
19
20
  # project: PAY
20
21
  # component: api
@@ -28,48 +29,62 @@ nodes:
28
29
  transitionTo:
29
30
  parentStatus: In Progress
30
31
  onSuccess:
31
- - target: implement
32
+ - target: coding
32
33
  when: The parent story is ready for implementation
33
34
 
34
- implement:
35
+ coding:
35
36
  type: agent
36
37
  agent: build
37
- description: Implement the parent story and verify the changes.
38
- # nudgePrompt: "Finish {{node}} for {{ticket}} and choose one of: {{nextSteps}}"
38
+ description: Implement the parent story in the current ticket worktree.
39
+ nudgePrompt: |
40
+ Continue working on {{ticket}}. Read the parent {{taskSystem}} ticket
41
+ {{ticket}} and your assigned ticket {{mailbox}} to understand the requirements. Read
42
+ the latest feedback, address the requested changes, and work on the next
43
+ bounded task slice. Return the complete report. Valid choices are:
44
+ {{nextSteps}}.
39
45
  taskConfig:
40
46
  # assignee: developer@example.com
41
47
  transitionTo:
42
48
  taskStatus: In Progress
43
49
  # parentStatus: In Progress
44
50
  onSuccess:
45
- - target: review
51
+ - target: reviewing
46
52
  when: Implementation and verification are complete
47
53
  onFailure:
48
- - target: implement
54
+ - target: coding
49
55
  when: Implementation needs another pass
50
56
 
51
- review:
57
+ reviewing:
52
58
  type: agent
53
59
  agent: plan
54
- description: Review the implementation for correctness, regressions, and missing tests.
55
- # nudgePrompt: "Complete the review for {{ticket}}. Valid routes: {{nextSteps}}"
60
+ description: Review the completed implementation and report required changes.
61
+ nudgePrompt: |
62
+ Review {{ticket}}. Read the parent {{taskSystem}} ticket {{ticket}} and
63
+ your assigned ticket {{mailbox}} to understand the requirements. Re-check the
64
+ implementation and latest coding feedback, then return the complete
65
+ report. Valid choices are:
66
+ {{nextSteps}}.
56
67
  taskConfig:
57
68
  # assignee: reviewer@example.com
58
69
  transitionTo:
59
70
  taskStatus: In Progress
60
71
  # parentStatus: In Review
61
72
  onSuccess:
62
- - target: prReview
73
+ - target: humanReview
63
74
  when: The changes are ready for human approval
64
75
  onFailure:
65
- - target: implement
76
+ - target: coding
66
77
  when: Code changes are required
67
78
 
68
- prReview:
79
+ humanReview:
69
80
  type: hitl
70
81
  agent: plan
71
- description: Discuss the pull request with the human and obtain approval.
72
- # nudgePrompt: "Review {{ticket}} with the human. Valid routes: {{nextSteps}}"
82
+ description: Approve the reviewed implementation or request changes.
83
+ nudgePrompt: |
84
+ Review the completed work for {{ticket}} with the human. Read the parent
85
+ {{taskSystem}} ticket {{ticket}} and your assigned ticket {{mailbox}} to
86
+ understand the requirements. Return the complete report. Valid choices are:
87
+ {{nextSteps}}.
73
88
  taskConfig:
74
89
  # assignee: human-reviewer@example.com
75
90
  transitionTo:
@@ -79,7 +94,7 @@ nodes:
79
94
  - target: end
80
95
  when: The human approves the review
81
96
  onFailure:
82
- - target: implement
97
+ - target: coding
83
98
  when: The human requests code changes
84
99
 
85
100
  end:
@@ -14,7 +14,7 @@ import (
14
14
  "github.com/rajpopat27/relay-flow/internal/workflow"
15
15
  )
16
16
 
17
- const configuredPlugin = "relay-flow-plugin@0.2.6-alpha"
17
+ const configuredPlugin = "relay-flow-plugin@0.2.8-alpha"
18
18
 
19
19
  func TestBuildCommandArgv(t *testing.T) {
20
20
  t.Setenv("RELAY_FLOW_HOME", "/var/lib/relay-flow-test")
@@ -10,7 +10,7 @@ import (
10
10
  "github.com/rajpopat27/relay-flow/internal/config"
11
11
  )
12
12
 
13
- const relayFlowPlugin = "relay-flow-plugin@0.2.6-alpha"
13
+ const relayFlowPlugin = "relay-flow-plugin@0.2.8-alpha"
14
14
 
15
15
  type jsoncToken struct {
16
16
  kind byte
@@ -79,6 +79,12 @@ func (s *Service) RequiredRepoKeys() []string {
79
79
  return keys
80
80
  }
81
81
 
82
+ // RegistrationFields returns task-plugin-owned registration prompts and
83
+ // choices. Core repo management does not interpret the returned keys.
84
+ func (s *Service) RegistrationFields(ctx context.Context, values config.RawValues) ([]task.RegistrationField, error) {
85
+ return task.RegistrationFields(ctx, s.taskPlugin, values)
86
+ }
87
+
82
88
  type RegisterInput struct {
83
89
  Name string
84
90
  Path string
@@ -116,6 +122,13 @@ func (s *Service) Register(ctx context.Context, input RegisterInput) (Info, erro
116
122
  return Info{}, fmt.Errorf("repo %q: required task config key %q is missing", input.Name, k)
117
123
  }
118
124
  }
125
+ if err := task.ValidateRegistration(ctx, s.taskPlugin, task.RepoRegistrationSpec{
126
+ Name: input.Name,
127
+ RootConfig: cfg.TaskConfig,
128
+ RepoConfig: input.TaskConfig,
129
+ }); err != nil {
130
+ return Info{}, fmt.Errorf("repo %q: %w", input.Name, err)
131
+ }
119
132
  // Duplicate registration identity: compute this repo's identity and compare
120
133
  // against every registered repo. Most plugins use their physical task scope;
121
134
  // Beads includes its derived repository label so distinct repositories may
@@ -264,7 +264,10 @@ func (errInvalidRepo) Error() string { return "invalid repo" }
264
264
 
265
265
  func TestRegisterAtomicallyPersists(t *testing.T) {
266
266
  fx := newServiceFixture(t, &fakeRunnerDiscovery{})
267
- if _, err := fx.svc.Register(context.Background(), repo.RegisterInput{Name: "payments", Path: "/srv/payments", TaskConfig: config.RawValues{"project": "PAY", "component": "api"}}); err != nil {
267
+ statusDefaults := map[string]any{"start": "Open", "work": "Working", "end": "Closed"}
268
+ if _, err := fx.svc.Register(context.Background(), repo.RegisterInput{Name: "payments", Path: "/srv/payments", TaskConfig: config.RawValues{
269
+ "project": "PAY", "component": "api", "statusDefaults": statusDefaults,
270
+ }}); err != nil {
268
271
  t.Fatal(err)
269
272
  }
270
273
  cfg, err := config.LoadMachine(fx.cfgPath)
@@ -278,6 +281,16 @@ func TestRegisterAtomicallyPersists(t *testing.T) {
278
281
  if r.TaskConfig["project"] != "PAY" {
279
282
  t.Fatalf("repo taskConfig not persisted: %+v", r.TaskConfig)
280
283
  }
284
+ var gotDefaults map[string]any
285
+ switch values := r.TaskConfig["statusDefaults"].(type) {
286
+ case map[string]any:
287
+ gotDefaults = values
288
+ case config.RawValues:
289
+ gotDefaults = map[string]any(values)
290
+ }
291
+ if gotDefaults == nil || gotDefaults["start"] != "Open" || gotDefaults["work"] != "Working" || gotDefaults["end"] != "Closed" {
292
+ t.Fatalf("repo statusDefaults not persisted: %#v", r.TaskConfig["statusDefaults"])
293
+ }
281
294
  }
282
295
 
283
296
  func TestRegisterSetsUpHarnessRepo(t *testing.T) {
@@ -2,15 +2,19 @@ package server_test
2
2
 
3
3
  import (
4
4
  "bytes"
5
+ "context"
5
6
  "encoding/json"
6
7
  "fmt"
7
8
  "io"
8
9
  "net/http"
10
+ "path/filepath"
9
11
  "strings"
10
12
  "testing"
11
13
  "time"
12
14
 
15
+ "github.com/rajpopat27/relay-flow/internal/config"
13
16
  "github.com/rajpopat27/relay-flow/internal/run"
17
+ "github.com/rajpopat27/relay-flow/internal/server"
14
18
  "github.com/rajpopat27/relay-flow/internal/task"
15
19
  "github.com/rajpopat27/relay-flow/internal/workflow"
16
20
  )
@@ -287,6 +291,48 @@ func TestRunEndpointsExposeRetryDetails(t *testing.T) {
287
291
  }
288
292
  }
289
293
 
294
+ func TestRepoTaskFieldsSupportsInitialGetAndDependentPost(t *testing.T) {
295
+ fake := &fakeServices{}
296
+ c, cleanup := startHandler(t, fake)
297
+ defer cleanup()
298
+
299
+ code, env := do(t, c, http.MethodGet, "http://relay/repos/task-fields", nil)
300
+ if code != http.StatusOK || !env.OK || !bytes.Contains(env.Data, []byte(`"fields"`)) {
301
+ t.Fatalf("GET /repos/task-fields: code=%d env=%+v", code, env)
302
+ }
303
+ code, env = do(t, c, http.MethodPost, "http://relay/repos/task-fields", []byte(`{"values":{"project":"PAY"}}`))
304
+ if code != http.StatusOK || !env.OK || len(fake.registrationValues) != 2 {
305
+ t.Fatalf("POST /repos/task-fields: code=%d env=%+v values=%v", code, env, fake.registrationValues)
306
+ }
307
+ if fake.registrationValues[1]["project"] != "PAY" {
308
+ t.Fatalf("dependent registration values = %#v", fake.registrationValues[1])
309
+ }
310
+ }
311
+
312
+ func TestClientRepoRegistrationFieldsUsesGetThenPost(t *testing.T) {
313
+ fake := &fakeServices{}
314
+ dir := t.TempDir()
315
+ _, cleanup := startHandlerOnSocket(t, dir, fake)
316
+ defer cleanup()
317
+ client := server.NewClient(filepath.Join(dir, "server.sock"))
318
+ keys, err := client.RepoTaskFields(context.Background())
319
+ if err != nil || len(keys) != 2 || keys[0] != "project" || keys[1] != "component" {
320
+ t.Fatalf("documented task fields = %v, err=%v", keys, err)
321
+ }
322
+
323
+ initial, err := client.RepoRegistrationFields(context.Background(), nil)
324
+ if err != nil || len(initial.Fields) != 2 {
325
+ t.Fatalf("initial registration fields = %#v, err=%v", initial, err)
326
+ }
327
+ dependent, err := client.RepoRegistrationFields(context.Background(), config.RawValues{"project": "PAY"})
328
+ if err != nil || len(dependent.Fields) != 2 {
329
+ t.Fatalf("dependent registration fields = %#v, err=%v", dependent, err)
330
+ }
331
+ if len(fake.registrationValues) != 3 || fake.registrationValues[0] != nil || fake.registrationValues[1] != nil || fake.registrationValues[2]["project"] != "PAY" {
332
+ t.Fatalf("registration request values = %#v", fake.registrationValues)
333
+ }
334
+ }
335
+
290
336
  func TestRepoOperations(t *testing.T) {
291
337
  c, cleanup := startHandler(t, &fakeServices{})
292
338
  defer cleanup()
@@ -10,9 +10,11 @@ import (
10
10
  "net/http"
11
11
  "net/url"
12
12
 
13
+ "github.com/rajpopat27/relay-flow/internal/config"
13
14
  "github.com/rajpopat27/relay-flow/internal/repo"
14
15
  "github.com/rajpopat27/relay-flow/internal/run"
15
16
  "github.com/rajpopat27/relay-flow/internal/runner"
17
+ "github.com/rajpopat27/relay-flow/internal/task"
16
18
  "github.com/rajpopat27/relay-flow/internal/workflow"
17
19
  )
18
20
 
@@ -134,8 +136,7 @@ func (c *Client) DiscoverRepos(ctx context.Context) ([]runner.RepoCandidate, err
134
136
  return out, nil
135
137
  }
136
138
 
137
- // RepoTaskFields returns the task factory's required repo keys, matching
138
- // GET /repos/task-fields (server wraps the list as {"fields": [...]}).
139
+ // RepoTaskFields returns the documented initial required-key metadata.
139
140
  func (c *Client) RepoTaskFields(ctx context.Context) ([]string, error) {
140
141
  var out struct {
141
142
  Fields []string `json:"fields"`
@@ -146,6 +147,33 @@ func (c *Client) RepoTaskFields(ctx context.Context) ([]string, error) {
146
147
  return out.Fields, nil
147
148
  }
148
149
 
150
+ // RepoRegistrationFields returns task-plugin-owned repository prompts. Values
151
+ // are flat registration inputs so plugins can discover dependent choices (for
152
+ // example project statuses after project is selected). Initial discovery uses
153
+ // the documented GET endpoint; dependent discovery uses POST.
154
+ func (c *Client) RepoRegistrationFields(ctx context.Context, values config.RawValues) (task.Registration, error) {
155
+ if len(values) == 0 {
156
+ var out struct {
157
+ Registration task.Registration `json:"registration"`
158
+ }
159
+ if err := c.call(ctx, http.MethodGet, "/repos/task-fields", nil, &out); err != nil {
160
+ return task.Registration{}, err
161
+ }
162
+ return out.Registration, nil
163
+ }
164
+ payload, err := json.Marshal(struct {
165
+ Values config.RawValues `json:"values,omitempty"`
166
+ }{Values: values})
167
+ if err != nil {
168
+ return task.Registration{}, err
169
+ }
170
+ var out task.Registration
171
+ if err := c.call(ctx, http.MethodPost, "/repos/task-fields", payload, &out); err != nil {
172
+ return task.Registration{}, err
173
+ }
174
+ return out, nil
175
+ }
176
+
149
177
  // RegisterRepo registers a repo by name/path with optional task config.
150
178
  func (c *Client) RegisterRepo(ctx context.Context, input repo.RegisterInput) (repo.Info, error) {
151
179
  payload, _ := json.Marshal(input)
@@ -12,10 +12,12 @@ import (
12
12
  "path/filepath"
13
13
  "testing"
14
14
 
15
+ "github.com/rajpopat27/relay-flow/internal/config"
15
16
  "github.com/rajpopat27/relay-flow/internal/repo"
16
17
  "github.com/rajpopat27/relay-flow/internal/run"
17
18
  "github.com/rajpopat27/relay-flow/internal/runner"
18
19
  "github.com/rajpopat27/relay-flow/internal/server"
20
+ "github.com/rajpopat27/relay-flow/internal/task"
19
21
  "github.com/rajpopat27/relay-flow/internal/workflow"
20
22
  )
21
23
 
@@ -23,20 +25,21 @@ import (
23
25
  // seams. activeWorkflows seeds workflows with an active run (drives 409);
24
26
  // failRepos forces a 500; slowReport simulates a long-running report call.
25
27
  type fakeServices struct {
26
- activeWorkflows map[string]bool
27
- failRepos bool
28
- slowReport chan struct{}
29
- workflows map[string]*workflow.Workflow
30
- repos map[string]repo.Info
31
- runs []run.Run
32
- shutdownCh chan struct{}
33
- registrations []run.NodeRuntimeRegistration
34
- runtimeAck run.NodeRuntimeRegistrationAck
35
- processedReports map[string]bool
36
- submittedReports int
37
- restartRun run.Run
38
- restartErr error
39
- restarts []string
28
+ activeWorkflows map[string]bool
29
+ failRepos bool
30
+ slowReport chan struct{}
31
+ workflows map[string]*workflow.Workflow
32
+ repos map[string]repo.Info
33
+ runs []run.Run
34
+ shutdownCh chan struct{}
35
+ registrations []run.NodeRuntimeRegistration
36
+ runtimeAck run.NodeRuntimeRegistrationAck
37
+ processedReports map[string]bool
38
+ submittedReports int
39
+ restartRun run.Run
40
+ restartErr error
41
+ restarts []string
42
+ registrationValues []config.RawValues
40
43
  }
41
44
 
42
45
  func (f *fakeServices) SubmitWorkflow(_ context.Context, yaml []byte) (*workflow.Workflow, error) {
@@ -152,8 +155,9 @@ func (f *fakeServices) DiscoverRepos(context.Context) ([]runner.RepoCandidate, e
152
155
  return []runner.RepoCandidate{{Name: "payments", Path: "/srv/payments"}}, nil
153
156
  }
154
157
 
155
- func (f *fakeServices) TaskFields(context.Context) ([]string, error) {
156
- return []string{"status", "labels"}, nil
158
+ func (f *fakeServices) TaskRegistrationFields(_ context.Context, values config.RawValues) ([]task.RegistrationField, error) {
159
+ f.registrationValues = append(f.registrationValues, values)
160
+ return []task.RegistrationField{{Key: "project", Title: "Project"}, {Key: "component", Title: "Component", Derived: true}}, nil
157
161
  }
158
162
 
159
163
  func (f *fakeServices) RegisterRepo(_ context.Context, input repo.RegisterInput) (repo.Info, error) {