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
package/cmd/relay-flow/main.go
CHANGED
|
@@ -17,6 +17,7 @@ import (
|
|
|
17
17
|
"os/exec"
|
|
18
18
|
"os/signal"
|
|
19
19
|
"path/filepath"
|
|
20
|
+
"runtime"
|
|
20
21
|
"strings"
|
|
21
22
|
"syscall"
|
|
22
23
|
"time"
|
|
@@ -52,6 +53,14 @@ const (
|
|
|
52
53
|
exitUsage = 2
|
|
53
54
|
)
|
|
54
55
|
|
|
56
|
+
// Build metadata has safe local-build defaults and can be replaced by release
|
|
57
|
+
// builds with -ldflags -X main.version=... and friends.
|
|
58
|
+
var (
|
|
59
|
+
version = "dev"
|
|
60
|
+
commit = "unknown"
|
|
61
|
+
buildDate = "unknown"
|
|
62
|
+
)
|
|
63
|
+
|
|
55
64
|
func main() { os.Exit(run(os.Args[1:], os.Stdin)) }
|
|
56
65
|
|
|
57
66
|
// home returns the relay-flow root, honoring RELAY_FLOW_HOME (tests and
|
|
@@ -81,6 +90,13 @@ func run(args []string, stdin io.Reader) int {
|
|
|
81
90
|
usage(os.Stderr)
|
|
82
91
|
return exitUsage
|
|
83
92
|
}
|
|
93
|
+
if isVersionArgs(args) {
|
|
94
|
+
printVersion(os.Stdout, len(args) == 1 && args[0] == "version")
|
|
95
|
+
return exitOK
|
|
96
|
+
}
|
|
97
|
+
if hasHelpArg(args) {
|
|
98
|
+
return printScopedHelp(args, os.Stdout)
|
|
99
|
+
}
|
|
84
100
|
p, err := home()
|
|
85
101
|
if err != nil {
|
|
86
102
|
fmt.Fprintln(os.Stderr, err)
|
|
@@ -133,28 +149,185 @@ func usage(w io.Writer) {
|
|
|
133
149
|
fmt.Fprintln(w, `relay-flow — durable ticket runner
|
|
134
150
|
|
|
135
151
|
Usage:
|
|
152
|
+
relay-flow [command] [flags]
|
|
153
|
+
|
|
154
|
+
Setup and server:
|
|
136
155
|
relay-flow init [--force] [--task-plugin <name> --runner-plugin <name> --harness-plugin <name>]
|
|
137
156
|
[--executor-plugin <goworkflows|temporal>]
|
|
138
157
|
[--temporal-address <host:port>] [--temporal-namespace <name>]
|
|
158
|
+
(interactive first run also authenticates and optionally registers repos)
|
|
139
159
|
relay-flow task auth [task-plugin options]
|
|
140
160
|
relay-flow serve [--recover] [--debug] [--background]
|
|
141
161
|
relay-flow stop
|
|
142
162
|
relay-flow report
|
|
163
|
+
relay-flow version | --version | -v
|
|
143
164
|
|
|
165
|
+
Workflow:
|
|
144
166
|
relay-flow workflow submit --file <path>
|
|
145
167
|
relay-flow workflow remove --name <name>
|
|
146
|
-
relay-flow workflow list
|
|
147
|
-
relay-flow workflow get --name <name>
|
|
168
|
+
relay-flow workflow list [--json]
|
|
169
|
+
relay-flow workflow get --name <name> [--json]
|
|
148
170
|
|
|
171
|
+
Repository:
|
|
149
172
|
relay-flow repo register [--name <name> --path <path> --set key=value ...]
|
|
150
173
|
relay-flow repo remove --name <name>
|
|
151
174
|
relay-flow repo list
|
|
152
175
|
relay-flow repo get --name <name>
|
|
153
176
|
|
|
154
|
-
|
|
155
|
-
relay-flow run
|
|
177
|
+
Run:
|
|
178
|
+
relay-flow run list [--repo <name>] [--workflow <name>] [--ticket <key>] [--active] [--json]
|
|
179
|
+
relay-flow run get --ticket <key> [--json]
|
|
156
180
|
relay-flow run restart --ticket <key>
|
|
157
|
-
relay-flow run cancel --ticket <key
|
|
181
|
+
relay-flow run cancel --ticket <key>
|
|
182
|
+
|
|
183
|
+
Examples:
|
|
184
|
+
relay-flow workflow list --json
|
|
185
|
+
relay-flow run get --ticket PAY-101
|
|
186
|
+
Example: relay-flow run get --ticket PAY-101`)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func isVersionArgs(args []string) bool {
|
|
190
|
+
return len(args) == 1 && (args[0] == "version" || args[0] == "--version" || args[0] == "-v")
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
func hasHelpArg(args []string) bool {
|
|
194
|
+
for _, arg := range args {
|
|
195
|
+
if arg == "--help" || arg == "-h" {
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return false
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
func printVersion(w io.Writer, long bool) {
|
|
203
|
+
if !long {
|
|
204
|
+
fmt.Fprintf(w, "relay-flow %s\n", version)
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
fmt.Fprintf(w, "relay-flow %s\n", version)
|
|
208
|
+
fmt.Fprintf(w, "Commit: %s\n", commit)
|
|
209
|
+
fmt.Fprintf(w, "Build date: %s\n", buildDate)
|
|
210
|
+
fmt.Fprintf(w, "Go: %s\n", runtime.Version())
|
|
211
|
+
fmt.Fprintf(w, "Platform: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
func printScopedHelp(args []string, w io.Writer) int {
|
|
215
|
+
// Help is handled before home/config/client creation. Keep each block
|
|
216
|
+
// scoped to the requested command instead of dumping the entire surface.
|
|
217
|
+
path := make([]string, 0, len(args))
|
|
218
|
+
for _, arg := range args {
|
|
219
|
+
if arg == "--help" || arg == "-h" {
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
// Command names are positional; once a flag appears, the remaining
|
|
223
|
+
// tokens are command arguments and must not change the help scope.
|
|
224
|
+
if strings.HasPrefix(arg, "-") {
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
path = append(path, arg)
|
|
228
|
+
}
|
|
229
|
+
if len(path) == 0 {
|
|
230
|
+
usage(w)
|
|
231
|
+
return exitOK
|
|
232
|
+
}
|
|
233
|
+
name := strings.Join(path, " ")
|
|
234
|
+
switch name {
|
|
235
|
+
case "init":
|
|
236
|
+
fmt.Fprintln(w, "Usage: relay-flow init [flags]")
|
|
237
|
+
fmt.Fprintln(w, "\nInitialize relay-flow and its durable state.")
|
|
238
|
+
fmt.Fprintln(w, "Flags: --force, --task-plugin, --runner-plugin, --harness-plugin, --executor-plugin")
|
|
239
|
+
fmt.Fprintln(w, "Example: relay-flow init --task-plugin jira --runner-plugin orca --harness-plugin opencode")
|
|
240
|
+
case "serve":
|
|
241
|
+
fmt.Fprintln(w, "Usage: relay-flow serve [--recover] [--debug] [--background]")
|
|
242
|
+
fmt.Fprintln(w, "\nStart the relay-flow server.")
|
|
243
|
+
fmt.Fprintln(w, "Example: relay-flow serve --background")
|
|
244
|
+
case "stop":
|
|
245
|
+
fmt.Fprintln(w, "Usage: relay-flow stop")
|
|
246
|
+
fmt.Fprintln(w, "\nStop the running relay-flow server.")
|
|
247
|
+
fmt.Fprintln(w, "Example: relay-flow stop")
|
|
248
|
+
case "report":
|
|
249
|
+
fmt.Fprintln(w, "Usage: relay-flow report < report.json")
|
|
250
|
+
fmt.Fprintln(w, "\nSubmit one complete structured node report.")
|
|
251
|
+
fmt.Fprintln(w, "Example: relay-flow report < report.json")
|
|
252
|
+
case "task":
|
|
253
|
+
fmt.Fprintln(w, "Usage: relay-flow task <command>")
|
|
254
|
+
fmt.Fprintln(w, "\nCommands: auth")
|
|
255
|
+
fmt.Fprintln(w, "Example: relay-flow task auth")
|
|
256
|
+
case "task auth":
|
|
257
|
+
fmt.Fprintln(w, "Usage: relay-flow task auth [task-plugin options]")
|
|
258
|
+
fmt.Fprintln(w, "\nDelegate authentication to the configured task-system plugin.")
|
|
259
|
+
fmt.Fprintln(w, "Example: relay-flow task auth")
|
|
260
|
+
case "runtime-register":
|
|
261
|
+
fmt.Fprintln(w, "Usage: relay-flow runtime-register < session.json")
|
|
262
|
+
fmt.Fprintln(w, "\nRegister a harness runtime session with the server.")
|
|
263
|
+
fmt.Fprintln(w, "Example: relay-flow runtime-register < session.json")
|
|
264
|
+
case "version":
|
|
265
|
+
fmt.Fprintln(w, "Usage: relay-flow version")
|
|
266
|
+
fmt.Fprintln(w, "\nPrint release and build metadata without contacting the server.")
|
|
267
|
+
fmt.Fprintln(w, "Example: relay-flow version")
|
|
268
|
+
case "workflow":
|
|
269
|
+
fmt.Fprintln(w, "Usage: relay-flow workflow <command>")
|
|
270
|
+
fmt.Fprintln(w, "\nCommands: submit, remove, list, get")
|
|
271
|
+
fmt.Fprintln(w, "Use relay-flow workflow <command> --help for command-specific flags.")
|
|
272
|
+
fmt.Fprintln(w, "Example: relay-flow workflow list --json")
|
|
273
|
+
case "workflow submit":
|
|
274
|
+
fmt.Fprintln(w, "Usage: relay-flow workflow submit --file <path>")
|
|
275
|
+
fmt.Fprintln(w, "\nCreate or replace a validated workflow definition.")
|
|
276
|
+
fmt.Fprintln(w, "Example: relay-flow workflow submit --file workflow.yaml")
|
|
277
|
+
case "workflow remove":
|
|
278
|
+
fmt.Fprintln(w, "Usage: relay-flow workflow remove --name <name>")
|
|
279
|
+
fmt.Fprintln(w, "\nRemove a workflow with no active runs.")
|
|
280
|
+
fmt.Fprintln(w, "Example: relay-flow workflow remove --name basicFlow")
|
|
281
|
+
case "workflow list":
|
|
282
|
+
fmt.Fprintln(w, "Usage: relay-flow workflow list [--json] [--no-color]")
|
|
283
|
+
fmt.Fprintln(w, "\nList configured workflows and their latest execution summaries.")
|
|
284
|
+
fmt.Fprintln(w, "Example: relay-flow workflow list --json")
|
|
285
|
+
case "workflow get":
|
|
286
|
+
fmt.Fprintln(w, "Usage: relay-flow workflow get --name <name> [--json] [--no-color]")
|
|
287
|
+
fmt.Fprintln(w, "\nShow validated static graph and recent runs. Use --json for automation.")
|
|
288
|
+
fmt.Fprintln(w, "Example: relay-flow workflow get --name basicFlow")
|
|
289
|
+
case "repo":
|
|
290
|
+
fmt.Fprintln(w, "Usage: relay-flow repo <command>")
|
|
291
|
+
fmt.Fprintln(w, "\nCommands: register, remove, list, get")
|
|
292
|
+
fmt.Fprintln(w, "Use relay-flow repo <command> --help for command-specific flags.")
|
|
293
|
+
fmt.Fprintln(w, "Example: relay-flow repo list")
|
|
294
|
+
case "repo register":
|
|
295
|
+
fmt.Fprintln(w, "Usage: relay-flow repo register [--name <name> --path <path> --set key=value]")
|
|
296
|
+
fmt.Fprintln(w, "\nRegister a runner repository and task-system configuration.")
|
|
297
|
+
fmt.Fprintln(w, "Example: relay-flow repo register --name payments --path /work/payments")
|
|
298
|
+
case "repo remove", "repo get":
|
|
299
|
+
fmt.Fprintf(w, "Usage: relay-flow %s --name <name>\n", name)
|
|
300
|
+
fmt.Fprintf(w, "Example: relay-flow %s --name payments\n", name)
|
|
301
|
+
case "repo list":
|
|
302
|
+
fmt.Fprintln(w, "Usage: relay-flow repo list")
|
|
303
|
+
fmt.Fprintln(w, "\nList registered repositories.")
|
|
304
|
+
fmt.Fprintln(w, "Example: relay-flow repo list")
|
|
305
|
+
case "run":
|
|
306
|
+
fmt.Fprintln(w, "Usage: relay-flow run <command>")
|
|
307
|
+
fmt.Fprintln(w, "\nCommands: list, get, restart, cancel")
|
|
308
|
+
fmt.Fprintln(w, "Use relay-flow run <command> --help for command-specific flags.")
|
|
309
|
+
fmt.Fprintln(w, "Example: relay-flow run get --ticket PAY-101")
|
|
310
|
+
case "run list":
|
|
311
|
+
fmt.Fprintln(w, "Usage: relay-flow run list [--repo <name>] [--workflow <name>] [--ticket <key>] [--active] [--json] [--no-color]")
|
|
312
|
+
fmt.Fprintln(w, "\nList durable runs. Use --json for automation.")
|
|
313
|
+
fmt.Fprintln(w, "Example: relay-flow run list --active --json")
|
|
314
|
+
case "run get":
|
|
315
|
+
fmt.Fprintln(w, "Usage: relay-flow run get --ticket <key> [--json] [--no-color]")
|
|
316
|
+
fmt.Fprintln(w, "\nShow an Argo-shaped execution detail view and actual step visits.")
|
|
317
|
+
fmt.Fprintln(w, "Example: relay-flow run get --ticket PAY-101")
|
|
318
|
+
case "run restart":
|
|
319
|
+
fmt.Fprintln(w, "Usage: relay-flow run restart --ticket <key>")
|
|
320
|
+
fmt.Fprintln(w, "\nStart a fresh attempt for a canceled run.")
|
|
321
|
+
fmt.Fprintln(w, "Example: relay-flow run restart --ticket PAY-101")
|
|
322
|
+
case "run cancel":
|
|
323
|
+
fmt.Fprintln(w, "Usage: relay-flow run cancel --ticket <key> [--reason <text>]")
|
|
324
|
+
fmt.Fprintln(w, "\nPermanently cancel the current execution attempt.")
|
|
325
|
+
fmt.Fprintln(w, "Example: relay-flow run cancel --ticket PAY-101 --reason "+`"operator request"`)
|
|
326
|
+
default:
|
|
327
|
+
fmt.Fprintf(os.Stderr, "unknown help target: %s\n", name)
|
|
328
|
+
return exitUsage
|
|
329
|
+
}
|
|
330
|
+
return exitOK
|
|
158
331
|
}
|
|
159
332
|
|
|
160
333
|
// --- init / serve delegate to the section-5 composition root ---
|
|
@@ -168,6 +341,14 @@ Usage:
|
|
|
168
341
|
// a TTY → three stdin lines (task, runner, harness), the documented test
|
|
169
342
|
// seam and script path.
|
|
170
343
|
func cmdInit(p paths.Paths, args []string, stdin io.Reader) int {
|
|
344
|
+
if err := recoverFirstRunPublication(p); err != nil {
|
|
345
|
+
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
346
|
+
return exitFail
|
|
347
|
+
}
|
|
348
|
+
if err := cleanupFirstRunStaging(p); err != nil {
|
|
349
|
+
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
350
|
+
return exitFail
|
|
351
|
+
}
|
|
171
352
|
fs := flag.NewFlagSet("init", flag.ContinueOnError)
|
|
172
353
|
taskName := fs.String("task-plugin", "", "task plugin name (non-interactive)")
|
|
173
354
|
runnerName := fs.String("runner-plugin", "", "runner plugin name (non-interactive)")
|
|
@@ -206,11 +387,17 @@ func cmdInit(p paths.Paths, args []string, stdin io.Reader) int {
|
|
|
206
387
|
fmt.Fprintln(os.Stderr, "init: database is missing; refusing to recreate existing durable state")
|
|
207
388
|
return exitFail
|
|
208
389
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
390
|
+
// A first interactive setup keeps the final home untouched until the
|
|
391
|
+
// selected task plugin has authenticated successfully. Scripted setup and
|
|
392
|
+
// --force retain the existing eager directory creation behavior.
|
|
393
|
+
firstRunInteractive := !*force && !configExists && !databaseExists && !flagged && interactiveInitTTY(stdin)
|
|
394
|
+
if !firstRunInteractive {
|
|
395
|
+
if err := os.MkdirAll(p.Root, 0o700); err != nil {
|
|
396
|
+
fmt.Fprintln(os.Stderr, err)
|
|
397
|
+
return exitFail
|
|
398
|
+
}
|
|
399
|
+
_ = os.Chmod(p.Root, 0o700)
|
|
212
400
|
}
|
|
213
|
-
_ = os.Chmod(p.Root, 0o700)
|
|
214
401
|
var unlock func()
|
|
215
402
|
if *force {
|
|
216
403
|
unlock, err = lockForForcedInit(p.Lock)
|
|
@@ -240,9 +427,9 @@ func cmdInit(p paths.Paths, args []string, stdin io.Reader) int {
|
|
|
240
427
|
switch {
|
|
241
428
|
case flagged:
|
|
242
429
|
names = []string{*taskName, *runnerName, *harnessName}
|
|
243
|
-
case
|
|
430
|
+
case interactiveInitTTY(stdin):
|
|
244
431
|
var err error
|
|
245
|
-
names, err =
|
|
432
|
+
names, err = interactiveInitPluginPick()
|
|
246
433
|
if err != nil {
|
|
247
434
|
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
248
435
|
return exitFail
|
|
@@ -258,7 +445,7 @@ func cmdInit(p paths.Paths, args []string, stdin io.Reader) int {
|
|
|
258
445
|
return exitFail
|
|
259
446
|
}
|
|
260
447
|
}
|
|
261
|
-
if executor == executorTemporal &&
|
|
448
|
+
if executor == executorTemporal && interactiveInitTTY(stdin) {
|
|
262
449
|
address, namespace, err := promptTemporalSettings(*temporalAddress, *temporalNamespace)
|
|
263
450
|
if err != nil {
|
|
264
451
|
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
@@ -353,6 +540,19 @@ func cmdInit(p paths.Paths, args []string, stdin io.Reader) int {
|
|
|
353
540
|
} else {
|
|
354
541
|
cfg.HarnessConfig = config.Merge(harnessDefaults, cfg.HarnessConfig)
|
|
355
542
|
}
|
|
543
|
+
var stagedAuthDir string
|
|
544
|
+
if firstRunInteractive {
|
|
545
|
+
if names[0] == "beads" {
|
|
546
|
+
fmt.Println("Beads authentication is managed by the Beads workspace and Dolt setup; relay-flow does not store task credentials.")
|
|
547
|
+
}
|
|
548
|
+
var authErr error
|
|
549
|
+
cfg, stagedAuthDir, authErr = stageFirstRunAuthentication(p, cfg, names[0], stdin)
|
|
550
|
+
if authErr != nil {
|
|
551
|
+
fmt.Fprintln(os.Stderr, "init: task auth: "+authErr.Error())
|
|
552
|
+
return exitFail
|
|
553
|
+
}
|
|
554
|
+
defer os.RemoveAll(stagedAuthDir)
|
|
555
|
+
}
|
|
356
556
|
identity := projection.ExecutorIdentity{
|
|
357
557
|
ExecutorPlugin: executor,
|
|
358
558
|
TemporalAddress: cfg.TemporalAddress,
|
|
@@ -373,13 +573,30 @@ func cmdInit(p paths.Paths, args []string, stdin io.Reader) int {
|
|
|
373
573
|
return exitFail
|
|
374
574
|
}
|
|
375
575
|
}
|
|
376
|
-
if !databaseExists {
|
|
576
|
+
if !firstRunInteractive && !databaseExists {
|
|
377
577
|
if err := projection.InitDatabaseWithIdentity(p.Database, identity); err != nil {
|
|
378
578
|
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
379
579
|
return exitFail
|
|
380
580
|
}
|
|
381
581
|
}
|
|
382
|
-
if
|
|
582
|
+
if firstRunInteractive {
|
|
583
|
+
if err := commitFirstRun(p, stagedAuthDir, identity); err != nil {
|
|
584
|
+
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
585
|
+
return exitFail
|
|
586
|
+
}
|
|
587
|
+
register, err := interactiveInitRepoPrompt()
|
|
588
|
+
if err != nil {
|
|
589
|
+
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
590
|
+
return exitFail
|
|
591
|
+
}
|
|
592
|
+
if register {
|
|
593
|
+
if err := interactiveInitRepoSetup(p, stdin); err != nil {
|
|
594
|
+
fmt.Fprintln(os.Stderr, "init: "+err.Error())
|
|
595
|
+
return exitFail
|
|
596
|
+
}
|
|
597
|
+
fmt.Println("Repository setup complete. The server is running; stop it with `relay-flow stop`.")
|
|
598
|
+
}
|
|
599
|
+
} else if err := config.SaveMachine(p.Config, cfg); err != nil {
|
|
383
600
|
fmt.Fprintln(os.Stderr, err)
|
|
384
601
|
return exitFail
|
|
385
602
|
}
|
|
@@ -540,55 +757,121 @@ func cmdServe(p paths.Paths, args []string) int {
|
|
|
540
757
|
return exitOK
|
|
541
758
|
}
|
|
542
759
|
|
|
760
|
+
var (
|
|
761
|
+
backgroundServeStartupTimeout = 10 * time.Second
|
|
762
|
+
backgroundServePollInterval = 50 * time.Millisecond
|
|
763
|
+
)
|
|
764
|
+
|
|
543
765
|
func startBackgroundServe(p paths.Paths, recover, debug bool) error {
|
|
544
766
|
client := server.NewClient(p.Socket)
|
|
545
767
|
if serverResponding(client, 200*time.Millisecond) {
|
|
546
768
|
return fmt.Errorf("serve --background: server is already running")
|
|
547
769
|
}
|
|
548
|
-
|
|
770
|
+
deadline := time.Now().Add(backgroundServeStartupTimeout)
|
|
771
|
+
lockHeld, err := serverLockHeld(p.Lock)
|
|
549
772
|
if err != nil {
|
|
550
|
-
return fmt.Errorf("serve --background:
|
|
773
|
+
return fmt.Errorf("serve --background: inspect server lock: %w", err)
|
|
551
774
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
775
|
+
|
|
776
|
+
var (
|
|
777
|
+
cmd *exec.Cmd
|
|
778
|
+
wait chan error
|
|
779
|
+
observedOwner = lockHeld
|
|
780
|
+
devNull *os.File
|
|
781
|
+
)
|
|
782
|
+
if !lockHeld {
|
|
783
|
+
executable, resolveErr := os.Executable()
|
|
784
|
+
if resolveErr != nil {
|
|
785
|
+
return fmt.Errorf("serve --background: resolve executable: %w", resolveErr)
|
|
786
|
+
}
|
|
787
|
+
childArgs := []string{"serve"}
|
|
788
|
+
if recover {
|
|
789
|
+
childArgs = append(childArgs, "--recover")
|
|
790
|
+
}
|
|
791
|
+
if debug {
|
|
792
|
+
childArgs = append(childArgs, "--debug")
|
|
793
|
+
}
|
|
794
|
+
devNull, err = os.OpenFile(os.DevNull, os.O_RDWR, 0)
|
|
795
|
+
if err != nil {
|
|
796
|
+
return fmt.Errorf("serve --background: open %s: %w", os.DevNull, err)
|
|
797
|
+
}
|
|
798
|
+
defer devNull.Close()
|
|
799
|
+
cmd = exec.Command(executable, childArgs...)
|
|
800
|
+
cmd.Stdin, cmd.Stdout, cmd.Stderr = devNull, devNull, devNull
|
|
801
|
+
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
|
802
|
+
if err := cmd.Start(); err != nil {
|
|
803
|
+
return fmt.Errorf("serve --background: start: %w; see %s", err, p.ServerLog)
|
|
804
|
+
}
|
|
805
|
+
wait = make(chan error, 1)
|
|
806
|
+
go func() { wait <- cmd.Wait() }()
|
|
558
807
|
}
|
|
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)
|
|
808
|
+
|
|
809
|
+
ticker := time.NewTicker(backgroundServePollInterval)
|
|
575
810
|
defer ticker.Stop()
|
|
576
811
|
for {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
812
|
+
if serverResponding(client, 200*time.Millisecond) {
|
|
813
|
+
return nil
|
|
814
|
+
}
|
|
815
|
+
if !time.Now().Before(deadline) {
|
|
816
|
+
if cmd != nil && wait != nil {
|
|
817
|
+
_ = cmd.Process.Kill()
|
|
818
|
+
<-wait
|
|
819
|
+
}
|
|
583
820
|
return fmt.Errorf("serve --background: startup timed out; see %s", p.ServerLog)
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
821
|
+
}
|
|
822
|
+
if observedOwner && wait == nil {
|
|
823
|
+
held, lockErr := serverLockHeld(p.Lock)
|
|
824
|
+
if lockErr != nil {
|
|
825
|
+
return fmt.Errorf("serve --background: inspect server lock: %w", lockErr)
|
|
826
|
+
}
|
|
827
|
+
if !held {
|
|
828
|
+
return fmt.Errorf("serve --background: server exited before readiness; see %s", p.ServerLog)
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
select {
|
|
832
|
+
case childErr := <-wait:
|
|
833
|
+
held, lockErr := serverLockHeld(p.Lock)
|
|
834
|
+
if lockErr != nil {
|
|
835
|
+
return fmt.Errorf("serve --background: inspect server lock: %w", lockErr)
|
|
836
|
+
}
|
|
837
|
+
if held {
|
|
838
|
+
// Another process acquired the lock while this child was
|
|
839
|
+
// starting. Wait for that owner instead of spawning again.
|
|
840
|
+
observedOwner = true
|
|
841
|
+
wait = nil
|
|
842
|
+
cmd = nil
|
|
843
|
+
continue
|
|
587
844
|
}
|
|
845
|
+
return fmt.Errorf("serve --background: server exited before readiness: %v; see %s", childErr, p.ServerLog)
|
|
846
|
+
case <-ticker.C:
|
|
588
847
|
}
|
|
589
848
|
}
|
|
590
849
|
}
|
|
591
850
|
|
|
851
|
+
func serverLockHeld(path string) (bool, error) {
|
|
852
|
+
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
|
|
853
|
+
if err != nil {
|
|
854
|
+
if os.IsNotExist(err) {
|
|
855
|
+
return false, nil
|
|
856
|
+
}
|
|
857
|
+
return false, err
|
|
858
|
+
}
|
|
859
|
+
defer f.Close()
|
|
860
|
+
if err := f.Chmod(0o600); err != nil {
|
|
861
|
+
return false, err
|
|
862
|
+
}
|
|
863
|
+
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
|
|
864
|
+
if err == syscall.EWOULDBLOCK || err == syscall.EAGAIN {
|
|
865
|
+
return true, nil
|
|
866
|
+
}
|
|
867
|
+
return false, err
|
|
868
|
+
}
|
|
869
|
+
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
|
|
870
|
+
return false, err
|
|
871
|
+
}
|
|
872
|
+
return false, nil
|
|
873
|
+
}
|
|
874
|
+
|
|
592
875
|
func serverResponding(client *server.Client, timeout time.Duration) bool {
|
|
593
876
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
594
877
|
defer cancel()
|
|
@@ -690,21 +973,28 @@ func cmdWorkflow(c *server.Client, args []string) int {
|
|
|
690
973
|
return exitOK
|
|
691
974
|
case "list":
|
|
692
975
|
fs := flag.NewFlagSet("workflow list", flag.ContinueOnError)
|
|
976
|
+
jsonOutput := fs.Bool("json", false, "print stable JSON")
|
|
977
|
+
fs.Bool("no-color", false, "disable terminal colors")
|
|
693
978
|
if err := fs.Parse(args[1:]); err != nil {
|
|
694
979
|
return exitUsage
|
|
695
980
|
}
|
|
696
|
-
|
|
981
|
+
finishLoading := beginLoading("Loading workflows", *jsonOutput)
|
|
982
|
+
summaries, err := c.ListWorkflowSummaries(context.Background())
|
|
983
|
+
finishLoading(err)
|
|
697
984
|
if err != nil {
|
|
698
985
|
fmt.Fprintln(os.Stderr, err)
|
|
699
986
|
return exitFail
|
|
700
987
|
}
|
|
701
|
-
|
|
702
|
-
|
|
988
|
+
if *jsonOutput {
|
|
989
|
+
return encodeJSON(summaries)
|
|
703
990
|
}
|
|
991
|
+
renderWorkflowSummaries(os.Stdout, summaries, cliRenderOptions())
|
|
704
992
|
return exitOK
|
|
705
993
|
case "get":
|
|
706
994
|
fs := flag.NewFlagSet("workflow get", flag.ContinueOnError)
|
|
707
995
|
name := fs.String("name", "", "workflow name")
|
|
996
|
+
jsonOutput := fs.Bool("json", false, "print stable JSON")
|
|
997
|
+
fs.Bool("no-color", false, "disable terminal colors")
|
|
708
998
|
if err := fs.Parse(args[1:]); err != nil {
|
|
709
999
|
return exitUsage
|
|
710
1000
|
}
|
|
@@ -712,14 +1002,23 @@ func cmdWorkflow(c *server.Client, args []string) int {
|
|
|
712
1002
|
fmt.Fprintln(os.Stderr, "workflow get: --name is required")
|
|
713
1003
|
return exitUsage
|
|
714
1004
|
}
|
|
715
|
-
|
|
1005
|
+
finishLoading := beginLoading("Loading workflow", *jsonOutput)
|
|
1006
|
+
if *jsonOutput {
|
|
1007
|
+
wf, err := c.GetWorkflow(context.Background(), *name)
|
|
1008
|
+
finishLoading(err)
|
|
1009
|
+
if err != nil {
|
|
1010
|
+
fmt.Fprintln(os.Stderr, err)
|
|
1011
|
+
return exitFail
|
|
1012
|
+
}
|
|
1013
|
+
return encodeJSON(wf)
|
|
1014
|
+
}
|
|
1015
|
+
detail, err := c.GetWorkflowDetail(context.Background(), *name)
|
|
1016
|
+
finishLoading(err)
|
|
716
1017
|
if err != nil {
|
|
717
1018
|
fmt.Fprintln(os.Stderr, err)
|
|
718
1019
|
return exitFail
|
|
719
1020
|
}
|
|
720
|
-
|
|
721
|
-
enc.SetIndent("", " ")
|
|
722
|
-
_ = enc.Encode(wf)
|
|
1021
|
+
renderWorkflowDetail(os.Stdout, detail, cliRenderOptions())
|
|
723
1022
|
return exitOK
|
|
724
1023
|
}
|
|
725
1024
|
usage(os.Stderr)
|
|
@@ -811,7 +1110,7 @@ func cmdRepoRegister(c *server.Client, flagName, flagPath string, sets kvFlags,
|
|
|
811
1110
|
}
|
|
812
1111
|
registration, err := loadRepoRegistration(ctx, c, sets)
|
|
813
1112
|
if err != nil {
|
|
814
|
-
fmt.Fprintln(os.Stderr, err)
|
|
1113
|
+
fmt.Fprintln(os.Stderr, standaloneRepoServerHint(err))
|
|
815
1114
|
return exitFail
|
|
816
1115
|
}
|
|
817
1116
|
taskCfg, err := registrationTaskConfig(registration, sets, flagName)
|
|
@@ -821,7 +1120,7 @@ func cmdRepoRegister(c *server.Client, flagName, flagPath string, sets kvFlags,
|
|
|
821
1120
|
}
|
|
822
1121
|
info, err := c.RegisterRepo(ctx, repo.RegisterInput{Name: flagName, Path: flagPath, TaskConfig: taskCfg})
|
|
823
1122
|
if err != nil {
|
|
824
|
-
fmt.Fprintln(os.Stderr, err)
|
|
1123
|
+
fmt.Fprintln(os.Stderr, standaloneRepoServerHint(err))
|
|
825
1124
|
return exitFail
|
|
826
1125
|
}
|
|
827
1126
|
fmt.Println(info.Name)
|
|
@@ -830,7 +1129,7 @@ func cmdRepoRegister(c *server.Client, flagName, flagPath string, sets kvFlags,
|
|
|
830
1129
|
|
|
831
1130
|
registration, err := c.RepoRegistrationFields(ctx, nil)
|
|
832
1131
|
if err != nil {
|
|
833
|
-
fmt.Fprintln(os.Stderr, err)
|
|
1132
|
+
fmt.Fprintln(os.Stderr, standaloneRepoServerHint(err))
|
|
834
1133
|
return exitFail
|
|
835
1134
|
}
|
|
836
1135
|
|
|
@@ -840,23 +1139,17 @@ func cmdRepoRegister(c *server.Client, flagName, flagPath string, sets kvFlags,
|
|
|
840
1139
|
}
|
|
841
1140
|
candidates, err := c.DiscoverRepos(ctx)
|
|
842
1141
|
if err != nil {
|
|
843
|
-
fmt.Fprintln(os.Stderr, err)
|
|
1142
|
+
fmt.Fprintln(os.Stderr, standaloneRepoServerHint(err))
|
|
844
1143
|
return exitFail
|
|
845
1144
|
}
|
|
846
|
-
|
|
847
|
-
|
|
1145
|
+
registered, err := c.ListRepos(ctx)
|
|
1146
|
+
if err != nil {
|
|
1147
|
+
fmt.Fprintln(os.Stderr, standaloneRepoServerHint(err))
|
|
848
1148
|
return exitFail
|
|
849
1149
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
for i, cand := range candidates {
|
|
854
|
-
options[i] = huh.NewOption(cand.Name+" ("+cand.Path+")", i)
|
|
855
|
-
}
|
|
856
|
-
pickField := repoMultiSelect(options, &selected)
|
|
857
|
-
pick := huh.NewForm(huh.NewGroup(pickField))
|
|
858
|
-
if err := pick.Run(); err != nil {
|
|
859
|
-
fmt.Fprintln(os.Stderr, "repo register: "+err.Error())
|
|
1150
|
+
candidates, selected, err := selectReposInteractive(ctx, c, candidates, registered)
|
|
1151
|
+
if err != nil {
|
|
1152
|
+
fmt.Fprintln(os.Stderr, "repo register: "+standaloneRepoServerHint(err).Error())
|
|
860
1153
|
return exitFail
|
|
861
1154
|
}
|
|
862
1155
|
|
|
@@ -868,7 +1161,7 @@ func cmdRepoRegister(c *server.Client, flagName, flagPath string, sets kvFlags,
|
|
|
868
1161
|
if len(shared) > 0 {
|
|
869
1162
|
dependent, err := c.RepoRegistrationFields(ctx, flatRegistrationValues(shared))
|
|
870
1163
|
if err != nil {
|
|
871
|
-
fmt.Fprintln(os.Stderr, err)
|
|
1164
|
+
fmt.Fprintln(os.Stderr, standaloneRepoServerHint(err))
|
|
872
1165
|
return exitFail
|
|
873
1166
|
}
|
|
874
1167
|
registration = mergeRegistration(registration, dependent)
|
|
@@ -878,7 +1171,7 @@ func cmdRepoRegister(c *server.Client, flagName, flagPath string, sets kvFlags,
|
|
|
878
1171
|
}
|
|
879
1172
|
}
|
|
880
1173
|
if err := registerSelectedReposDynamic(ctx, c, candidates, selected, registration, shared); err != nil {
|
|
881
|
-
fmt.Fprintln(os.Stderr, "repo register: "+err.Error())
|
|
1174
|
+
fmt.Fprintln(os.Stderr, "repo register: "+standaloneRepoServerHint(err).Error())
|
|
882
1175
|
return exitFail
|
|
883
1176
|
}
|
|
884
1177
|
return exitOK
|
|
@@ -887,6 +1180,8 @@ func cmdRepoRegister(c *server.Client, flagName, flagPath string, sets kvFlags,
|
|
|
887
1180
|
func repoMultiSelect(options []huh.Option[int], selected *[]int) *huh.MultiSelect[int] {
|
|
888
1181
|
return huh.NewMultiSelect[int]().
|
|
889
1182
|
Title("Select repositories").
|
|
1183
|
+
Description("Press / to filter repositories; select [+] Add repository to add a runner resource.").
|
|
1184
|
+
Filterable(true).
|
|
890
1185
|
Options(options...).
|
|
891
1186
|
Value(selected).
|
|
892
1187
|
Validate(func(values []int) error {
|
|
@@ -927,25 +1222,35 @@ func cmdRun(c *server.Client, args []string) int {
|
|
|
927
1222
|
repoName := fs.String("repo", "", "filter by repo")
|
|
928
1223
|
workflowName := fs.String("workflow", "", "filter by workflow")
|
|
929
1224
|
ticket := fs.String("ticket", "", "filter by ticket")
|
|
1225
|
+
activeOnly := fs.Bool("active", false, "show only active runs")
|
|
1226
|
+
jsonOutput := fs.Bool("json", false, "print stable JSON")
|
|
1227
|
+
fs.Bool("no-color", false, "disable terminal colors")
|
|
930
1228
|
if err := fs.Parse(args[1:]); err != nil {
|
|
931
1229
|
return exitUsage
|
|
932
1230
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
}
|
|
1231
|
+
var active *bool
|
|
1232
|
+
if *activeOnly {
|
|
1233
|
+
value := true
|
|
1234
|
+
active = &value
|
|
1235
|
+
}
|
|
1236
|
+
filter := runsvc.Filter{Repo: *repoName, Workflow: *workflowName, Ticket: *ticket, Active: active}
|
|
1237
|
+
finishLoading := beginLoading("Loading runs", *jsonOutput)
|
|
1238
|
+
runs, err := c.ListRuns(context.Background(), filter)
|
|
1239
|
+
finishLoading(err)
|
|
938
1240
|
if err != nil {
|
|
939
1241
|
fmt.Fprintln(os.Stderr, err)
|
|
940
1242
|
return exitFail
|
|
941
1243
|
}
|
|
942
|
-
|
|
943
|
-
|
|
1244
|
+
if *jsonOutput {
|
|
1245
|
+
return encodeJSON(runs)
|
|
944
1246
|
}
|
|
1247
|
+
renderRunsFiltered(os.Stdout, runs, cliRenderOptions(), filter)
|
|
945
1248
|
return exitOK
|
|
946
1249
|
case "get":
|
|
947
1250
|
fs := flag.NewFlagSet("run get", flag.ContinueOnError)
|
|
948
1251
|
ticket := fs.String("ticket", "", "ticket key")
|
|
1252
|
+
jsonOutput := fs.Bool("json", false, "print stable JSON")
|
|
1253
|
+
fs.Bool("no-color", false, "disable terminal colors")
|
|
949
1254
|
if err := fs.Parse(args[1:]); err != nil {
|
|
950
1255
|
return exitUsage
|
|
951
1256
|
}
|
|
@@ -953,14 +1258,17 @@ func cmdRun(c *server.Client, args []string) int {
|
|
|
953
1258
|
fmt.Fprintln(os.Stderr, "run get: --ticket is required")
|
|
954
1259
|
return exitUsage
|
|
955
1260
|
}
|
|
956
|
-
|
|
1261
|
+
finishLoading := beginLoading("Loading run", *jsonOutput)
|
|
1262
|
+
detail, err := c.GetRunDetailByTicket(context.Background(), *ticket)
|
|
1263
|
+
finishLoading(err)
|
|
957
1264
|
if err != nil {
|
|
958
1265
|
fmt.Fprintln(os.Stderr, err)
|
|
959
1266
|
return exitFail
|
|
960
1267
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1268
|
+
if *jsonOutput {
|
|
1269
|
+
return encodeJSON(detail)
|
|
1270
|
+
}
|
|
1271
|
+
renderRunDetail(os.Stdout, detail, cliRenderOptions())
|
|
964
1272
|
return exitOK
|
|
965
1273
|
case "restart":
|
|
966
1274
|
fs := flag.NewFlagSet("run restart", flag.ContinueOnError)
|