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>
203 lines
5.2 KiB
Go
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
|
|
}
|