Implements horizontally-scalable worker pool architecture: - claudebox-sidecar: HTTP server for Claude Code, git, and SDLC ops - rdev-worker: standalone worker binary polling rdev-api for tasks - HTTP client adapter for sidecar communication - HPA with custom Prometheus metrics for autoscaling - ServiceMonitor for metrics scraping Code review fixes applied: - URL-encode query parameters in GitStatus (Critical #1) - Remove unused shellQuote function (Critical #2) - Use stdlib strings.Split/TrimSpace (Critical #3) - Add version injection via ldflags (Warning #4) - Add debug logging for swallowed git/sdlc errors (Warning #5, #6) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
185 lines
4.7 KiB
Go
185 lines
4.7 KiB
Go
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(),
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|