Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
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>
200 lines
5.2 KiB
Go
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
|
|
}
|