rdev/internal/service/build_service.go
jordan d69da6d627 feat: add structured logging infrastructure and SDLC extensions
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>
2026-02-04 22:56:04 -07:00

203 lines
5.2 KiB
Go

// Package service provides business logic services.
package service
import (
"context"
"fmt"
"time"
"github.com/orchard9/rdev/internal/domain"
"github.com/orchard9/rdev/internal/logging"
"github.com/orchard9/rdev/internal/port"
)
// BuildService orchestrates build task submission and tracking.
// It coordinates between the work queue (execution) and build audit (history).
type BuildService struct {
queue port.WorkQueue
audit port.BuildAudit
}
// NewBuildService creates a new build service.
func NewBuildService(
queue port.WorkQueue,
audit port.BuildAudit,
) *BuildService {
return &BuildService{
queue: queue,
audit: audit,
}
}
// StartBuild enqueues a build task and creates an audit entry.
// Returns the task ID for status tracking.
func (s *BuildService) StartBuild(ctx context.Context, projectID string, spec domain.BuildSpec) (string, error) {
if err := spec.Validate(); err != nil {
return "", err
}
if projectID == "" {
return "", fmt.Errorf("project_id is required")
}
// Build work task spec from build spec
taskSpec := map[string]any{
"prompt": spec.Prompt,
"auto_commit": spec.AutoCommit,
"auto_push": spec.AutoPush,
}
if spec.Template != "" {
taskSpec["template"] = spec.Template
}
if len(spec.Variables) > 0 {
taskSpec["variables"] = spec.Variables
}
if spec.GitCloneURL != "" {
taskSpec["git_clone_url"] = spec.GitCloneURL
}
// Create work task
task := &domain.WorkTask{
ProjectID: projectID,
Type: domain.WorkTaskTypeBuild,
Spec: taskSpec,
CallbackURL: spec.CallbackURL,
MaxRetries: 3,
}
// Enqueue to work queue
taskID, err := s.queue.Enqueue(ctx, task)
if err != nil {
return "", fmt.Errorf("enqueue build task: %w", err)
}
// Create audit entry (non-critical - don't fail the build if audit fails)
auditEntry := &domain.BuildAuditEntry{
TaskID: taskID,
ProjectID: projectID,
Spec: spec,
Status: domain.BuildStatusPending,
StartedAt: time.Now(),
}
log := logging.FromContext(ctx).WithService("build")
if err := s.audit.Record(ctx, auditEntry); err != nil {
log.Warn("failed to record audit entry",
"task_id", taskID,
logging.FieldError, err.Error(),
)
}
log.Info("build enqueued",
"task_id", taskID,
logging.FieldProjectID, projectID,
"template", spec.Template,
"auto_push", spec.AutoPush,
)
return taskID, nil
}
// GetBuildStatus returns the current status of a build.
func (s *BuildService) GetBuildStatus(ctx context.Context, taskID string) (*domain.BuildAuditEntry, error) {
return s.audit.Get(ctx, taskID)
}
// ListBuilds returns build history for a project.
func (s *BuildService) ListBuilds(ctx context.Context, projectID string, limit int) ([]*domain.BuildAuditEntry, error) {
if limit <= 0 {
limit = 50
}
return s.audit.List(ctx, port.BuildAuditFilter{
ProjectID: projectID,
Limit: limit,
})
}
// CompleteBuild updates the audit entry when a build finishes.
// Called by the work queue processor on task completion.
func (s *BuildService) CompleteBuild(ctx context.Context, taskID string, result *domain.BuildResult) error {
if err := s.audit.Update(ctx, taskID, result); err != nil {
return fmt.Errorf("update audit: %w", err)
}
log := logging.FromContext(ctx).WithService("build")
log.Info("build completed",
"task_id", taskID,
"success", result.Success,
logging.FieldDuration, result.DurationMs,
)
return nil
}
// StartBuildWithSDLCContext enqueues a build task with SDLC context for callback routing.
// The SDLC context is included in the task spec and will be passed through to the callback.
func (s *BuildService) StartBuildWithSDLCContext(ctx context.Context, projectID string, spec domain.BuildSpec, sdlcCtx map[string]any) (string, error) {
if err := spec.Validate(); err != nil {
return "", err
}
if projectID == "" {
return "", fmt.Errorf("project_id is required")
}
// Build work task spec from build spec
taskSpec := map[string]any{
"prompt": spec.Prompt,
"auto_commit": spec.AutoCommit,
"auto_push": spec.AutoPush,
}
if spec.Template != "" {
taskSpec["template"] = spec.Template
}
if len(spec.Variables) > 0 {
taskSpec["variables"] = spec.Variables
}
if spec.GitCloneURL != "" {
taskSpec["git_clone_url"] = spec.GitCloneURL
}
// Add SDLC context for callback routing
if sdlcCtx != nil {
taskSpec["sdlc_context"] = sdlcCtx
}
// Create work task
task := &domain.WorkTask{
ProjectID: projectID,
Type: domain.WorkTaskTypeBuild,
Spec: taskSpec,
CallbackURL: spec.CallbackURL,
MaxRetries: 3,
}
// Enqueue to work queue
taskID, err := s.queue.Enqueue(ctx, task)
if err != nil {
return "", fmt.Errorf("enqueue build task: %w", err)
}
// Create audit entry (non-critical - don't fail the build if audit fails)
auditEntry := &domain.BuildAuditEntry{
TaskID: taskID,
ProjectID: projectID,
Spec: spec,
Status: domain.BuildStatusPending,
StartedAt: time.Now(),
}
log := logging.FromContext(ctx).WithService("build")
if err := s.audit.Record(ctx, auditEntry); err != nil {
log.Warn("failed to record audit entry",
"task_id", taskID,
logging.FieldError, err.Error(),
)
}
log.Info("build enqueued with SDLC context",
"task_id", taskID,
logging.FieldProjectID, projectID,
"sdlc_context", sdlcCtx,
)
return taskID, nil
}