Add review workflows, tree-shaped workflows and a workflow editor
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
"g.fc.run/ai/byre/internal/workflow"
|
||||
)
|
||||
|
||||
type oldStage struct {
|
||||
Name string `json:"name"`
|
||||
After *[]string `json:"after"`
|
||||
Base *string `json:"base"`
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Agents *[]api.TeamMember `json:"agents"`
|
||||
}
|
||||
|
||||
type oldDefinition struct {
|
||||
Description string `json:"description"`
|
||||
Stages []oldStage `json:"stages"`
|
||||
Steps *json.RawMessage `json:"steps"`
|
||||
}
|
||||
|
||||
func migrateWorkflowSteps(tx *sql.Tx) error {
|
||||
err := convertColumn(tx, "workflows", "name", "definition")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return convertColumn(tx, "tasks", "id", "workflow_definition")
|
||||
}
|
||||
|
||||
func convertColumn(tx *sql.Tx, table, key, column string) error {
|
||||
rows, err := tx.Query(fmt.Sprintf(`SELECT %s, %s FROM %s WHERE %s != ''`, key, column, table, column))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
type row struct{ key, def string }
|
||||
all := []row{}
|
||||
for rows.Next() {
|
||||
r := row{}
|
||||
err = rows.Scan(&r.key, &r.def)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
all = append(all, r)
|
||||
}
|
||||
rows.Close()
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, r := range all {
|
||||
old := oldDefinition{}
|
||||
err = json.Unmarshal([]byte(r.def), &old)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %s: %w", table, r.key, err)
|
||||
}
|
||||
if old.Steps != nil {
|
||||
continue
|
||||
}
|
||||
def, err := toSteps(old)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %s: %w", table, r.key, err)
|
||||
}
|
||||
j, err := json.Marshal(def)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(fmt.Sprintf(`UPDATE %s SET %s = ? WHERE %s = ?`, table, column, key), string(j), r.key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func toSteps(old oldDefinition) (api.WorkflowDefinition, error) {
|
||||
after := map[string][]string{}
|
||||
byName := map[string]oldStage{}
|
||||
for i, s := range old.Stages {
|
||||
if s.Base != nil && *s.Base != "parent" {
|
||||
return api.WorkflowDefinition{}, fmt.Errorf("stage %q has base %q, which steps can't express", s.Name, *s.Base)
|
||||
}
|
||||
byName[s.Name] = s
|
||||
switch {
|
||||
case s.After != nil:
|
||||
after[s.Name] = *s.After
|
||||
case i == 0:
|
||||
after[s.Name] = []string{}
|
||||
default:
|
||||
after[s.Name] = []string{old.Stages[i-1].Name}
|
||||
}
|
||||
}
|
||||
|
||||
pending := func(names []string, done map[string]bool) []string {
|
||||
out := []string{}
|
||||
for _, n := range names {
|
||||
if !done[n] {
|
||||
out = append(out, n)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
var closure func(name string, done map[string]bool) map[string]bool
|
||||
closure = func(name string, done map[string]bool) map[string]bool {
|
||||
c := map[string]bool{name: true}
|
||||
for _, a := range pending(after[name], done) {
|
||||
for n := range closure(a, done) {
|
||||
c[n] = true
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
sequence := func(steps ...api.WorkflowStep) api.WorkflowStep {
|
||||
order := []api.WorkflowStep{}
|
||||
for _, s := range steps {
|
||||
if s.Order != nil {
|
||||
order = append(order, *s.Order...)
|
||||
continue
|
||||
}
|
||||
order = append(order, s)
|
||||
}
|
||||
return api.WorkflowStep{Order: &order}
|
||||
}
|
||||
|
||||
var tree func(ends []string, done map[string]bool) (api.WorkflowStep, error)
|
||||
tree = func(ends []string, done map[string]bool) (api.WorkflowStep, error) {
|
||||
groups := [][]string{}
|
||||
for _, e := range ends {
|
||||
merged := []string{e}
|
||||
kept := [][]string{}
|
||||
for _, g := range groups {
|
||||
if slices.ContainsFunc(g, func(other string) bool { return shareAny(closure(e, done), closure(other, done)) }) {
|
||||
merged = append(g, merged...)
|
||||
continue
|
||||
}
|
||||
kept = append(kept, g)
|
||||
}
|
||||
groups = append(kept, merged)
|
||||
}
|
||||
if len(groups) > 1 {
|
||||
branches := []api.WorkflowStep{}
|
||||
for _, g := range groups {
|
||||
b, err := tree(g, done)
|
||||
if err != nil {
|
||||
return api.WorkflowStep{}, err
|
||||
}
|
||||
branches = append(branches, b)
|
||||
}
|
||||
return api.WorkflowStep{Parallel: &branches}, nil
|
||||
}
|
||||
|
||||
if len(ends) > 1 {
|
||||
common := closure(ends[0], done)
|
||||
for _, e := range ends[1:] {
|
||||
c := closure(e, done)
|
||||
for n := range common {
|
||||
if !c[n] {
|
||||
delete(common, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
heads := []string{}
|
||||
for n := range common {
|
||||
later := false
|
||||
for m := range common {
|
||||
later = later || (m != n && closure(m, done)[n])
|
||||
}
|
||||
if !later {
|
||||
heads = append(heads, n)
|
||||
}
|
||||
}
|
||||
if len(heads) == 0 {
|
||||
return api.WorkflowStep{}, fmt.Errorf("stages %v overlap in earlier stages without sharing all of them, which steps can't express", ends)
|
||||
}
|
||||
slices.Sort(heads)
|
||||
prefix, err := tree(heads, done)
|
||||
if err != nil {
|
||||
return api.WorkflowStep{}, err
|
||||
}
|
||||
next := maps.Clone(done)
|
||||
for n := range common {
|
||||
next[n] = true
|
||||
}
|
||||
rest, err := tree(ends, next)
|
||||
if err != nil {
|
||||
return api.WorkflowStep{}, err
|
||||
}
|
||||
return sequence(prefix, rest), nil
|
||||
}
|
||||
|
||||
s := byName[ends[0]]
|
||||
stage := api.WorkflowStep{Stage: s.Name, Model: s.Model, Prompt: s.Prompt, Agents: s.Agents}
|
||||
preds := pending(after[s.Name], done)
|
||||
if len(preds) == 0 {
|
||||
return stage, nil
|
||||
}
|
||||
before, err := tree(preds, done)
|
||||
if err != nil {
|
||||
return api.WorkflowStep{}, err
|
||||
}
|
||||
return sequence(before, stage), nil
|
||||
}
|
||||
|
||||
finals := []string{}
|
||||
for _, s := range old.Stages {
|
||||
next := false
|
||||
for _, a := range after {
|
||||
next = next || slices.Contains(a, s.Name)
|
||||
}
|
||||
if !next {
|
||||
finals = append(finals, s.Name)
|
||||
}
|
||||
}
|
||||
top, err := tree(finals, map[string]bool{})
|
||||
if err != nil {
|
||||
return api.WorkflowDefinition{}, err
|
||||
}
|
||||
steps := []api.WorkflowStep{top}
|
||||
if top.Order != nil {
|
||||
steps = *top.Order
|
||||
}
|
||||
def := api.WorkflowDefinition{Description: old.Description, Steps: steps}
|
||||
|
||||
if len(workflow.Stages(def)) != len(old.Stages) {
|
||||
return api.WorkflowDefinition{}, fmt.Errorf("converted to %d stages from %d", len(workflow.Stages(def)), len(old.Stages))
|
||||
}
|
||||
for _, s := range old.Stages {
|
||||
got := workflow.After(def, s.Name)
|
||||
want := after[s.Name]
|
||||
if !slices.Equal(sorted(got), sorted(want)) {
|
||||
return api.WorkflowDefinition{}, fmt.Errorf("stage %q converted to come after %v, not %v", s.Name, got, want)
|
||||
}
|
||||
}
|
||||
return def, nil
|
||||
}
|
||||
|
||||
func shareAny(a, b map[string]bool) bool {
|
||||
for n := range a {
|
||||
if b[n] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sorted(names []string) []string {
|
||||
out := slices.Clone(names)
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
ALTER TABLE projects ADD COLUMN review_workflow TEXT NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE reviews ADD COLUMN stage TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE reviews ADD COLUMN definition TEXT NOT NULL DEFAULT '';
|
||||
|
||||
UPDATE reviews SET stage = 'Review' WHERE state != 'skipped' AND stage = '';
|
||||
|
||||
UPDATE events SET payload = json_set(payload, '$.stage', CASE json_extract(payload, '$.state') WHEN 'skipped' THEN '' ELSE 'Review' END)
|
||||
WHERE kind IN ('review.started', 'review.finished', 'review.finding') AND json_extract(payload, '$.stage') IS NULL;
|
||||
|
||||
UPDATE events SET payload = json_set(payload, '$.input.reviewer', 'Review')
|
||||
WHERE kind = 'agent.tool_call' AND json_extract(payload, '$.name') LIKE '%respond_to_finding'
|
||||
AND json_extract(payload, '$.input.reviewer') IS NULL;
|
||||
|
||||
INSERT OR IGNORE INTO workflows (name, definition)
|
||||
SELECT 'Review', json_object(
|
||||
'description', 'One reviewer looks over all of the task''s work.',
|
||||
'kind', 'review',
|
||||
'steps', json_array(json_object(
|
||||
'stage', 'Review',
|
||||
'model', u.review_model,
|
||||
'prompt', 'Look for bugs, security problems, missing or wrong tests, and anything that doesn''t do what the task asked.'
|
||||
))
|
||||
)
|
||||
FROM projects p JOIN users u ON u.login = p.created_by
|
||||
ORDER BY p.created_at
|
||||
LIMIT 1;
|
||||
|
||||
UPDATE projects SET review_workflow = 'Review'
|
||||
WHERE review_workflow = '' AND EXISTS (SELECT 1 FROM workflows WHERE name = 'Review');
|
||||
|
||||
ALTER TABLE users DROP COLUMN review_model;
|
||||
ALTER TABLE users DROP COLUMN second_review_model;
|
||||
@@ -53,17 +53,3 @@ func (s *Store) UpdateAway(ctx context.Context, login string, away bool) (api.Us
|
||||
}
|
||||
return s.GetUser(ctx, login)
|
||||
}
|
||||
|
||||
func (s *Store) UpdateReviewModels(ctx context.Context, login, model, second string) (api.User, error) {
|
||||
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
_, err := tx.ExecContext(ctx, `UPDATE users SET review_model = ?, second_review_model = ? WHERE login = ?`, model, second, login)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return emit(api.Event{Kind: "user.updated", Actor: login, Payload: map[string]any{"login": login}})
|
||||
})
|
||||
if err != nil {
|
||||
return api.User{}, err
|
||||
}
|
||||
return s.GetUser(ctx, login)
|
||||
}
|
||||
|
||||
@@ -10,18 +10,26 @@ import (
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
)
|
||||
|
||||
const reviewColumns = `id, task_id, snapshot_n, coalesce(agent_session_id, ''), vendor, model, state, result, error, created_at, finished_at, finding_states, finding_responses`
|
||||
const reviewColumns = `id, task_id, snapshot_n, stage, coalesce(agent_session_id, ''), vendor, model, state, result, error, created_at, finished_at, finding_states, finding_responses`
|
||||
|
||||
func (s *Store) CreateReview(ctx context.Context, r api.Review) error {
|
||||
func (s *Store) CreateReview(ctx context.Context, r api.Review, def *api.WorkflowDefinition) error {
|
||||
finishedAt := ""
|
||||
if r.FinishedAt != nil {
|
||||
finishedAt = formatTime(*r.FinishedAt)
|
||||
}
|
||||
defText := ""
|
||||
if def != nil {
|
||||
b, err := json.Marshal(def)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defText = string(b)
|
||||
}
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO reviews (id, task_id, snapshot_n, agent_session_id, vendor, model, state, created_at, finished_at)
|
||||
VALUES (?, ?, ?, nullif(?, ''), ?, ?, ?, ?, ?)
|
||||
`, r.Id, r.TaskId, r.SnapshotN, r.AgentSessionId, r.Vendor, r.Model, string(r.State), formatTime(r.CreatedAt), finishedAt)
|
||||
INSERT INTO reviews (id, task_id, snapshot_n, stage, definition, agent_session_id, vendor, model, state, error, created_at, finished_at)
|
||||
VALUES (?, ?, ?, ?, ?, nullif(?, ''), ?, ?, ?, ?, ?, ?)
|
||||
`, r.Id, r.TaskId, r.SnapshotN, r.Stage, defText, r.AgentSessionId, r.Vendor, r.Model, string(r.State), r.Error, formatTime(r.CreatedAt), finishedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -32,6 +40,23 @@ func (s *Store) CreateReview(ctx context.Context, r api.Review) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) ReviewDefinition(ctx context.Context, id string) (api.WorkflowDefinition, error) {
|
||||
text := ""
|
||||
err := s.db.QueryRowContext(ctx, `SELECT definition FROM reviews WHERE id = ?`, id).Scan(&text)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return api.WorkflowDefinition{}, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return api.WorkflowDefinition{}, err
|
||||
}
|
||||
def := api.WorkflowDefinition{}
|
||||
if text == "" {
|
||||
return def, nil
|
||||
}
|
||||
err = json.Unmarshal([]byte(text), &def)
|
||||
return def, err
|
||||
}
|
||||
|
||||
func (s *Store) GetReview(ctx context.Context, id string) (api.Review, error) {
|
||||
r, err := scanReview(s.db.QueryRowContext(ctx, `SELECT `+reviewColumns+` FROM reviews WHERE id = ?`, id))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@@ -49,7 +74,7 @@ func (s *Store) ReviewBySession(ctx context.Context, sessionID string) (api.Revi
|
||||
}
|
||||
|
||||
func (s *Store) ListReviews(ctx context.Context, taskID string) ([]api.Review, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT `+reviewColumns+` FROM reviews WHERE task_id = ? ORDER BY snapshot_n`, taskID)
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT `+reviewColumns+` FROM reviews WHERE task_id = ? ORDER BY snapshot_n, created_at`, taskID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -64,6 +89,29 @@ func (s *Store) RunningReviews(ctx context.Context) ([]api.Review, error) {
|
||||
return scanReviews(rows)
|
||||
}
|
||||
|
||||
func (s *Store) LatestDoneStages(ctx context.Context) ([]string, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT r.id FROM reviews r JOIN tasks t ON t.id = r.task_id
|
||||
WHERE t.status = ? AND r.state = ? AND r.definition != ''
|
||||
AND r.snapshot_n = (SELECT max(snapshot_n) FROM reviews WHERE task_id = r.task_id)
|
||||
ORDER BY r.created_at`, string(api.TaskStatusActive), string(api.ReviewStateDone))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
ids := []string{}
|
||||
for rows.Next() {
|
||||
id := ""
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) FinishReview(ctx context.Context, id string, state api.ReviewState, result map[string]any, errText string) error {
|
||||
text := ""
|
||||
if result != nil {
|
||||
@@ -205,7 +253,7 @@ func scanReview(row scanner) (api.Review, error) {
|
||||
finishedAt := ""
|
||||
findingStates := ""
|
||||
findingResponses := ""
|
||||
err := row.Scan(&r.Id, &r.TaskId, &r.SnapshotN, &r.AgentSessionId, &r.Vendor, &r.Model, &state, &result, &r.Error, &createdAt, &finishedAt, &findingStates, &findingResponses)
|
||||
err := row.Scan(&r.Id, &r.TaskId, &r.SnapshotN, &r.Stage, &r.AgentSessionId, &r.Vendor, &r.Model, &state, &result, &r.Error, &createdAt, &finishedAt, &findingStates, &findingResponses)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
|
||||
+58
-14
@@ -9,6 +9,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -25,8 +26,14 @@ var ErrNotFound = errors.New("not found")
|
||||
var migrationFiles embed.FS
|
||||
|
||||
type migration struct {
|
||||
key string
|
||||
name string
|
||||
sql string
|
||||
run func(*sql.Tx) error
|
||||
}
|
||||
|
||||
var goMigrations = map[string]func(*sql.Tx) error{
|
||||
"071-workflow-steps": migrateWorkflowSteps,
|
||||
}
|
||||
|
||||
func loadMigrations() ([]migration, error) {
|
||||
@@ -37,17 +44,25 @@ func loadMigrations() ([]migration, error) {
|
||||
|
||||
migrations := []migration{}
|
||||
for _, e := range entries {
|
||||
num, name, ok := strings.Cut(strings.TrimSuffix(e.Name(), ".sql"), "-")
|
||||
_, err = strconv.Atoi(num)
|
||||
if !ok || len(num) != 3 || err != nil {
|
||||
return nil, fmt.Errorf("migration file %s: want NNN-name.sql", e.Name())
|
||||
}
|
||||
|
||||
key := strings.TrimSuffix(e.Name(), ".sql")
|
||||
sql, err := migrationFiles.ReadFile("migrations/" + e.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
migrations = append(migrations, migration{name: name, sql: string(sql)})
|
||||
migrations = append(migrations, migration{key: key, sql: string(sql)})
|
||||
}
|
||||
for key, run := range goMigrations {
|
||||
migrations = append(migrations, migration{key: key, run: run})
|
||||
}
|
||||
slices.SortFunc(migrations, func(a, b migration) int { return strings.Compare(a.key, b.key) })
|
||||
|
||||
for i, m := range migrations {
|
||||
num, name, ok := strings.Cut(m.key, "-")
|
||||
_, err = strconv.Atoi(num)
|
||||
if !ok || len(num) != 3 || err != nil {
|
||||
return nil, fmt.Errorf("migration %s: want NNN-name", m.key)
|
||||
}
|
||||
migrations[i].name = name
|
||||
}
|
||||
return migrations, nil
|
||||
}
|
||||
@@ -151,7 +166,11 @@ func applyMigration(db *sql.DB, m migration) error {
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
_, err = tx.Exec(m.sql)
|
||||
if m.run != nil {
|
||||
err = m.run(tx)
|
||||
} else {
|
||||
_, err = tx.Exec(m.sql)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -218,8 +237,8 @@ func (s *Store) EnsureUser(ctx context.Context, login, displayName string) (api.
|
||||
func (s *Store) GetUser(ctx context.Context, login string) (api.User, error) {
|
||||
u := api.User{}
|
||||
prefs := ""
|
||||
err := s.db.QueryRowContext(ctx, `SELECT login, display_name, git_name, git_email, notification_prefs, default_model, review_model, second_review_model, away FROM users WHERE login = ?`, login).
|
||||
Scan(&u.Login, &u.DisplayName, &u.GitName, &u.GitEmail, &prefs, &u.DefaultModel, &u.ReviewModel, &u.SecondReviewModel, &u.Away)
|
||||
err := s.db.QueryRowContext(ctx, `SELECT login, display_name, git_name, git_email, notification_prefs, default_model, away FROM users WHERE login = ?`, login).
|
||||
Scan(&u.Login, &u.DisplayName, &u.GitName, &u.GitEmail, &prefs, &u.DefaultModel, &u.Away)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return u, ErrNotFound
|
||||
}
|
||||
@@ -275,6 +294,31 @@ func (s *Store) ListProjects(ctx context.Context) ([]api.Project, error) {
|
||||
return projects, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) SetReviewWorkflow(ctx context.Context, id, workflow, actor string) (api.Project, error) {
|
||||
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
res, err := tx.ExecContext(ctx, `UPDATE projects SET review_workflow = ? WHERE id = ?`, workflow, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
p, err := scanProject(tx.QueryRowContext(ctx, `SELECT `+projectColumns+` FROM projects WHERE id = ?`, id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return emit(api.Event{Kind: "project.updated", ProjectId: &id, Actor: actor, Payload: ToPayload(p)})
|
||||
})
|
||||
if err != nil {
|
||||
return api.Project{}, err
|
||||
}
|
||||
return s.GetProject(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Store) GetProject(ctx context.Context, id string) (api.Project, error) {
|
||||
p, err := scanProject(s.db.QueryRowContext(ctx, `SELECT `+projectColumns+` FROM projects WHERE id = ?`, id))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@@ -724,7 +768,7 @@ func (s *Store) FinishedMergeAfterReviews(ctx context.Context) ([]string, error)
|
||||
return ids, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) TakeMergeAfterReview(ctx context.Context, id, reviewID string) (MergeAfterReview, error) {
|
||||
func (s *Store) TakeMergeAfterReview(ctx context.Context, id string, reviewIDs []string) (MergeAfterReview, error) {
|
||||
mar := MergeAfterReview{}
|
||||
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
projectID, target := "", ""
|
||||
@@ -737,7 +781,7 @@ func (s *Store) TakeMergeAfterReview(ctx context.Context, id, reviewID string) (
|
||||
return err
|
||||
}
|
||||
mar.Target = api.MergeTaskJSONBodyTarget(target)
|
||||
if mar.By == "" || (reviewID != "" && mar.ReviewID != reviewID) {
|
||||
if mar.By == "" || (reviewIDs != nil && !slices.Contains(reviewIDs, mar.ReviewID)) {
|
||||
mar = MergeAfterReview{}
|
||||
return nil
|
||||
}
|
||||
@@ -1638,7 +1682,7 @@ func (s *Store) ClearReviewRequests(ctx context.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
const projectColumns = `id, name, repo_path, default_branch, remote, created_by, created_at`
|
||||
const projectColumns = `id, name, repo_path, default_branch, remote, created_by, created_at, review_workflow`
|
||||
|
||||
const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name, workflow_run, workflow_stage, parent_task_id, parent_merge_by, merge_after_review_by != '', state, merging_by != '' OR finalizing != '', discard_state != '' AND status = 'active', (SELECT coalesce(max(seq), 0) FROM events WHERE events.task_id = tasks.id AND kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1)`
|
||||
|
||||
@@ -1651,7 +1695,7 @@ type scanner interface {
|
||||
func scanProject(row scanner) (api.Project, error) {
|
||||
p := api.Project{}
|
||||
createdAt := ""
|
||||
err := row.Scan(&p.Id, &p.Name, &p.RepoPath, &p.DefaultBranch, &p.Remote, &p.CreatedBy, &createdAt)
|
||||
err := row.Scan(&p.Id, &p.Name, &p.RepoPath, &p.DefaultBranch, &p.Remote, &p.CreatedBy, &createdAt, &p.ReviewWorkflow)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ type FrozenWorkflow struct {
|
||||
}
|
||||
|
||||
func (s *Store) ListWorkflows(ctx context.Context) ([]api.WorkflowSummary, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT name, coalesce(json_extract(definition, '$.description'), '') FROM workflows ORDER BY name`)
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT name, coalesce(json_extract(definition, '$.description'), ''), coalesce(nullif(json_extract(definition, '$.kind'), ''), 'task') FROM workflows ORDER BY name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -27,7 +27,7 @@ func (s *Store) ListWorkflows(ctx context.Context) ([]api.WorkflowSummary, error
|
||||
workflows := []api.WorkflowSummary{}
|
||||
for rows.Next() {
|
||||
w := api.WorkflowSummary{}
|
||||
err = rows.Scan(&w.Name, &w.Description)
|
||||
err = rows.Scan(&w.Name, &w.Description, &w.Kind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user