rdev/internal/service/build_service.go
jordan c59d348040 chore: prepare for composable monorepo template implementation
This commit captures the current state before implementing the composable
monorepo template system. Key changes included:

Infrastructure:
- Add CockroachDB provisioner adapter for database provisioning
- Add Redis provisioner adapter for cache provisioning
- Add build events system with PostgreSQL storage
- Add WebSocket endpoint for real-time build progress

Code agent improvements:
- Fix Claude Code adapter to use default allowed tools instead of dangerously-skip-permissions
- Add context-aware stream closing for cancellation support
- Improve parser tests for edge cases

Build system:
- Add build event constants and metrics
- Remove deprecated git_operations.go (replaced by pod_git_operations.go)
- Add rollback logic for multi-step provisioning operations

Documentation:
- Add composable-monorepo feature documentation
- Add DNS/Cloudflare service documentation
- Update deployment and troubleshooting guides

Cookbooks:
- Add fullstack-app cookbook
- Refactor landing-test with shared library

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:39:28 -07:00

136 lines
3.3 KiB
Go

// Package service provides business logic services.
package service
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/orchard9/rdev/internal/domain"
"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
logger *slog.Logger
}
// NewBuildService creates a new build service.
func NewBuildService(
queue port.WorkQueue,
audit port.BuildAudit,
logger *slog.Logger,
) *BuildService {
if logger == nil {
logger = slog.Default()
}
return &BuildService{
queue: queue,
audit: audit,
logger: logger.With("service", "build"),
}
}
// 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(),
}
if err := s.audit.Record(ctx, auditEntry); err != nil {
s.logger.Warn("failed to record audit entry",
"task_id", taskID,
"error", err,
)
}
s.logger.Info("build enqueued",
"task_id", taskID,
"project_id", 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)
}
s.logger.Info("build completed",
"task_id", taskID,
"success", result.Success,
"duration_ms", result.DurationMs,
)
return nil
}