rdev/internal/worker/http_sdlc_executor.go
jordan b6e778d5ab
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
fix(git): harden git flow for concurrent SDLC stress test failures
5 fixes from stress test analysis:

1. CRITICAL: Add pull-before-push to claudebox GitOperations.CommitAndPush,
   matching the fix already in PodGitOperations (prevents push rejections
   when concurrent builds advance the remote).

2. HIGH: Extract ResetToMain into PodGitOperations as a shared public method.
   Wire into BuildExecutor after CloneRepo and update SDLCTaskExecutor to
   use the shared method. Prevents builds from running on wrong branch when
   worker pods are reused across tasks.

3. HIGH: Make branch create push failure fatal with retry+rollback in
   cmd/sdlc/cmd_branch.go. Prevents orphaned .sdlc/ state that causes
   merge failures after completing all 10 SDLC phases.

4. MEDIUM: Shell-escape token in credential helpers (both PodGitOperations
   and claudebox GitOperations) to prevent shell injection via tokens
   containing special characters.

5. MEDIUM: Add GitResetToMain to claudebox sidecar (git.go implementation,
   server.go endpoint, client.go HTTP method) and wire into
   HTTPSDLCTaskExecutor for the HTTP sidecar path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 20:57:27 -07:00

200 lines
5.2 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(),
}
}
// 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
}