Add expose_port previews with a Preview tab; leave setup to AGENTS.md
This commit is contained in:
@@ -61,7 +61,8 @@ func (m *Manager) bounceProseQuestion(l *live) claude.HookFunc {
|
||||
}
|
||||
|
||||
func (m *Manager) taskGuidance(ctx context.Context, task api.Task) (string, error) {
|
||||
g := guidance + fmt.Sprintf("\n- This task's ports are %d to %d; $PORT holds the first. Run dev servers and anything else that listens on these, so parallel tasks don't collide.",
|
||||
g := guidance + fmt.Sprintf("\n- This task's ports are %d to %d; $PORT holds the first. Run dev servers and anything else that listens on these, so parallel tasks don't collide, and listen on 127.0.0.1. To show the human a running server, call mcp__byre__expose_port."+
|
||||
"\n- This worktree starts with only what git tracks. Before other work in a new worktree, do any setup the project's AGENTS.md describes.",
|
||||
task.PortBase, task.PortBase+store.PortBlock-1)
|
||||
if task.TodoItem != nil {
|
||||
g += fmt.Sprintf("\n- This task was started from the item %q in %s at the repo root. When the work is done, remove that item from %s in this worktree, so the removal lands in the same merge as the work.",
|
||||
|
||||
@@ -297,15 +297,15 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
||||
|
||||
tool("show",
|
||||
"Point the human's right pane at a task, or at a project with no task: tabs are the tabs to put first, in order (the rest follow), and tab is the one to select. "+
|
||||
"Tabs: transcript, changes, files, terminal (a shell in the task's worktree), inbox (the task's open items), project (the project's dashboard: its active tasks and its TODO.md items); with only project_id, only project shows anything. "+
|
||||
"Tabs: transcript, changes, files, terminal (a shell in the task's worktree), preview (the dev servers the task's agent exposed with expose_port), inbox (the task's open items), project (the project's dashboard: its active tasks and its TODO.md items); with only project_id, only project shows anything. "+
|
||||
"Omit both task_id and project_id to clear the pane. "+
|
||||
"If the pane already shows that task (or project) on that tab, only the tab order changes and you are told so, even while the human is working in the pane. "+
|
||||
"Otherwise refused while the human is working in the right pane, unless it is on the project tab or what it shows is finished (a merged or discarded task, or an inbox tab with nothing left).",
|
||||
`{"type": "object", "properties": {
|
||||
"task_id": {"type": "string"},
|
||||
"project_id": {"type": "string", "description": "Ignored when task_id is given."},
|
||||
"tabs": {"type": "array", "items": {"type": "string", "enum": ["transcript", "changes", "files", "terminal", "inbox", "project"]}},
|
||||
"tab": {"type": "string", "enum": ["transcript", "changes", "files", "terminal", "inbox", "project"]}
|
||||
"tabs": {"type": "array", "items": {"type": "string", "enum": ["transcript", "changes", "files", "terminal", "preview", "inbox", "project"]}},
|
||||
"tab": {"type": "string", "enum": ["transcript", "changes", "files", "terminal", "preview", "inbox", "project"]}
|
||||
}, "required": ["tabs", "tab"]}`,
|
||||
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
|
||||
f := api.Focus{}
|
||||
|
||||
+16
-25
@@ -62,10 +62,6 @@ type Manager struct {
|
||||
snapMu sync.Mutex
|
||||
snaps map[string]*snapState
|
||||
|
||||
setupMu sync.Mutex
|
||||
setups map[string]bool
|
||||
setupFailures map[string]string
|
||||
|
||||
mergeMu sync.Mutex
|
||||
mergeHolders map[string]string
|
||||
|
||||
@@ -102,21 +98,19 @@ type live struct {
|
||||
|
||||
func New(st *store.Store, limits Limits, worktreeRoot, leadRoot string) *Manager {
|
||||
m := &Manager{
|
||||
store: st,
|
||||
limits: limits,
|
||||
worktreeRoot: worktreeRoot,
|
||||
leadRoot: leadRoot,
|
||||
live: map[string]*live{},
|
||||
rateLimits: map[api.AgentSessionVendor]map[string]any{},
|
||||
leadAnswered: map[string]bool{},
|
||||
paneFocused: map[string]bool{},
|
||||
signals: make(chan api.Event, 1024),
|
||||
snaps: map[string]*snapState{},
|
||||
setups: map[string]bool{},
|
||||
setupFailures: map[string]string{},
|
||||
mergeHolders: map[string]string{},
|
||||
bad: newBadLines(),
|
||||
watchStop: make(chan struct{}),
|
||||
store: st,
|
||||
limits: limits,
|
||||
worktreeRoot: worktreeRoot,
|
||||
leadRoot: leadRoot,
|
||||
live: map[string]*live{},
|
||||
rateLimits: map[api.AgentSessionVendor]map[string]any{},
|
||||
leadAnswered: map[string]bool{},
|
||||
paneFocused: map[string]bool{},
|
||||
signals: make(chan api.Event, 1024),
|
||||
snaps: map[string]*snapState{},
|
||||
mergeHolders: map[string]string{},
|
||||
bad: newBadLines(),
|
||||
watchStop: make(chan struct{}),
|
||||
}
|
||||
go m.signalLoop()
|
||||
return m
|
||||
@@ -422,7 +416,7 @@ func (m *Manager) turn(ctx context.Context, sess api.AgentSession, task api.Task
|
||||
return api.AgentSession{}, err
|
||||
}
|
||||
|
||||
if room && !m.settingUp(task.Id) {
|
||||
if room {
|
||||
return m.start(ctx, sess, task, actor, text, images, api.AgentSessionStateIdle)
|
||||
}
|
||||
|
||||
@@ -516,9 +510,6 @@ func (m *Manager) drain(ctx context.Context) error {
|
||||
}
|
||||
|
||||
for _, q := range queued {
|
||||
if m.settingUp(q.TaskID) {
|
||||
continue
|
||||
}
|
||||
room, err := m.hasRoom(ctx, q.Vendor)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -669,9 +660,9 @@ func (m *Manager) ensureLive(ctx context.Context, sess api.AgentSession, task ap
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tools := []mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.createTaskTool(l), m.setMergeTitleTool(l)}
|
||||
tools := []mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.createTaskTool(l), m.setMergeTitleTool(l), m.exposePortTool(l)}
|
||||
hooks := map[string][]claude.HookMatcher{
|
||||
"UserPromptSubmit": {{Func: m.remindTaskGuidance(l, g)}},
|
||||
"UserPromptSubmit": {{Func: remindGuidance(g)}},
|
||||
"Stop": {{Func: m.bounceProseQuestion(l)}},
|
||||
"PostToolUse": {{Func: m.afterTool(l)}},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
"g.fc.run/misc/byre/internal/mcp"
|
||||
"g.fc.run/misc/byre/internal/store"
|
||||
"g.fc.run/misc/byre/internal/tailscale"
|
||||
)
|
||||
|
||||
const maxPreviewName = 40
|
||||
|
||||
func portEnv(t api.Task) []string {
|
||||
return []string{"PORT=" + strconv.Itoa(t.PortBase)}
|
||||
}
|
||||
|
||||
type exposePortArgs struct {
|
||||
Name string `json:"name"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
func (m *Manager) exposePortTool(l *live) mcp.Tool {
|
||||
return mcp.Tool{
|
||||
Name: "expose_port",
|
||||
Description: "Show the human a server you are running, such as a dev server, in the task's Preview tab. " +
|
||||
"Start the server yourself first (in the background), listening on 127.0.0.1 (not 0.0.0.0 or localhost) on a port in this task's block. " +
|
||||
"byre then serves it over HTTPS on the same port of byre's host through tailscale serve, and returns the URL. " +
|
||||
"name labels it in the tab (\"web\", \"storybook\"); exposing a name or port again replaces the earlier one. " +
|
||||
"The server stops when your process does (a byre restart, compaction, the task closing); the human sees it as unreachable until you start it again.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Short label, shown in the Preview tab."},
|
||||
"port": {"type": "integer", "description": "The port the server listens on, at 127.0.0.1, within this task's block."}
|
||||
},
|
||||
"required": ["name", "port"]
|
||||
}`),
|
||||
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
return m.exposePort(ctx, l, raw)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) exposePort(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
|
||||
args := exposePortArgs{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("malformed expose_port call: %w", err)
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(args.Name)
|
||||
if name == "" || strings.ContainsAny(name, "\r\n") || utf8.RuneCountInString(name) > maxPreviewName {
|
||||
return "", fmt.Errorf("name must be one non-empty line of at most %d characters", maxPreviewName)
|
||||
}
|
||||
|
||||
task, err := m.store.GetTask(ctx, l.taskID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
last := task.PortBase + store.PortBlock - 1
|
||||
if args.Port < task.PortBase || args.Port > last {
|
||||
return "", fmt.Errorf("port %d is outside this task's block, %d to %d", args.Port, task.PortBase, last)
|
||||
}
|
||||
|
||||
addr := net.JoinHostPort("127.0.0.1", strconv.Itoa(args.Port))
|
||||
conn, err := net.DialTimeout("tcp", addr, time.Second)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("nothing is listening on %s; start the server listening on 127.0.0.1 first: %w", addr, err)
|
||||
}
|
||||
conn.Close()
|
||||
|
||||
err = tailscale.Serve(ctx, args.Port)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dropped, err := m.store.SetPreview(ctx, api.Preview{TaskId: task.Id, Name: name, Port: args.Port, CreatedAt: time.Now().UTC()}, agentActor)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return "", errors.New("this task is no longer active")
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, port := range dropped {
|
||||
err = tailscale.Off(ctx, port)
|
||||
if err != nil {
|
||||
slog.Error("stop serving replaced preview", "task", task.Id, "port", port, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Exposed %q: https://<byre's host>:%d/ proxies to %s, and the human sees it in the task's Preview tab.", name, args.Port, addr), nil
|
||||
}
|
||||
|
||||
func (m *Manager) Unexpose(ctx context.Context, taskID string) {
|
||||
previews, err := m.store.ListPreviews(ctx, taskID)
|
||||
if err != nil {
|
||||
slog.Error("list previews", "task", taskID, "err", err)
|
||||
return
|
||||
}
|
||||
for _, p := range previews {
|
||||
err = tailscale.Off(ctx, p.Port)
|
||||
if err != nil {
|
||||
slog.Error("stop serving preview", "task", taskID, "port", p.Port, "err", err)
|
||||
continue
|
||||
}
|
||||
slog.Info("stopped serving preview", "task", taskID, "name", p.Name, "port", p.Port)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
"g.fc.run/misc/byre/internal/store"
|
||||
)
|
||||
|
||||
func TestExposePortRefusals(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
st, err := store.Open(filepath.Join(t.TempDir(), "byre.db"), store.DefaultPortFirst, func(api.Event) {})
|
||||
if err != nil {
|
||||
t.Fatalf("open store: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { st.Close() })
|
||||
|
||||
now := time.Now().UTC()
|
||||
_, err = st.EnsureUser(ctx, "alice", "Alice")
|
||||
if err != nil {
|
||||
t.Fatalf("ensure user: %v", err)
|
||||
}
|
||||
err = st.CreateProject(ctx, api.Project{Id: "p1", Name: "demo", RepoPath: "/nonexistent", DefaultBranch: "main", Remote: "none", CreatedBy: "alice", CreatedAt: now})
|
||||
if err != nil {
|
||||
t.Fatalf("create project: %v", err)
|
||||
}
|
||||
err = st.CreateTask(ctx, api.Task{Id: "t1", ProjectId: "p1", Title: "t1", Branch: "b", WorktreePath: "/nonexistent", BaseRef: "x", Status: api.TaskStatusActive, CreatedBy: "alice", CreatedAt: now}, "alice")
|
||||
if err != nil {
|
||||
t.Fatalf("create task: %v", err)
|
||||
}
|
||||
|
||||
m := New(st, DefaultLimits, t.TempDir(), t.TempDir())
|
||||
l := &live{id: "s1", vendor: api.Claude, taskID: "t1", projectID: "p1"}
|
||||
call := func(name string, port int) error {
|
||||
raw, err := json.Marshal(exposePortArgs{Name: name, Port: port})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
_, err = m.exposePort(ctx, l, raw)
|
||||
return err
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
port int
|
||||
want string
|
||||
}{
|
||||
{"", 41000, "name must be"},
|
||||
{"two\nlines", 41000, "name must be"},
|
||||
{strings.Repeat("x", maxPreviewName+1), 41000, "name must be"},
|
||||
{"web", 40999, "outside this task's block, 41000 to 41009"},
|
||||
{"web", 41010, "outside this task's block, 41000 to 41009"},
|
||||
{"web", 41009, "nothing is listening on 127.0.0.1:41009"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
err = call(c.name, c.port)
|
||||
if err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Fatalf("expose_port(%q, %d) = %v, want an error containing %q", c.name, c.port, err, c.want)
|
||||
}
|
||||
}
|
||||
|
||||
previews, err := st.ListPreviews(ctx, "t1")
|
||||
if err != nil || len(previews) != 0 {
|
||||
t.Fatalf("previews after refusals = %v, %v", previews, err)
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
"g.fc.run/misc/byre/internal/claude"
|
||||
)
|
||||
|
||||
const (
|
||||
setupScript = "script/setup"
|
||||
setupTimeout = 15 * time.Minute
|
||||
setupOutputBytes = 64 << 10
|
||||
)
|
||||
|
||||
func portEnv(t api.Task) []string {
|
||||
return []string{"PORT=" + strconv.Itoa(t.PortBase)}
|
||||
}
|
||||
|
||||
func (m *Manager) startSetup(t api.Task) {
|
||||
path := filepath.Join(t.WorktreePath, setupScript)
|
||||
_, err := os.Stat(path)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return
|
||||
}
|
||||
|
||||
m.setupMu.Lock()
|
||||
m.setups[t.Id] = true
|
||||
m.setupMu.Unlock()
|
||||
|
||||
go m.runSetup(t, path)
|
||||
}
|
||||
|
||||
func (m *Manager) settingUp(taskID string) bool {
|
||||
m.setupMu.Lock()
|
||||
defer m.setupMu.Unlock()
|
||||
return m.setups[taskID]
|
||||
}
|
||||
|
||||
func (m *Manager) takeSetupFailure(taskID string) string {
|
||||
m.setupMu.Lock()
|
||||
defer m.setupMu.Unlock()
|
||||
msg := m.setupFailures[taskID]
|
||||
delete(m.setupFailures, taskID)
|
||||
return msg
|
||||
}
|
||||
|
||||
func (m *Manager) runSetup(t api.Task, path string) {
|
||||
ctx := context.Background()
|
||||
m.emitTask(ctx, t, "setup.started", map[string]any{"script": setupScript, "port_base": t.PortBase})
|
||||
|
||||
start := time.Now()
|
||||
cctx, cancel := context.WithTimeout(ctx, setupTimeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(cctx, path)
|
||||
cmd.Dir = t.WorktreePath
|
||||
cmd.Env = append(os.Environ(), portEnv(t)...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if cctx.Err() != nil {
|
||||
err = fmt.Errorf("timed out after %s", setupTimeout)
|
||||
}
|
||||
|
||||
output := string(out)
|
||||
if len(out) > setupOutputBytes {
|
||||
output = "…" + string(out[len(out)-setupOutputBytes:])
|
||||
}
|
||||
payload := map[string]any{"script": setupScript, "output": output, "duration_ms": time.Since(start).Milliseconds()}
|
||||
if err != nil {
|
||||
payload["error"] = err.Error()
|
||||
}
|
||||
slog.Info("setup ended", "task", t.Id, "err", err, "took", time.Since(start))
|
||||
m.emitTask(ctx, t, "setup.ended", payload)
|
||||
|
||||
m.setupMu.Lock()
|
||||
delete(m.setups, t.Id)
|
||||
if err != nil {
|
||||
m.setupFailures[t.Id] = fmt.Sprintf("byre ran %s in this worktree when the task was created, and it failed (%s). Its output:\n\n```\n%s\n```\n\nThe worktree may be missing dependencies or env files the setup would have provided.", setupScript, err, output)
|
||||
}
|
||||
m.setupMu.Unlock()
|
||||
|
||||
err = m.drain(ctx)
|
||||
if err != nil {
|
||||
slog.Error("start queued turns", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) emitTask(ctx context.Context, t api.Task, kind string, payload map[string]any) {
|
||||
err := m.store.AppendEvent(ctx, api.Event{Kind: kind, ProjectId: &t.ProjectId, TaskId: &t.Id, Actor: "byre", Payload: payload})
|
||||
if err != nil {
|
||||
slog.Error("record setup event", "task", t.Id, "kind", kind, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) remindTaskGuidance(l *live, text string) claude.HookFunc {
|
||||
return func(ctx context.Context, input json.RawMessage) (map[string]any, error) {
|
||||
failure := m.takeSetupFailure(l.taskID)
|
||||
if failure == "" {
|
||||
return remindGuidance(text)(ctx, input)
|
||||
}
|
||||
m.fileError(l, failure)
|
||||
return remindGuidance(text+"\n\n"+failure)(ctx, input)
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package agents_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
)
|
||||
|
||||
func (e *env) commitSetup(script string) {
|
||||
e.t.Helper()
|
||||
err := os.MkdirAll(filepath.Join(e.repo, "script"), 0o755)
|
||||
if err != nil {
|
||||
e.t.Fatal(err)
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(e.repo, "script", "setup"), []byte(script), 0o755)
|
||||
if err != nil {
|
||||
e.t.Fatal(err)
|
||||
}
|
||||
gitOut(e.t, e.repo, "add", "script/setup")
|
||||
gitOut(e.t, e.repo, "commit", "--quiet", "--message", "setup")
|
||||
}
|
||||
|
||||
func (e *env) waitSetup(taskID string) api.Event {
|
||||
e.t.Helper()
|
||||
seen := e.waitFor("setup.ended", func(ev api.Event) bool {
|
||||
return ev.Kind == "setup.ended" && ev.TaskId != nil && *ev.TaskId == taskID
|
||||
})
|
||||
return seen[len(seen)-1]
|
||||
}
|
||||
|
||||
func TestSetupScriptAndPorts(t *testing.T) {
|
||||
e := newEnv(t)
|
||||
e.commitSetup("#!/bin/sh\nset -e\ncp \"$(git rev-parse --path-format=absolute --git-common-dir)/../.env\" .env\necho \"port $PORT\"\n")
|
||||
err := os.WriteFile(filepath.Join(e.repo, ".env"), []byte("SECRET=1\n"), 0o644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := e.manager()
|
||||
|
||||
task, err := m.CreateTask(e.ctx, e.project, "with setup", login, "main", login)
|
||||
if err != nil {
|
||||
t.Fatalf("create task: %v", err)
|
||||
}
|
||||
if task.PortBase != 41010 {
|
||||
t.Fatalf("port_base = %d, want 41010 (41000 is taken by the first task)", task.PortBase)
|
||||
}
|
||||
|
||||
ev := e.waitSetup(task.Id)
|
||||
if ev.Payload["error"] != nil {
|
||||
t.Fatalf("setup failed: %v", ev.Payload)
|
||||
}
|
||||
want := "port 41010"
|
||||
if out, _ := ev.Payload["output"].(string); !strings.Contains(out, want) {
|
||||
t.Fatalf("output %q lacks %q", out, want)
|
||||
}
|
||||
env, err := os.ReadFile(filepath.Join(task.WorktreePath, ".env"))
|
||||
if err != nil || string(env) != "SECRET=1\n" {
|
||||
t.Fatalf("worktree .env = %q, %v", env, err)
|
||||
}
|
||||
|
||||
_, err = e.store.DiscardTask(e.ctx, e.task.Id, login)
|
||||
if err != nil {
|
||||
t.Fatalf("discard: %v", err)
|
||||
}
|
||||
reused, err := m.CreateTask(e.ctx, e.project, "reuses ports", login, "main", login)
|
||||
if err != nil {
|
||||
t.Fatalf("create task: %v", err)
|
||||
}
|
||||
if reused.PortBase != 41000 {
|
||||
t.Fatalf("port_base = %d, want the freed 41000", reused.PortBase)
|
||||
}
|
||||
e.waitSetup(reused.Id)
|
||||
}
|
||||
|
||||
func TestSetupFailure(t *testing.T) {
|
||||
e := newEnv(t)
|
||||
e.commitSetup("#!/bin/sh\necho installing\nexit 3\n")
|
||||
m := e.manager()
|
||||
|
||||
task, err := m.CreateTask(e.ctx, e.project, "failing setup", login, "main", login)
|
||||
if err != nil {
|
||||
t.Fatalf("create task: %v", err)
|
||||
}
|
||||
ev := e.waitSetup(task.Id)
|
||||
if ev.Payload["error"] != "exit status 3" || ev.Payload["output"] != "installing\n" {
|
||||
t.Fatalf("setup.ended payload = %v", ev.Payload)
|
||||
}
|
||||
}
|
||||
@@ -85,12 +85,7 @@ func (m *Manager) createTask(ctx context.Context, project api.Project, t api.Tas
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
}
|
||||
t, err = m.store.GetTask(ctx, t.Id)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
}
|
||||
m.startSetup(t)
|
||||
return t, nil
|
||||
return m.store.GetTask(ctx, t.Id)
|
||||
}
|
||||
|
||||
type createTaskArgs struct {
|
||||
|
||||
Reference in New Issue
Block a user