// Package service provides business logic services. package service import ( "context" "time" "github.com/google/uuid" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/logging" "github.com/orchard9/rdev/internal/port" ) // OperationService provides business logic for tracking operations. // It wraps the repository with convenient methods for step-by-step tracking. type OperationService struct { repo port.OperationRepository } // NewOperationService creates a new operation service. func NewOperationService(repo port.OperationRepository) *OperationService { return &OperationService{ repo: repo, } } // StartOperation creates a new operation and returns its ID. // The operation starts in "running" status. func (s *OperationService) StartOperation( ctx context.Context, projectID string, opType domain.OperationType, input map[string]any, requestID string, ) (string, error) { op := &domain.Operation{ ID: uuid.New().String(), ProjectID: projectID, Type: opType, Status: domain.OperationStatusRunning, RequestID: requestID, StartedAt: time.Now(), Input: input, Steps: []domain.OperationStep{}, } log := logging.FromContext(ctx).WithService("operation") if err := s.repo.Create(ctx, op); err != nil { log.Error("failed to create operation", logging.FieldError, err, logging.FieldProjectID, projectID, "type", opType, ) return "", err } log.Info("operation started", "operation_id", op.ID, logging.FieldProjectID, projectID, "type", opType, ) return op.ID, nil } // StartStep adds a new step to an operation and marks it as running. func (s *OperationService) StartStep(ctx context.Context, operationID, stepName string) error { step := domain.OperationStep{ Name: stepName, Status: domain.OperationStatusRunning, StartedAt: time.Now(), } log := logging.FromContext(ctx).WithService("operation") if err := s.repo.AddStep(ctx, operationID, step); err != nil { log.Error("failed to start step", logging.FieldError, err, "operation_id", operationID, "step", stepName, ) return err } return nil } // CompleteStep marks a step as completed with optional output. func (s *OperationService) CompleteStep( ctx context.Context, operationID, stepName string, startedAt time.Time, output map[string]any, ) error { step := domain.OperationStep{ Name: stepName, Status: domain.OperationStatusCompleted, StartedAt: startedAt, DurationMs: time.Since(startedAt).Milliseconds(), Output: output, } log := logging.FromContext(ctx).WithService("operation") if err := s.repo.UpdateStep(ctx, operationID, step); err != nil { log.Error("failed to complete step", logging.FieldError, err, "operation_id", operationID, "step", stepName, ) return err } return nil } // FailStep marks a step as failed with error details. func (s *OperationService) FailStep( ctx context.Context, operationID, stepName string, startedAt time.Time, errMsg, errDetail string, ) error { step := domain.OperationStep{ Name: stepName, Status: domain.OperationStatusFailed, StartedAt: startedAt, DurationMs: time.Since(startedAt).Milliseconds(), Error: errMsg, ErrorDetail: domain.TruncateErrorDetail(errDetail), } log := logging.FromContext(ctx).WithService("operation") if err := s.repo.UpdateStep(ctx, operationID, step); err != nil { log.Error("failed to fail step", logging.FieldError, err, "operation_id", operationID, "step", stepName, ) return err } return nil } // CompleteOperation marks the operation as completed with optional output. func (s *OperationService) CompleteOperation( ctx context.Context, operationID string, output map[string]any, ) error { log := logging.FromContext(ctx).WithService("operation") if err := s.repo.Complete(ctx, operationID, domain.OperationStatusCompleted, output, "", ""); err != nil { log.Error("failed to complete operation", logging.FieldError, err, "operation_id", operationID, ) return err } log.Info("operation completed", "operation_id", operationID, ) return nil } // FailOperation marks the operation as failed with error details. func (s *OperationService) FailOperation( ctx context.Context, operationID string, errMsg, errDetail string, ) error { log := logging.FromContext(ctx).WithService("operation") if err := s.repo.Complete(ctx, operationID, domain.OperationStatusFailed, nil, errMsg, errDetail); err != nil { log.Error("failed to fail operation", logging.FieldError, err, "operation_id", operationID, ) return err } log.Info("operation failed", "operation_id", operationID, logging.FieldError, errMsg, ) return nil } // SetCommitSHA updates the commit SHA for an operation. // Called after a git commit is created as part of the operation. func (s *OperationService) SetCommitSHA(ctx context.Context, operationID, sha string) error { return s.repo.SetCommitSHA(ctx, operationID, sha) } // SetExternalRef updates the external reference for an operation. // Called when linking to external systems like Woodpecker builds. func (s *OperationService) SetExternalRef(ctx context.Context, operationID, ref string) error { op, err := s.repo.Get(ctx, operationID) if err != nil { return err } op.ExternalRef = ref return s.repo.Update(ctx, op) } // FindByCommit finds the operation that created a specific commit. // Used to link builds to the operation that triggered them. func (s *OperationService) FindByCommit(ctx context.Context, projectID, sha string) (*domain.Operation, error) { return s.repo.GetByCommitSHA(ctx, projectID, sha) } // Get retrieves an operation by ID. func (s *OperationService) Get(ctx context.Context, operationID string) (*domain.Operation, error) { return s.repo.Get(ctx, operationID) } // List returns operations matching the filter criteria. func (s *OperationService) List(ctx context.Context, filter domain.OperationFilters) ([]*domain.Operation, error) { return s.repo.List(ctx, filter) } // LinkToParent sets the triggered_by field to link to a parent operation. func (s *OperationService) LinkToParent(ctx context.Context, operationID, parentID string) error { return s.repo.SetTriggeredBy(ctx, operationID, parentID) }