Add review workflows, tree-shaped workflows and a workflow editor
This commit is contained in:
+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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user