Major changes: - Add internal/logging package with field constants, context propagation, sensitive data auto-redaction, and per-component log levels - Add worker timeout constants (TimeoutQuickOp, TimeoutHealthCheck, etc.) - Extend SDLC with callback handlers, generate endpoints, and executor - Add new cookbook trees for aeries and slackpath progression - Add skeleton templates for queue, realtime, and microservices - Add worker component template with async job processing - Refactor services and handlers to use new logging infrastructure - Split component.go into component_infra.go and component_listing.go Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
274 lines
7.3 KiB
Go
274 lines
7.3 KiB
Go
package worker
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
)
|
|
|
|
// 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
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// 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
|
|
|
|
Logger *slog.Logger
|
|
}
|
|
|
|
// NewSDLCTaskExecutor creates a new SDLC task executor.
|
|
func NewSDLCTaskExecutor(cfg SDLCTaskExecutorConfig) *SDLCTaskExecutor {
|
|
logger := cfg.Logger
|
|
if logger == nil {
|
|
logger = slog.Default()
|
|
}
|
|
return &SDLCTaskExecutor{
|
|
podGitOps: cfg.PodGitOps,
|
|
namespace: cfg.Namespace,
|
|
logger: logger.With("component", "sdlc-task-executor"),
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
|
|
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"
|
|
|
|
e.logger.Info("executing SDLC task",
|
|
"task_id", task.ID,
|
|
"project_id", task.ProjectID,
|
|
"command", spec.Command,
|
|
"pod", 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 {
|
|
e.logger.Warn("sdlc init check failed, continuing anyway",
|
|
"task_id", task.ID,
|
|
"error", 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
|
|
e.logger.Info("SDLC changes committed",
|
|
"task_id", task.ID,
|
|
"commit", gitResult.CommitSHA,
|
|
"files", len(gitResult.FilesChanged),
|
|
"pushed", gitResult.Pushed,
|
|
)
|
|
}
|
|
}
|
|
|
|
e.logger.Info("SDLC task completed",
|
|
"task_id", task.ID,
|
|
"command", spec.Command,
|
|
"duration_ms", 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 {
|
|
// 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
|
|
e.logger.Info("initializing .sdlc directory", "pod", 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())
|
|
}
|
|
|
|
e.logger.Info("sdlc initialized", "pod", 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"`
|
|
}
|