package worker import ( "bytes" "context" "encoding/json" "fmt" "os/exec" "strings" "time" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/logging" ) // SDLCTaskExecutor handles WorkTaskTypeSDLC tasks by executing sdlc CLI commands // inside worker pods via kubectl exec. This enables SDLC operations on skeleton // projects that don't have dedicated pods. type SDLCTaskExecutor struct { podGitOps *PodGitOperations namespace string } // SDLCTaskExecutorConfig holds configuration for the SDLC task executor. type SDLCTaskExecutorConfig struct { // Namespace is the Kubernetes namespace for kubectl exec. Namespace string // PodGitOps provides git clone/commit/push operations. PodGitOps *PodGitOperations } // NewSDLCTaskExecutor creates a new SDLC task executor. func NewSDLCTaskExecutor(cfg SDLCTaskExecutorConfig) *SDLCTaskExecutor { return &SDLCTaskExecutor{ podGitOps: cfg.PodGitOps, namespace: cfg.Namespace, } } // Execute runs an SDLC task by cloning the repo, executing the sdlc CLI command, // and optionally committing/pushing changes. func (e *SDLCTaskExecutor) Execute(ctx context.Context, task *domain.WorkTask) *domain.BuildResult { start := time.Now() log := logging.FromContext(ctx).WithWorker("sdlc-task-executor") 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(), } } // Use the first available claudebox worker pod podName := "claudebox-0" // Working directory in the pod workDir := "/workspace" log.Info("executing SDLC task", "task_id", task.ID, logging.FieldProjectID, task.ProjectID, "command", spec.Command, logging.FieldPodName, podName, ) // 1. Clone repo to worker pod if e.podGitOps == nil { return &domain.BuildResult{ Success: false, Error: "pod git operations not configured", DurationMs: time.Since(start).Milliseconds(), } } cloneResult := e.podGitOps.CloneRepo(ctx, podName, workDir, spec.GitCloneURL) if cloneResult.Error != nil { return &domain.BuildResult{ Success: false, Error: fmt.Sprintf("git clone failed: %v", cloneResult.Error), DurationMs: time.Since(start).Milliseconds(), } } // 2. Ensure .sdlc/ is initialized (auto-init for skeleton projects) if err := e.ensureSDLCInit(ctx, podName, workDir); err != nil { log.Warn("sdlc init check failed, continuing anyway", "task_id", task.ID, logging.FieldError, err, ) } // 3. Run SDLC CLI command output, err := e.runSDLCCommand(ctx, podName, workDir, spec.Command, spec.Args) if err != nil { return &domain.BuildResult{ Success: false, Error: fmt.Sprintf("sdlc command failed: %v", err), Output: output, DurationMs: time.Since(start).Milliseconds(), } } result := &domain.BuildResult{ Success: true, Output: output, DurationMs: time.Since(start).Milliseconds(), } // 4. Commit and push if enabled if spec.AutoCommit { commitMsg := fmt.Sprintf("sdlc: %s", spec.Command) gitResult := e.podGitOps.CommitAndPush(ctx, podName, workDir, commitMsg, spec.AutoPush) if gitResult.Error != nil { result.Success = false result.Error = fmt.Sprintf("git operations failed: %v", gitResult.Error) return result } if gitResult.HasChanges { result.CommitSHA = gitResult.CommitSHA result.FilesChanged = gitResult.FilesChanged log.Info("SDLC changes committed", "task_id", task.ID, "commit", gitResult.CommitSHA, "files", len(gitResult.FilesChanged), "pushed", gitResult.Pushed, ) } } log.Info("SDLC task completed", "task_id", task.ID, "command", spec.Command, logging.FieldDuration, result.DurationMs, ) return result } // ensureSDLCInit checks if .sdlc/ exists and runs `sdlc init` if it doesn't. // This enables SDLC operations on skeleton projects that don't have .sdlc/ pre-initialized. func (e *SDLCTaskExecutor) ensureSDLCInit(ctx context.Context, podName, workDir string) error { log := logging.FromContext(ctx).WithWorker("sdlc-task-executor") // Check if .sdlc/ directory exists checkArgs := []string{ "exec", "-n", e.namespace, podName, "--", "sh", "-c", fmt.Sprintf("test -d %s/.sdlc && echo exists || echo missing", workDir), } cmd := exec.CommandContext(ctx, "kubectl", checkArgs...) var stdout bytes.Buffer cmd.Stdout = &stdout if err := cmd.Run(); err != nil { return fmt.Errorf("check .sdlc failed: %w", err) } if strings.TrimSpace(stdout.String()) == "exists" { return nil // Already initialized } // Run sdlc init log.Info("initializing .sdlc directory", logging.FieldPodName, podName, "workDir", workDir) initArgs := []string{ "exec", "-n", e.namespace, podName, "--", "sh", "-c", fmt.Sprintf("cd %s && sdlc init --json", workDir), } initCmd := exec.CommandContext(ctx, "kubectl", initArgs...) var initStdout, initStderr bytes.Buffer initCmd.Stdout = &initStdout initCmd.Stderr = &initStderr if err := initCmd.Run(); err != nil { return fmt.Errorf("sdlc init failed: %w: %s", err, initStderr.String()) } log.Info("sdlc initialized", logging.FieldPodName, podName, "output", initStdout.String()) return nil } // runSDLCCommand executes the sdlc CLI command in the worker pod. func (e *SDLCTaskExecutor) runSDLCCommand(ctx context.Context, podName, workDir, command string, args []string) (string, error) { // Build the full command: sdlc {command} {args...} --json sdlcArgs := []string{command} sdlcArgs = append(sdlcArgs, args...) sdlcArgs = append(sdlcArgs, "--json") // Build kubectl exec command kubectlArgs := []string{ "exec", "-n", e.namespace, podName, "--", "sh", "-c", fmt.Sprintf("cd %s && sdlc %s", workDir, strings.Join(sdlcArgs, " ")), } cmd := exec.CommandContext(ctx, "kubectl", kubectlArgs...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return stdout.String(), fmt.Errorf("%s: %s", err, stderr.String()) } return stdout.String(), nil } // parsedSDLCSpec holds typed fields extracted from the task spec map. type parsedSDLCSpec struct { Command string Args []string GitCloneURL string AutoCommit bool AutoPush bool } // parseSpec extracts typed SDLCTaskSpec fields from the generic map[string]any. func (e *SDLCTaskExecutor) parseSpec(spec map[string]any) (*parsedSDLCSpec, 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 &parsedSDLCSpec{ Command: command, Args: args, GitCloneURL: gitCloneURL, AutoCommit: autoCommit, AutoPush: autoPush, }, nil } // SDLCResult represents the parsed JSON output from an SDLC command. // Used by WorkerSDLCExecutor to parse results. type SDLCResult struct { Success bool `json:"success"` Data json.RawMessage `json:"data,omitempty"` Error string `json:"error,omitempty"` }