Add review workflows, tree-shaped workflows and a workflow editor

This commit is contained in:
Ian Gulliver
2026-10-20 18:52:04 -07:00
parent 9869fad988
commit f2188c53eb
31 changed files with 2179 additions and 863 deletions
+51 -5
View File
@@ -28,7 +28,25 @@ func (m *Manager) SaveWorkflow(ctx context.Context, name string, file []byte, ac
if err != nil {
return api.WorkflowSummary{}, err
}
return api.WorkflowSummary{Name: name, Description: def.Description}, nil
kind := api.WorkflowSummaryKindTask
if def.Kind == api.WorkflowKindReview {
kind = api.WorkflowSummaryKindReview
}
return api.WorkflowSummary{Name: name, Description: def.Description, Kind: kind}, nil
}
func (m *Manager) SetReviewWorkflow(ctx context.Context, projectID, name, actor string) (api.Project, error) {
name = strings.TrimSpace(name)
if name != "" {
def, err := m.store.GetWorkflow(ctx, name)
if err != nil {
return api.Project{}, err
}
if def.Kind != api.WorkflowKindReview {
return api.Project{}, fmt.Errorf("%w: workflow %q is a task workflow; a project reviews with a workflow of kind review", ErrInvalid, name)
}
}
return m.store.SetReviewWorkflow(ctx, projectID, name, actor)
}
func (m *Manager) WorkflowFile(ctx context.Context, name string) ([]byte, error) {
@@ -150,13 +168,13 @@ func (m *Manager) WorkflowRun(ctx context.Context, taskID string) (api.WorkflowR
attempts[*t.WorkflowStage] = append(attempts[*t.WorkflowStage], a)
}
for _, s := range frozen.Definition.Stages {
for _, s := range workflow.Stages(frozen.Definition) {
stage := api.WorkflowRunStage{
Name: s.Name,
After: workflow.After(frozen.Definition, s.Name),
Name: s.Stage,
After: workflow.After(frozen.Definition, s.Stage),
State: api.StageStateWaiting,
WaitingOn: []string{},
Attempts: attempts[s.Name],
Attempts: attempts[s.Stage],
}
if stage.Attempts == nil {
stage.Attempts = []api.StageAttempt{}
@@ -212,6 +230,9 @@ func (m *Manager) StartWorkflow(ctx context.Context, project api.Project, creato
if err != nil {
return nil, err
}
if frozen.Definition.Kind == api.WorkflowKindReview {
return nil, fmt.Errorf("%w: workflow %q is a review workflow, which a project names to review its snapshots; it doesn't run on a brief", ErrInvalid, name)
}
frozen.Brief = brief
frozen.Title = title
@@ -726,6 +747,31 @@ func (m *Manager) workflowTools(tool func(name, description, schema string, run
return fmt.Sprintf("Saved workflow %q.", w.Name), nil
}),
tool("set_review_workflow",
"Set the review workflow (a saved workflow of kind review) whose stages review every snapshot of a project's tasks, from the next snapshot on; an empty workflow turns reviews off for the project. Only when the human asks.",
`{"type": "object", "properties": {
"project_id": {"type": "string"},
"workflow": {"type": "string"}
}, "required": ["project_id", "workflow"]}`,
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
args := struct {
ProjectID string `json:"project_id"`
Workflow string `json:"workflow"`
}{}
err := json.Unmarshal(raw, &args)
if err != nil {
return "", err
}
p, err := m.SetReviewWorkflow(ctx, args.ProjectID, args.Workflow, leadActor(user.Login))
if err != nil {
return "", err
}
if p.ReviewWorkflow == "" {
return fmt.Sprintf("Project %s has no review workflow now: its snapshots aren't reviewed.", p.Name), nil
}
return fmt.Sprintf("Project %s reviews with workflow %q from its next snapshot on.", p.Name, p.ReviewWorkflow), nil
}),
tool("delete_workflow",
"Delete a workflow from the server. Tasks that froze it keep their copy. Only when the human asks.",
`{"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}`,