summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/executor/container.go9
-rw-r--r--internal/executor/container_test.go87
-rw-r--r--internal/executor/executor.go13
-rw-r--r--internal/executor/executor_test.go3
-rw-r--r--internal/task/task.go1
5 files changed, 110 insertions, 3 deletions
diff --git a/internal/executor/container.go b/internal/executor/container.go
index d9ed8ef..d270e20 100644
--- a/internal/executor/container.go
+++ b/internal/executor/container.go
@@ -101,8 +101,13 @@ func (r *ContainerRunner) Run(ctx context.Context, t *task.Task, e *storage.Exec
if err := os.Remove(workspace); err != nil {
return fmt.Errorf("removing workspace before clone: %w", err)
}
- r.Logger.Info("cloning repository", "url", repoURL, "workspace", workspace)
- if out, err := r.command(ctx, "git", "clone", repoURL, workspace).CombinedOutput(); err != nil {
+ r.Logger.Info("cloning repository", "url", repoURL, "workspace", workspace, "branch", t.BranchName)
+ cloneArgs := []string{"clone"}
+ if t.BranchName != "" {
+ cloneArgs = append(cloneArgs, "--branch", t.BranchName)
+ }
+ cloneArgs = append(cloneArgs, repoURL, workspace)
+ if out, err := r.command(ctx, "git", cloneArgs...).CombinedOutput(); err != nil {
return fmt.Errorf("git clone failed: %w\n%s", err, string(out))
}
if err = os.Chmod(workspace, 0755); err != nil {
diff --git a/internal/executor/container_test.go b/internal/executor/container_test.go
index b6946ef..c56d1b2 100644
--- a/internal/executor/container_test.go
+++ b/internal/executor/container_test.go
@@ -514,3 +514,90 @@ func TestContainerRunner_AuthError_SyncsAndRetries(t *testing.T) {
t.Error("expected sync-credentials to be called, but marker file not found")
}
}
+
+func TestContainerRunner_ClonesStoryBranch(t *testing.T) {
+ logger := slog.New(slog.NewTextHandler(io.Discard, nil))
+
+ var cloneArgs []string
+ runner := &ContainerRunner{
+ Logger: logger,
+ Image: "busybox",
+ Command: func(ctx context.Context, name string, arg ...string) *exec.Cmd {
+ if name == "git" && len(arg) > 0 && arg[0] == "clone" {
+ cloneArgs = append([]string{}, arg...)
+ dir := arg[len(arg)-1]
+ os.MkdirAll(dir, 0755)
+ return exec.Command("true")
+ }
+ // docker run fails so the test exits quickly
+ if name == "docker" {
+ return exec.Command("sh", "-c", "exit 1")
+ }
+ return exec.Command("true")
+ },
+ }
+
+ tk := &task.Task{
+ ID: "story-branch-test",
+ RepositoryURL: "https://example.com/repo.git",
+ BranchName: "story/my-feature",
+ Agent: task.AgentConfig{Type: "claude"},
+ }
+ e := &storage.Execution{ID: "exec-1", TaskID: "story-branch-test"}
+
+ runner.Run(context.Background(), tk, e)
+ os.RemoveAll(e.SandboxDir)
+
+ // Assert git clone was called with --branch <branchName>
+ if len(cloneArgs) < 3 {
+ t.Fatalf("expected clone args, got %v", cloneArgs)
+ }
+ found := false
+ for i, a := range cloneArgs {
+ if a == "--branch" && i+1 < len(cloneArgs) && cloneArgs[i+1] == "story/my-feature" {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf("expected git clone --branch story/my-feature, got args: %v", cloneArgs)
+ }
+}
+
+func TestContainerRunner_ClonesDefaultBranchWhenNoBranchName(t *testing.T) {
+ logger := slog.New(slog.NewTextHandler(io.Discard, nil))
+
+ var cloneArgs []string
+ runner := &ContainerRunner{
+ Logger: logger,
+ Image: "busybox",
+ Command: func(ctx context.Context, name string, arg ...string) *exec.Cmd {
+ if name == "git" && len(arg) > 0 && arg[0] == "clone" {
+ cloneArgs = append([]string{}, arg...)
+ dir := arg[len(arg)-1]
+ os.MkdirAll(dir, 0755)
+ return exec.Command("true")
+ }
+ if name == "docker" {
+ return exec.Command("sh", "-c", "exit 1")
+ }
+ return exec.Command("true")
+ },
+ }
+
+ tk := &task.Task{
+ ID: "no-branch-test",
+ RepositoryURL: "https://example.com/repo.git",
+ Agent: task.AgentConfig{Type: "claude"},
+ }
+ e := &storage.Execution{ID: "exec-2", TaskID: "no-branch-test"}
+
+ runner.Run(context.Background(), tk, e)
+ os.RemoveAll(e.SandboxDir)
+
+ for _, a := range cloneArgs {
+ if a == "--branch" {
+ t.Errorf("expected no --branch flag for task without BranchName, got args: %v", cloneArgs)
+ }
+ }
+}
diff --git a/internal/executor/executor.go b/internal/executor/executor.go
index 440294c..6489060 100644
--- a/internal/executor/executor.go
+++ b/internal/executor/executor.go
@@ -33,6 +33,7 @@ type Store interface {
UpdateExecutionChangestats(execID string, stats *task.Changestats) error
RecordAgentEvent(e storage.AgentEvent) error
GetProject(id string) (*task.Project, error)
+ GetStory(id string) (*task.Story, error)
}
// LogPather is an optional interface runners can implement to provide the log
@@ -275,6 +276,12 @@ func (p *Pool) executeResume(ctx context.Context, t *task.Task, exec *storage.Ex
t.RepositoryURL = proj.RemoteURL
}
}
+ // Populate BranchName from Story if missing (ADR-007).
+ if t.BranchName == "" && t.StoryID != "" {
+ if story, err := p.store.GetStory(t.StoryID); err == nil && story.BranchName != "" {
+ t.BranchName = story.BranchName
+ }
+ }
err = runner.Run(ctx, t, exec)
exec.EndTime = time.Now().UTC()
@@ -715,6 +722,12 @@ func (p *Pool) execute(ctx context.Context, t *task.Task) {
t.RepositoryURL = proj.RemoteURL
}
}
+ // Populate BranchName from Story if missing (ADR-007).
+ if t.BranchName == "" && t.StoryID != "" {
+ if story, err := p.store.GetStory(t.StoryID); err == nil && story.BranchName != "" {
+ t.BranchName = story.BranchName
+ }
+ }
// Run the task.
err = runner.Run(ctx, t, exec)
diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go
index 1f4e92f..2e01230 100644
--- a/internal/executor/executor_test.go
+++ b/internal/executor/executor_test.go
@@ -1057,7 +1057,8 @@ func (m *minimalMockStore) UpdateExecutionChangestats(execID string, stats *task
return nil
}
func (m *minimalMockStore) RecordAgentEvent(_ storage.AgentEvent) error { return nil }
-func (m *minimalMockStore) GetProject(_ string) (*task.Project, error) { return nil, nil }
+func (m *minimalMockStore) GetProject(_ string) (*task.Project, error) { return nil, nil }
+func (m *minimalMockStore) GetStory(_ string) (*task.Story, error) { return nil, nil }
func (m *minimalMockStore) lastStateUpdate() (string, task.State, bool) {
m.mu.Lock()
diff --git a/internal/task/task.go b/internal/task/task.go
index ee79668..ba87360 100644
--- a/internal/task/task.go
+++ b/internal/task/task.go
@@ -82,6 +82,7 @@ type Task struct {
Tags []string `yaml:"tags" json:"tags"`
DependsOn []string `yaml:"depends_on" json:"depends_on"`
StoryID string `yaml:"-" json:"story_id,omitempty"`
+ BranchName string `yaml:"-" json:"branch_name,omitempty"`
State State `yaml:"-" json:"state"`
RejectionComment string `yaml:"-" json:"rejection_comment,omitempty"`
QuestionJSON string `yaml:"-" json:"question,omitempty"`