Run each workflow stage as its own task, with fan-out and fan-in

This commit is contained in:
Ian Gulliver
2026-10-15 22:53:54 -07:00
parent f7a0ffcd4c
commit 2180a7e824
15 changed files with 739 additions and 408 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"g.fc.run/misc/byre/internal/api"
)
var keyOrder = []string{"description", "stages", "name", "model", "prompt"}
var keyOrder = []string{"description", "stages", "name", "after", "base", "model", "prompt"}
func Parse(data []byte) (api.WorkflowDefinition, error) {
raw := any(nil)
+112
View File
@@ -69,6 +69,32 @@ func Validate(def api.WorkflowDefinition) error {
if strings.TrimSpace(s.Prompt) == "" {
problems = append(problems, fmt.Sprintf("%s: prompt is empty", where))
}
if s.Base != nil && !s.Base.Valid() {
problems = append(problems, fmt.Sprintf("%s: base must be parent or main, not %q", where, *s.Base))
}
if s.Base != nil && len(After(def, s.Name)) > 1 {
problems = append(problems, fmt.Sprintf("%s: base can't be given on a stage with more than one after stage, which always branches from the default branch", where))
}
if s.After == nil {
continue
}
listed := map[string]bool{}
for _, a := range *s.After {
switch {
case a == s.Name:
problems = append(problems, fmt.Sprintf("%s: after lists the stage itself", where))
case Index(def, a) < 0:
problems = append(problems, fmt.Sprintf("%s: after lists %q, which isn't a stage", where, a))
case listed[a]:
problems = append(problems, fmt.Sprintf("%s: after lists %q twice", where, a))
}
listed[a] = true
}
}
if len(problems) == 0 {
problems = append(problems, graphProblems(def)...)
}
if len(problems) > 0 {
@@ -76,3 +102,89 @@ func Validate(def api.WorkflowDefinition) error {
}
return nil
}
func graphProblems(def api.WorkflowDefinition) []string {
problems := []string{}
finals := []string{}
for _, s := range def.Stages {
if len(Next(def, s.Name)) == 0 {
finals = append(finals, fmt.Sprintf("%q", s.Name))
}
}
if len(finals) != 1 {
problems = append(problems, fmt.Sprintf("it must have exactly one final stage (one no other stage comes after), but has %d: %s", len(finals), strings.Join(finals, ", ")))
}
state := map[string]int{}
var visit func(name string, path []string)
visit = func(name string, path []string) {
switch state[name] {
case 1:
problems = append(problems, fmt.Sprintf("its stages form a cycle: %s", strings.Join(append(path, fmt.Sprintf("%q", name)), " after ")))
return
case 2:
return
}
state[name] = 1
for _, a := range After(def, name) {
visit(a, append(path, fmt.Sprintf("%q", name)))
}
state[name] = 2
}
for _, s := range def.Stages {
visit(s.Name, nil)
}
return problems
}
func Index(def api.WorkflowDefinition, name string) int {
for i, s := range def.Stages {
if s.Name == name {
return i
}
}
return -1
}
func Get(def api.WorkflowDefinition, name string) api.Stage {
return def.Stages[Index(def, name)]
}
func After(def api.WorkflowDefinition, name string) []string {
i := Index(def, name)
s := def.Stages[i]
if s.After != nil {
return *s.After
}
if i == 0 {
return []string{}
}
return []string{def.Stages[i-1].Name}
}
func Next(def api.WorkflowDefinition, name string) []string {
next := []string{}
for _, s := range def.Stages {
for _, a := range After(def, s.Name) {
if a == name {
next = append(next, s.Name)
}
}
}
return next
}
func Starts(def api.WorkflowDefinition) []string {
starts := []string{}
for _, s := range def.Stages {
if len(After(def, s.Name)) == 0 {
starts = append(starts, s.Name)
}
}
return starts
}
func FromMain(def api.WorkflowDefinition, name string) bool {
s := Get(def, name)
return len(After(def, name)) > 1 || (s.Base != nil && *s.Base == api.StageBaseMain)
}