rdev/internal/service/build_service.go
jordan bc47e426b0 feat: Add CI pipeline proxy, DNS alias management, and worker executor system
- Add ListPipelines/GetPipeline to CIProvider port with Woodpecker adapter
- Add DNS alias endpoints: GET/POST/DELETE /projects/{id}/domains
- Implement worker executor daemon, build executor, and git operations
- Add build service, worker service, and build audit tracking
- Add worker registry with PostgreSQL adapter and migration
- Add multi-provider code agent interface (Claude Code + OpenCode)
- Add create-and-build combo endpoint
- Update landing-page cookbook to reflect all gaps closed
- Fix tech debt: unified validation, auth scopes, error wrapping, slog patterns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 21:05:28 -07:00

133 lines
3.2 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
}
// 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
}