relay-flow 0.2.7-alpha → 0.2.9-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 -37
- package/cmd/relay-flow/commands_test.go +199 -19
- package/cmd/relay-flow/main.go +183 -144
- package/cmd/relay-flow/onboarding.go +344 -0
- package/cmd/relay-flow/onboarding_test.go +515 -0
- package/cmd/relay-flow/repo_registration.go +454 -0
- package/cmd/relay-flow/serve.go +5 -2
- package/examples/config-reference.yaml +7 -0
- package/go.mod +12 -8
- package/go.sum +24 -17
- package/internal/execution/goworkflows/engine_test.go +3 -3
- 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 +47 -3
- package/internal/repo/service_test.go +36 -1
- 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 +65 -0
- package/internal/server/client.go +40 -2
- package/internal/server/fixture_test.go +27 -16
- package/internal/server/server.go +80 -4
- package/internal/task/beads/beads.go +7 -2
- package/internal/task/beads/beads_test.go +13 -0
- package/internal/task/factory.go +9 -7
- package/internal/task/jira/auth_test.go +9 -1
- package/internal/task/jira/filters_test.go +2 -2
- package/internal/task/jira/helpers_test.go +13 -0
- package/internal/task/jira/jira.go +233 -44
- package/internal/task/jira/lifecycle_inheritance_test.go +5 -5
- package/internal/task/jira/rest/client.go +73 -31
- package/internal/task/jira/rest/client_test.go +25 -0
- package/internal/task/jira/status_defaults_test.go +134 -0
- package/internal/task/jira/templates_test.go +2 -2
- package/internal/task/jira/testdata/jira_search_issues.json +4 -4
- package/internal/task/jira/transition_defaults_test.go +8 -12
- package/internal/task/jira/validation_test.go +3 -1
- package/internal/task/registration.go +59 -0
- package/internal/workflow/workflow.go +4 -1
- package/internal/workflow/workflow_test.go +4 -4
- package/package.json +1 -1
package/cmd/relay-flow/main.go
CHANGED
|
@@ -136,6 +136,7 @@ Usage:
|
|
|
136
136
|
relay-flow init [--force] [--task-plugin <name> --runner-plugin <name> --harness-plugin <name>]
|
|
137
137
|
[--executor-plugin <goworkflows|temporal>]
|
|
138
138
|
[--temporal-address <host:port>] [--temporal-namespace <name>]
|
|
139
|
+
(interactive first run also authenticates and optionally registers repos)
|
|
139
140
|
relay-flow task auth [task-plugin options]
|
|
140
141
|
relay-flow serve [--recover] [--debug] [--background]
|
|
141
142
|
relay-flow stop
|
|
@@ -168,6 +169,14 @@ Usage:
|
|
|
168
169
|
// a TTY → three stdin lines (task, runner, harness), the documented test
|
|
169
170
|
// seam and script path.
|
|
170
171
|
func cmdInit(p paths.Paths, args []string, stdin io.Reader) int {
|
|
172
|
+
if err := recoverFirstRunPublication(p); err != nil {
|
|
173
|
+
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
174
|
+
return exitFail
|
|
175
|
+
}
|
|
176
|
+
if err := cleanupFirstRunStaging(p); err != nil {
|
|
177
|
+
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
178
|
+
return exitFail
|
|
179
|
+
}
|
|
171
180
|
fs := flag.NewFlagSet("init", flag.ContinueOnError)
|
|
172
181
|
taskName := fs.String("task-plugin", "", "task plugin name (non-interactive)")
|
|
173
182
|
runnerName := fs.String("runner-plugin", "", "runner plugin name (non-interactive)")
|
|
@@ -206,11 +215,17 @@ func cmdInit(p paths.Paths, args []string, stdin io.Reader) int {
|
|
|
206
215
|
fmt.Fprintln(os.Stderr, "init: database is missing; refusing to recreate existing durable state")
|
|
207
216
|
return exitFail
|
|
208
217
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
218
|
+
// A first interactive setup keeps the final home untouched until the
|
|
219
|
+
// selected task plugin has authenticated successfully. Scripted setup and
|
|
220
|
+
// --force retain the existing eager directory creation behavior.
|
|
221
|
+
firstRunInteractive := !*force && !configExists && !databaseExists && !flagged && interactiveInitTTY(stdin)
|
|
222
|
+
if !firstRunInteractive {
|
|
223
|
+
if err := os.MkdirAll(p.Root, 0o700); err != nil {
|
|
224
|
+
fmt.Fprintln(os.Stderr, err)
|
|
225
|
+
return exitFail
|
|
226
|
+
}
|
|
227
|
+
_ = os.Chmod(p.Root, 0o700)
|
|
212
228
|
}
|
|
213
|
-
_ = os.Chmod(p.Root, 0o700)
|
|
214
229
|
var unlock func()
|
|
215
230
|
if *force {
|
|
216
231
|
unlock, err = lockForForcedInit(p.Lock)
|
|
@@ -240,9 +255,9 @@ func cmdInit(p paths.Paths, args []string, stdin io.Reader) int {
|
|
|
240
255
|
switch {
|
|
241
256
|
case flagged:
|
|
242
257
|
names = []string{*taskName, *runnerName, *harnessName}
|
|
243
|
-
case
|
|
258
|
+
case interactiveInitTTY(stdin):
|
|
244
259
|
var err error
|
|
245
|
-
names, err =
|
|
260
|
+
names, err = interactiveInitPluginPick()
|
|
246
261
|
if err != nil {
|
|
247
262
|
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
248
263
|
return exitFail
|
|
@@ -258,7 +273,7 @@ func cmdInit(p paths.Paths, args []string, stdin io.Reader) int {
|
|
|
258
273
|
return exitFail
|
|
259
274
|
}
|
|
260
275
|
}
|
|
261
|
-
if executor == executorTemporal &&
|
|
276
|
+
if executor == executorTemporal && interactiveInitTTY(stdin) {
|
|
262
277
|
address, namespace, err := promptTemporalSettings(*temporalAddress, *temporalNamespace)
|
|
263
278
|
if err != nil {
|
|
264
279
|
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
@@ -353,6 +368,19 @@ func cmdInit(p paths.Paths, args []string, stdin io.Reader) int {
|
|
|
353
368
|
} else {
|
|
354
369
|
cfg.HarnessConfig = config.Merge(harnessDefaults, cfg.HarnessConfig)
|
|
355
370
|
}
|
|
371
|
+
var stagedAuthDir string
|
|
372
|
+
if firstRunInteractive {
|
|
373
|
+
if names[0] == "beads" {
|
|
374
|
+
fmt.Println("Beads authentication is managed by the Beads workspace and Dolt setup; relay-flow does not store task credentials.")
|
|
375
|
+
}
|
|
376
|
+
var authErr error
|
|
377
|
+
cfg, stagedAuthDir, authErr = stageFirstRunAuthentication(p, cfg, names[0], stdin)
|
|
378
|
+
if authErr != nil {
|
|
379
|
+
fmt.Fprintln(os.Stderr, "init: task auth: "+authErr.Error())
|
|
380
|
+
return exitFail
|
|
381
|
+
}
|
|
382
|
+
defer os.RemoveAll(stagedAuthDir)
|
|
383
|
+
}
|
|
356
384
|
identity := projection.ExecutorIdentity{
|
|
357
385
|
ExecutorPlugin: executor,
|
|
358
386
|
TemporalAddress: cfg.TemporalAddress,
|
|
@@ -373,13 +401,30 @@ func cmdInit(p paths.Paths, args []string, stdin io.Reader) int {
|
|
|
373
401
|
return exitFail
|
|
374
402
|
}
|
|
375
403
|
}
|
|
376
|
-
if !databaseExists {
|
|
404
|
+
if !firstRunInteractive && !databaseExists {
|
|
377
405
|
if err := projection.InitDatabaseWithIdentity(p.Database, identity); err != nil {
|
|
378
406
|
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
379
407
|
return exitFail
|
|
380
408
|
}
|
|
381
409
|
}
|
|
382
|
-
if
|
|
410
|
+
if firstRunInteractive {
|
|
411
|
+
if err := commitFirstRun(p, stagedAuthDir, identity); err != nil {
|
|
412
|
+
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
413
|
+
return exitFail
|
|
414
|
+
}
|
|
415
|
+
register, err := interactiveInitRepoPrompt()
|
|
416
|
+
if err != nil {
|
|
417
|
+
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
418
|
+
return exitFail
|
|
419
|
+
}
|
|
420
|
+
if register {
|
|
421
|
+
if err := interactiveInitRepoSetup(p, stdin); err != nil {
|
|
422
|
+
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
423
|
+
return exitFail
|
|
424
|
+
}
|
|
425
|
+
fmt.Println("Repository setup complete. The server is running; stop it with `relay-flow stop`.")
|
|
426
|
+
}
|
|
427
|
+
} else if err := config.SaveMachine(p.Config, cfg); err != nil {
|
|
383
428
|
fmt.Fprintln(os.Stderr, err)
|
|
384
429
|
return exitFail
|
|
385
430
|
}
|
|
@@ -540,53 +585,119 @@ func cmdServe(p paths.Paths, args []string) int {
|
|
|
540
585
|
return exitOK
|
|
541
586
|
}
|
|
542
587
|
|
|
588
|
+
var (
|
|
589
|
+
backgroundServeStartupTimeout = 10 * time.Second
|
|
590
|
+
backgroundServePollInterval = 50 * time.Millisecond
|
|
591
|
+
)
|
|
592
|
+
|
|
543
593
|
func startBackgroundServe(p paths.Paths, recover, debug bool) error {
|
|
544
594
|
client := server.NewClient(p.Socket)
|
|
545
595
|
if serverResponding(client, 200*time.Millisecond) {
|
|
546
596
|
return fmt.Errorf("serve --background: server is already running")
|
|
547
597
|
}
|
|
548
|
-
|
|
598
|
+
deadline := time.Now().Add(backgroundServeStartupTimeout)
|
|
599
|
+
lockHeld, err := serverLockHeld(p.Lock)
|
|
549
600
|
if err != nil {
|
|
550
|
-
return fmt.Errorf("serve --background:
|
|
551
|
-
}
|
|
552
|
-
childArgs := []string{"serve"}
|
|
553
|
-
if recover {
|
|
554
|
-
childArgs = append(childArgs, "--recover")
|
|
601
|
+
return fmt.Errorf("serve --background: inspect server lock: %w", err)
|
|
555
602
|
}
|
|
556
|
-
|
|
557
|
-
|
|
603
|
+
|
|
604
|
+
var (
|
|
605
|
+
cmd *exec.Cmd
|
|
606
|
+
wait chan error
|
|
607
|
+
observedOwner = lockHeld
|
|
608
|
+
devNull *os.File
|
|
609
|
+
)
|
|
610
|
+
if !lockHeld {
|
|
611
|
+
executable, resolveErr := os.Executable()
|
|
612
|
+
if resolveErr != nil {
|
|
613
|
+
return fmt.Errorf("serve --background: resolve executable: %w", resolveErr)
|
|
614
|
+
}
|
|
615
|
+
childArgs := []string{"serve"}
|
|
616
|
+
if recover {
|
|
617
|
+
childArgs = append(childArgs, "--recover")
|
|
618
|
+
}
|
|
619
|
+
if debug {
|
|
620
|
+
childArgs = append(childArgs, "--debug")
|
|
621
|
+
}
|
|
622
|
+
devNull, err = os.OpenFile(os.DevNull, os.O_RDWR, 0)
|
|
623
|
+
if err != nil {
|
|
624
|
+
return fmt.Errorf("serve --background: open %s: %w", os.DevNull, err)
|
|
625
|
+
}
|
|
626
|
+
defer devNull.Close()
|
|
627
|
+
cmd = exec.Command(executable, childArgs...)
|
|
628
|
+
cmd.Stdin, cmd.Stdout, cmd.Stderr = devNull, devNull, devNull
|
|
629
|
+
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
|
630
|
+
if err := cmd.Start(); err != nil {
|
|
631
|
+
return fmt.Errorf("serve --background: start: %w; see %s", err, p.ServerLog)
|
|
632
|
+
}
|
|
633
|
+
wait = make(chan error, 1)
|
|
634
|
+
go func() { wait <- cmd.Wait() }()
|
|
558
635
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
return fmt.Errorf("serve --background: open %s: %w", os.DevNull, err)
|
|
562
|
-
}
|
|
563
|
-
defer devNull.Close()
|
|
564
|
-
cmd := exec.Command(executable, childArgs...)
|
|
565
|
-
cmd.Stdin, cmd.Stdout, cmd.Stderr = devNull, devNull, devNull
|
|
566
|
-
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
|
567
|
-
if err := cmd.Start(); err != nil {
|
|
568
|
-
return fmt.Errorf("serve --background: start: %w; see %s", err, p.ServerLog)
|
|
569
|
-
}
|
|
570
|
-
wait := make(chan error, 1)
|
|
571
|
-
go func() { wait <- cmd.Wait() }()
|
|
572
|
-
deadline := time.NewTimer(10 * time.Second)
|
|
573
|
-
defer deadline.Stop()
|
|
574
|
-
ticker := time.NewTicker(50 * time.Millisecond)
|
|
636
|
+
|
|
637
|
+
ticker := time.NewTicker(backgroundServePollInterval)
|
|
575
638
|
defer ticker.Stop()
|
|
576
639
|
for {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
640
|
+
if serverResponding(client, 200*time.Millisecond) {
|
|
641
|
+
return nil
|
|
642
|
+
}
|
|
643
|
+
if !time.Now().Before(deadline) {
|
|
644
|
+
if cmd != nil && wait != nil {
|
|
645
|
+
_ = cmd.Process.Kill()
|
|
646
|
+
<-wait
|
|
647
|
+
}
|
|
583
648
|
return fmt.Errorf("serve --background: startup timed out; see %s", p.ServerLog)
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
649
|
+
}
|
|
650
|
+
if observedOwner && wait == nil {
|
|
651
|
+
held, lockErr := serverLockHeld(p.Lock)
|
|
652
|
+
if lockErr != nil {
|
|
653
|
+
return fmt.Errorf("serve --background: inspect server lock: %w", lockErr)
|
|
654
|
+
}
|
|
655
|
+
if !held {
|
|
656
|
+
return fmt.Errorf("serve --background: server exited before readiness; see %s", p.ServerLog)
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
select {
|
|
660
|
+
case childErr := <-wait:
|
|
661
|
+
held, lockErr := serverLockHeld(p.Lock)
|
|
662
|
+
if lockErr != nil {
|
|
663
|
+
return fmt.Errorf("serve --background: inspect server lock: %w", lockErr)
|
|
587
664
|
}
|
|
665
|
+
if held {
|
|
666
|
+
// Another process acquired the lock while this child was
|
|
667
|
+
// starting. Wait for that owner instead of spawning again.
|
|
668
|
+
observedOwner = true
|
|
669
|
+
wait = nil
|
|
670
|
+
cmd = nil
|
|
671
|
+
continue
|
|
672
|
+
}
|
|
673
|
+
return fmt.Errorf("serve --background: server exited before readiness: %v; see %s", childErr, p.ServerLog)
|
|
674
|
+
case <-ticker.C:
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
func serverLockHeld(path string) (bool, error) {
|
|
680
|
+
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
|
|
681
|
+
if err != nil {
|
|
682
|
+
if os.IsNotExist(err) {
|
|
683
|
+
return false, nil
|
|
588
684
|
}
|
|
685
|
+
return false, err
|
|
686
|
+
}
|
|
687
|
+
defer f.Close()
|
|
688
|
+
if err := f.Chmod(0o600); err != nil {
|
|
689
|
+
return false, err
|
|
690
|
+
}
|
|
691
|
+
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
|
|
692
|
+
if err == syscall.EWOULDBLOCK || err == syscall.EAGAIN {
|
|
693
|
+
return true, nil
|
|
694
|
+
}
|
|
695
|
+
return false, err
|
|
696
|
+
}
|
|
697
|
+
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
|
|
698
|
+
return false, err
|
|
589
699
|
}
|
|
700
|
+
return false, nil
|
|
590
701
|
}
|
|
591
702
|
|
|
592
703
|
func serverResponding(client *server.Client, timeout time.Duration) bool {
|
|
@@ -798,7 +909,7 @@ func cmdRepo(c *server.Client, args []string, stdin io.Reader) int {
|
|
|
798
909
|
}
|
|
799
910
|
|
|
800
911
|
// cmdRepoRegister registers one flagged repo or multiple interactively selected
|
|
801
|
-
// runner repos
|
|
912
|
+
// runner repos using registration metadata supplied by the selected task plugin.
|
|
802
913
|
func cmdRepoRegister(c *server.Client, flagName, flagPath string, sets kvFlags, stdin io.Reader) int {
|
|
803
914
|
ctx := context.Background()
|
|
804
915
|
flagged := flagName != "" || flagPath != "" || len(sets) > 0
|
|
@@ -809,32 +920,28 @@ func cmdRepoRegister(c *server.Client, flagName, flagPath string, sets kvFlags,
|
|
|
809
920
|
fmt.Fprintln(os.Stderr, "repo register: --name and --path are required for non-interactive registration")
|
|
810
921
|
return exitUsage
|
|
811
922
|
}
|
|
812
|
-
|
|
813
|
-
fmt.Fprintln(os.Stderr, "repo register: component is derived from --name and cannot be overridden")
|
|
814
|
-
return exitUsage
|
|
815
|
-
}
|
|
816
|
-
fields, err := c.RepoTaskFields(ctx)
|
|
923
|
+
registration, err := loadRepoRegistration(ctx, c, sets)
|
|
817
924
|
if err != nil {
|
|
818
|
-
fmt.Fprintln(os.Stderr, err)
|
|
925
|
+
fmt.Fprintln(os.Stderr, standaloneRepoServerHint(err))
|
|
819
926
|
return exitFail
|
|
820
927
|
}
|
|
821
|
-
taskCfg, err :=
|
|
928
|
+
taskCfg, err := registrationTaskConfig(registration, sets, flagName)
|
|
822
929
|
if err != nil {
|
|
823
930
|
fmt.Fprintln(os.Stderr, "repo register: "+err.Error())
|
|
824
931
|
return exitUsage
|
|
825
932
|
}
|
|
826
933
|
info, err := c.RegisterRepo(ctx, repo.RegisterInput{Name: flagName, Path: flagPath, TaskConfig: taskCfg})
|
|
827
934
|
if err != nil {
|
|
828
|
-
fmt.Fprintln(os.Stderr, err)
|
|
935
|
+
fmt.Fprintln(os.Stderr, standaloneRepoServerHint(err))
|
|
829
936
|
return exitFail
|
|
830
937
|
}
|
|
831
938
|
fmt.Println(info.Name)
|
|
832
939
|
return exitOK
|
|
833
940
|
}
|
|
834
941
|
|
|
835
|
-
|
|
942
|
+
registration, err := c.RepoRegistrationFields(ctx, nil)
|
|
836
943
|
if err != nil {
|
|
837
|
-
fmt.Fprintln(os.Stderr, err)
|
|
944
|
+
fmt.Fprintln(os.Stderr, standaloneRepoServerHint(err))
|
|
838
945
|
return exitFail
|
|
839
946
|
}
|
|
840
947
|
|
|
@@ -844,48 +951,39 @@ func cmdRepoRegister(c *server.Client, flagName, flagPath string, sets kvFlags,
|
|
|
844
951
|
}
|
|
845
952
|
candidates, err := c.DiscoverRepos(ctx)
|
|
846
953
|
if err != nil {
|
|
847
|
-
fmt.Fprintln(os.Stderr, err)
|
|
954
|
+
fmt.Fprintln(os.Stderr, standaloneRepoServerHint(err))
|
|
848
955
|
return exitFail
|
|
849
956
|
}
|
|
850
|
-
|
|
851
|
-
|
|
957
|
+
registered, err := c.ListRepos(ctx)
|
|
958
|
+
if err != nil {
|
|
959
|
+
fmt.Fprintln(os.Stderr, standaloneRepoServerHint(err))
|
|
960
|
+
return exitFail
|
|
961
|
+
}
|
|
962
|
+
candidates, selected, err := selectReposInteractive(ctx, c, candidates, registered)
|
|
963
|
+
if err != nil {
|
|
964
|
+
fmt.Fprintln(os.Stderr, "repo register: "+standaloneRepoServerHint(err).Error())
|
|
852
965
|
return exitFail
|
|
853
966
|
}
|
|
854
967
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
for i, cand := range candidates {
|
|
858
|
-
options[i] = huh.NewOption(cand.Name+" ("+cand.Path+")", i)
|
|
859
|
-
}
|
|
860
|
-
pickField := repoMultiSelect(options, &selected)
|
|
861
|
-
pick := huh.NewForm(huh.NewGroup(pickField))
|
|
862
|
-
if err := pick.Run(); err != nil {
|
|
968
|
+
shared := kvFlags{}
|
|
969
|
+
if err := promptRepoRegistration(registration, shared); err != nil {
|
|
863
970
|
fmt.Fprintln(os.Stderr, "repo register: "+err.Error())
|
|
864
971
|
return exitFail
|
|
865
972
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
}
|
|
875
|
-
inputs[i] = huh.NewInput().Title(title).Value(&values[i])
|
|
876
|
-
}
|
|
877
|
-
if len(inputs) > 0 {
|
|
878
|
-
if err := huh.NewForm(huh.NewGroup(inputs...)).Run(); err != nil {
|
|
973
|
+
if len(shared) > 0 {
|
|
974
|
+
dependent, err := c.RepoRegistrationFields(ctx, flatRegistrationValues(shared))
|
|
975
|
+
if err != nil {
|
|
976
|
+
fmt.Fprintln(os.Stderr, standaloneRepoServerHint(err))
|
|
977
|
+
return exitFail
|
|
978
|
+
}
|
|
979
|
+
registration = mergeRegistration(registration, dependent)
|
|
980
|
+
if err := promptRepoRegistration(registration, shared); err != nil {
|
|
879
981
|
fmt.Fprintln(os.Stderr, "repo register: "+err.Error())
|
|
880
982
|
return exitFail
|
|
881
983
|
}
|
|
882
984
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
shared[field] = values[i]
|
|
886
|
-
}
|
|
887
|
-
if err := registerSelectedRepos(ctx, c, candidates, selected, fields, shared); err != nil {
|
|
888
|
-
fmt.Fprintln(os.Stderr, "repo register: "+err.Error())
|
|
985
|
+
if err := registerSelectedReposDynamic(ctx, c, candidates, selected, registration, shared); err != nil {
|
|
986
|
+
fmt.Fprintln(os.Stderr, "repo register: "+standaloneRepoServerHint(err).Error())
|
|
889
987
|
return exitFail
|
|
890
988
|
}
|
|
891
989
|
return exitOK
|
|
@@ -894,6 +992,8 @@ func cmdRepoRegister(c *server.Client, flagName, flagPath string, sets kvFlags,
|
|
|
894
992
|
func repoMultiSelect(options []huh.Option[int], selected *[]int) *huh.MultiSelect[int] {
|
|
895
993
|
return huh.NewMultiSelect[int]().
|
|
896
994
|
Title("Select repositories").
|
|
995
|
+
Description("Press / to filter repositories; select [+] Add repository to add a runner resource.").
|
|
996
|
+
Filterable(true).
|
|
897
997
|
Options(options...).
|
|
898
998
|
Value(selected).
|
|
899
999
|
Validate(func(values []int) error {
|
|
@@ -904,67 +1004,6 @@ func repoMultiSelect(options []huh.Option[int], selected *[]int) *huh.MultiSelec
|
|
|
904
1004
|
})
|
|
905
1005
|
}
|
|
906
1006
|
|
|
907
|
-
func registrationSharedFields(fields []string) []string {
|
|
908
|
-
shared := make([]string, 0, len(fields))
|
|
909
|
-
for _, field := range fields {
|
|
910
|
-
if field != "component" {
|
|
911
|
-
shared = append(shared, field)
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
return shared
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
func repoTaskConfig(fields []string, supplied kvFlags, repoName string) (config.RawValues, error) {
|
|
918
|
-
required := map[string]bool{}
|
|
919
|
-
for _, field := range fields {
|
|
920
|
-
required[field] = true
|
|
921
|
-
}
|
|
922
|
-
values := config.RawValues{}
|
|
923
|
-
for key, value := range supplied {
|
|
924
|
-
if !required[key] {
|
|
925
|
-
return nil, fmt.Errorf("unknown task key %q (required keys: %s)", key, strings.Join(fields, ", "))
|
|
926
|
-
}
|
|
927
|
-
if value == "" {
|
|
928
|
-
return nil, fmt.Errorf("task key %q requires a non-empty value", key)
|
|
929
|
-
}
|
|
930
|
-
values[key] = value
|
|
931
|
-
}
|
|
932
|
-
if required["component"] {
|
|
933
|
-
values["component"] = repoName
|
|
934
|
-
}
|
|
935
|
-
var missing []string
|
|
936
|
-
for _, field := range fields {
|
|
937
|
-
if _, ok := values[field]; !ok {
|
|
938
|
-
missing = append(missing, field)
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
if len(missing) > 0 {
|
|
942
|
-
return nil, fmt.Errorf("missing required task keys: %s (pass --set %s=<value>)",
|
|
943
|
-
strings.Join(missing, ", "), missing[0])
|
|
944
|
-
}
|
|
945
|
-
return values, nil
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
func registerSelectedRepos(ctx context.Context, c *server.Client, candidates []runner.RepoCandidate, selected []int, fields []string, shared kvFlags) error {
|
|
949
|
-
for _, index := range selected {
|
|
950
|
-
candidate := candidates[index]
|
|
951
|
-
taskCfg, err := repoTaskConfig(fields, shared, candidate.Name)
|
|
952
|
-
if err != nil {
|
|
953
|
-
return fmt.Errorf("%s: %w", candidate.Name, err)
|
|
954
|
-
}
|
|
955
|
-
info, err := c.RegisterRepo(ctx, repo.RegisterInput{
|
|
956
|
-
Name: candidate.Name,
|
|
957
|
-
Path: candidate.Path,
|
|
958
|
-
TaskConfig: taskCfg,
|
|
959
|
-
})
|
|
960
|
-
if err != nil {
|
|
961
|
-
return fmt.Errorf("%s: %w", candidate.Name, err)
|
|
962
|
-
}
|
|
963
|
-
fmt.Println(info.Name)
|
|
964
|
-
}
|
|
965
|
-
return nil
|
|
966
|
-
}
|
|
967
|
-
|
|
968
1007
|
// kvFlags collects repeated key=value flags (registration task config).
|
|
969
1008
|
// Parse rejects malformed pairs, empty keys/values, and duplicates.
|
|
970
1009
|
type kvFlags map[string]string
|