package worker import ( "context" "fmt" "time" claudeboxclient "github.com/orchard9/rdev/internal/adapter/claudebox" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/logging" ) // HTTPSDLCTaskExecutor handles WorkTaskTypeSDLC tasks using HTTP calls to the // local claudebox sidecar instead of kubectl exec. type HTTPSDLCTaskExecutor struct { client *claudeboxclient.Client workDir string } // HTTPSDLCTaskExecutorConfig holds configuration for the HTTP SDLC executor. type HTTPSDLCTaskExecutorConfig struct { // ClaudeboxClient is the HTTP client for the claudebox sidecar. ClaudeboxClient *claudeboxclient.Client // WorkDir is the default working directory in the container. WorkDir string } // NewHTTPSDLCTaskExecutor creates a new HTTP-based SDLC executor. func NewHTTPSDLCTaskExecutor(cfg HTTPSDLCTaskExecutorConfig) *HTTPSDLCTaskExecutor { if cfg.WorkDir == "" { cfg.WorkDir = "/workspace" } return &HTTPSDLCTaskExecutor{ client: cfg.ClaudeboxClient, workDir: cfg.WorkDir, } } // Execute runs an SDLC task using the claudebox sidecar HTTP API. func (e *HTTPSDLCTaskExecutor) Execute(ctx context.Context, task *domain.WorkTask) *domain.BuildResult { start := time.Now() log := logging.FromContext(ctx).WithWorker("http-sdlc-executor") // Parse SDLC spec spec, err := e.parseSpec(task.Spec) if err != nil { return &domain.BuildResult{ Success: false, Error: fmt.Sprintf("invalid SDLC spec: %v", err), DurationMs: time.Since(start).Milliseconds(), } } log.Info("executing SDLC task via HTTP", "task_id", task.ID, logging.FieldProjectID, task.ProjectID, "command", spec.Command, ) // Clone repo to workspace cloneResp, err := e.client.GitClone(ctx, spec.GitCloneURL, e.workDir) if err != nil { return &domain.BuildResult{ Success: false, Error: fmt.Sprintf("git clone failed: %v", err), DurationMs: time.Since(start).Milliseconds(), } } if !cloneResp.Success { return &domain.BuildResult{ Success: false, Error: fmt.Sprintf("git clone failed: %s", cloneResp.Error), DurationMs: time.Since(start).Milliseconds(), } } // Reset workspace to main for clean state. // Worker pods may be left on a feature branch from a previous task. resetResp, resetErr := e.client.GitResetToMain(ctx, e.workDir) if resetErr != nil { log.Warn("failed to reset workspace to main, continuing", "task_id", task.ID, logging.FieldError, resetErr, ) } else if !resetResp.Success { log.Warn("reset to main returned failure, continuing", "task_id", task.ID, "error", resetResp.Error, ) } // Run SDLC command sdlcResp, err := e.client.RunSDLC(ctx, spec.Command, spec.Args, e.workDir) if err != nil { return &domain.BuildResult{ Success: false, Error: fmt.Sprintf("sdlc command failed: %v", err), DurationMs: time.Since(start).Milliseconds(), } } if !sdlcResp.Success { return &domain.BuildResult{ Success: false, Error: fmt.Sprintf("sdlc command failed: %s", sdlcResp.Error), Output: sdlcResp.Output, DurationMs: time.Since(start).Milliseconds(), } } result := &domain.BuildResult{ Success: true, Output: sdlcResp.Output, DurationMs: time.Since(start).Milliseconds(), } // Commit and push if enabled if spec.AutoCommit { commitMsg := fmt.Sprintf("sdlc: %s", spec.Command) gitResp, err := e.client.GitCommitAndPush(ctx, commitMsg, spec.AutoPush, e.workDir) if err != nil { result.Success = false result.Error = fmt.Sprintf("git operations failed: %v", err) return result } if !gitResp.Success { result.Success = false result.Error = fmt.Sprintf("git operations failed: %s", gitResp.Error) return result } if gitResp.HasChanges { result.CommitSHA = gitResp.CommitSHA result.FilesChanged = gitResp.FilesChanged log.Info("SDLC changes committed", "task_id", task.ID, "commit", gitResp.CommitSHA, "files", len(gitResp.FilesChanged), "pushed", gitResp.Pushed, ) } } log.Info("SDLC task completed", "task_id", task.ID, "command", spec.Command, logging.FieldDuration, result.DurationMs, ) return result } // httpSDLCSpec holds typed fields extracted from the task spec map. type httpSDLCSpec struct { Command string Args []string GitCloneURL string AutoCommit bool AutoPush bool } // parseSpec extracts typed SDLCTaskSpec fields from the generic map. func (e *HTTPSDLCTaskExecutor) parseSpec(spec map[string]any) (*httpSDLCSpec, error) { command, _ := spec["command"].(string) if command == "" { return nil, fmt.Errorf("command is required") } gitCloneURL, _ := spec["git_clone_url"].(string) if gitCloneURL == "" { return nil, fmt.Errorf("git_clone_url is required") } autoCommit, _ := spec["auto_commit"].(bool) autoPush, _ := spec["auto_push"].(bool) // Parse args (can be []string or []any from JSON) var args []string if argsRaw, ok := spec["args"]; ok { switch v := argsRaw.(type) { case []string: args = v case []any: for _, a := range v { if s, ok := a.(string); ok { args = append(args, s) } } } } return &httpSDLCSpec{ Command: command, Args: args, GitCloneURL: gitCloneURL, AutoCommit: autoCommit, AutoPush: autoPush, }, nil }