diff options
Diffstat (limited to 'internal/executor')
| -rw-r--r-- | internal/executor/container.go | 20 | ||||
| -rw-r--r-- | internal/executor/container_test.go | 77 |
2 files changed, 92 insertions, 5 deletions
diff --git a/internal/executor/container.go b/internal/executor/container.go index 3afce70..39f737a 100644 --- a/internal/executor/container.go +++ b/internal/executor/container.go @@ -155,6 +155,14 @@ func (r *ContainerRunner) Run(ctx context.Context, t *task.Task, e *storage.Exec } } } + // For non-story tasks (e.g. CI webhook tasks), also resolve local path from the task's + // project field. This lets the runner clone from the local mirror instead of an HTTPS + // remote that may require credentials not available on the host. + if storyLocalPath == "" && t.Project != "" && r.Store != nil { + if proj, err := r.Store.GetProject(t.Project); err == nil && proj != nil && proj.LocalPath != "" { + storyLocalPath = proj.LocalPath + } + } // Fall back to task-level BranchName (e.g. set explicitly by executor or tests). if storyBranch == "" { storyBranch = t.BranchName @@ -175,13 +183,15 @@ 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) - var cloneArgs []string + // Prefer the local path as the clone source to avoid HTTPS auth requirements. + // For story tasks the storyLocalPath is also available as a speed reference, + // but we clone from the local path directly since it is already authoritative. + cloneSrc := repoURL if storyLocalPath != "" { - cloneArgs = []string{"clone", "--reference", storyLocalPath, repoURL, workspace} - } else { - cloneArgs = []string{"clone", repoURL, workspace} + cloneSrc = storyLocalPath } + r.Logger.Info("cloning repository", "src", cloneSrc, "workspace", workspace) + cloneArgs := []string{"clone", cloneSrc, workspace} if out, err := r.command(ctx, "git", cloneArgs...).CombinedOutput(); err != nil { return fmt.Errorf("git clone failed: %w\n%s", err, string(out)) } diff --git a/internal/executor/container_test.go b/internal/executor/container_test.go index 26e67bc..dec666e 100644 --- a/internal/executor/container_test.go +++ b/internal/executor/container_test.go @@ -666,6 +666,73 @@ func TestContainerRunner_ClonesDefaultBranchWhenNoBranchName(t *testing.T) { } } +// TestContainerRunner_ClonesFromLocalPathForCITask verifies that when a task has +// an HTTPS repository_url but the project has a known local path, the runner +// clones from the local path to avoid HTTPS auth failures. +func TestContainerRunner_ClonesFromLocalPathForCITask(t *testing.T) { + dir := t.TempDir() + localRepo := filepath.Join(dir, "local-repo.git") + if out, err := exec.Command("git", "init", "--bare", localRepo).CombinedOutput(); err != nil { + t.Fatalf("git init bare: %v\n%s", err, out) + } + + store := testStore(t) + proj := &task.Project{ + ID: "nav", + Name: "nav", + RemoteURL: localRepo, + LocalPath: localRepo, + } + if err := store.CreateProject(proj); err != nil { + t.Fatalf("CreateProject: %v", err) + } + + var cloneSrc string + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + runner := &ContainerRunner{ + Logger: logger, + Image: "busybox", + Store: store, + Command: func(ctx context.Context, name string, arg ...string) *exec.Cmd { + if name == "git" && len(arg) > 0 && arg[0] == "clone" { + // Record what source was cloned from. + for i, a := range arg { + if a != "clone" && a != "--reference" && i > 0 && arg[i-1] != "--reference" { + // last non-dest arg before workspace is the source + } + _ = a + } + // source is second-to-last arg (before workspace dir) + cloneSrc = arg[len(arg)-2] + 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: "ci-task-1", + RepositoryURL: "https://github.com/thepeterstone/nav.git", + Project: "nav", + Agent: task.AgentConfig{Type: "claude"}, + } + e := &storage.Execution{ID: "exec-ci-1", TaskID: "ci-task-1"} + runner.Run(context.Background(), tk, e, noopChannel{}) + os.RemoveAll(e.SandboxDir) + + if cloneSrc == "https://github.com/thepeterstone/nav.git" { + t.Error("runner cloned from GitHub HTTPS URL; expected local path to avoid auth failure") + } + if cloneSrc != localRepo { + t.Errorf("expected clone source %q, got %q", localRepo, cloneSrc) + } +} + func TestEnsureStoryBranch_CreatesMissingBranch(t *testing.T) { // Set up a bare repo and a local clone to test branch creation. dir := t.TempDir() @@ -744,3 +811,13 @@ func TestEnsureStoryBranch_IdempotentIfExists(t *testing.T) { t.Fatalf("ensureStoryBranch on existing branch: %v", err) } } + +// noopChannel satisfies AgentChannel with no-op implementations for tests. +type noopChannel struct{} + +func (noopChannel) AskUser(_ context.Context, _ string) (string, error) { return "", nil } +func (noopChannel) ReportSummary(_ context.Context, _ string) error { return nil } +func (noopChannel) SpawnSubtask(_ context.Context, _ SubtaskSpec) (string, error) { + return "", nil +} +func (noopChannel) RecordProgress(_ context.Context, _ string) error { return nil } |
