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>
414 lines
14 KiB
Go
414 lines
14 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
"github.com/orchard9/rdev/internal/logging"
|
|
"github.com/orchard9/rdev/internal/port"
|
|
"github.com/orchard9/rdev/internal/sdlc"
|
|
)
|
|
|
|
// SDLCService provides SDLC operations for projects.
|
|
// It supports dual execution paths:
|
|
// 1. Pod executor: kubectl exec into project pods (for projects with dedicated pods)
|
|
// 2. Worker executor: enqueue tasks to worker pool (for skeleton/monorepo projects)
|
|
type SDLCService struct {
|
|
podExecutor port.SDLCExecutor // kubectl exec path (projects with pods)
|
|
workerExecutor port.SDLCExecutor // worker pool path (skeleton projects)
|
|
projectRepo port.ProjectRepository
|
|
db *sql.DB // For checking project existence in database
|
|
}
|
|
|
|
// NewSDLCService creates a new SDLC service with a single executor.
|
|
// For dual executor support, use NewSDLCServiceWithWorker.
|
|
func NewSDLCService(sdlcExec port.SDLCExecutor, projectRepo port.ProjectRepository) *SDLCService {
|
|
return &SDLCService{
|
|
podExecutor: sdlcExec,
|
|
projectRepo: projectRepo,
|
|
}
|
|
}
|
|
|
|
// NewSDLCServiceWithWorker creates an SDLC service with both pod and worker executors.
|
|
// The service automatically routes to the appropriate executor based on project pod availability.
|
|
func NewSDLCServiceWithWorker(
|
|
podExecutor port.SDLCExecutor,
|
|
workerExecutor port.SDLCExecutor,
|
|
projectRepo port.ProjectRepository,
|
|
db *sql.DB,
|
|
) *SDLCService {
|
|
return &SDLCService{
|
|
podExecutor: podExecutor,
|
|
workerExecutor: workerExecutor,
|
|
projectRepo: projectRepo,
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
// resolveExecutor determines which executor to use for a project.
|
|
// Returns (executor, target, error) where target is podName for pod executor
|
|
// or projectID for worker executor.
|
|
func (s *SDLCService) resolveExecutor(ctx context.Context, projectID string) (port.SDLCExecutor, string, error) {
|
|
// Try K8s repo first (projects with dedicated pods)
|
|
project, err := s.projectRepo.Get(ctx, domain.ProjectID(projectID))
|
|
if err == nil && project.PodName != "" {
|
|
return s.podExecutor, project.PodName, nil
|
|
}
|
|
|
|
// If no worker executor configured, fall back to the old behavior
|
|
if s.workerExecutor == nil {
|
|
if err != nil {
|
|
return nil, "", domain.ErrProjectNotFound
|
|
}
|
|
// Project exists but has no pod - this is the error case we're trying to fix
|
|
return nil, "", domain.ErrProjectNotFound
|
|
}
|
|
|
|
// Check if project exists in database (skeleton projects)
|
|
if s.db != nil {
|
|
log := logging.FromContext(ctx).WithService("sdlc")
|
|
var exists bool
|
|
dbErr := s.db.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM projects WHERE id = $1)`, projectID).Scan(&exists)
|
|
if dbErr != nil {
|
|
log.Warn("failed to check project existence in database",
|
|
logging.FieldProjectID, projectID,
|
|
logging.FieldError, dbErr,
|
|
)
|
|
// Fall through to project not found
|
|
} else if exists {
|
|
// Project exists in database but no pod - use worker executor
|
|
log.Info("using worker executor for skeleton project", logging.FieldProjectID, projectID)
|
|
return s.workerExecutor, projectID, nil
|
|
}
|
|
}
|
|
|
|
return nil, "", domain.ErrProjectNotFound
|
|
}
|
|
|
|
// GetState returns the global SDLC state for a project.
|
|
func (s *SDLCService) GetState(ctx context.Context, projectID string) (*sdlc.State, error) {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return exec.GetState(ctx, target)
|
|
}
|
|
|
|
// GetNext returns the classifier's recommendation for the next action.
|
|
func (s *SDLCService) GetNext(ctx context.Context, projectID, feature string) (*sdlc.Classification, error) {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return exec.GetNext(ctx, target, feature)
|
|
}
|
|
|
|
// ListFeatures returns all features in a project.
|
|
func (s *SDLCService) ListFeatures(ctx context.Context, projectID string) ([]*sdlc.Feature, error) {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return exec.ListFeatures(ctx, target)
|
|
}
|
|
|
|
// GetFeature returns a single feature by slug.
|
|
func (s *SDLCService) GetFeature(ctx context.Context, projectID, slug string) (*sdlc.Feature, error) {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return exec.GetFeature(ctx, target, slug)
|
|
}
|
|
|
|
// CreateFeature creates a new feature.
|
|
func (s *SDLCService) CreateFeature(ctx context.Context, projectID, slug, title string) (*sdlc.Feature, error) {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f, err := exec.CreateFeature(ctx, target, slug, title)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log := logging.FromContext(ctx).WithService("sdlc")
|
|
log.Info("feature created", logging.FieldProjectID, projectID, "feature", slug)
|
|
return f, nil
|
|
}
|
|
|
|
// TransitionFeature moves a feature to the specified phase.
|
|
func (s *SDLCService) TransitionFeature(ctx context.Context, projectID, slug string, phase sdlc.FeaturePhase) error {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log := logging.FromContext(ctx).WithService("sdlc")
|
|
if err := exec.TransitionFeature(ctx, target, slug, phase); err != nil {
|
|
log.Error("transition feature failed", logging.FieldProjectID, projectID, "feature", slug, "phase", string(phase), logging.FieldError, err)
|
|
return err
|
|
}
|
|
log.Info("feature transitioned", logging.FieldProjectID, projectID, "feature", slug, "phase", string(phase))
|
|
return nil
|
|
}
|
|
|
|
// BlockFeature adds a blocker reason to a feature.
|
|
func (s *SDLCService) BlockFeature(ctx context.Context, projectID, slug, reason string) error {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := exec.BlockFeature(ctx, target, slug, reason); err != nil {
|
|
return err
|
|
}
|
|
log := logging.FromContext(ctx).WithService("sdlc")
|
|
log.Info("feature blocked", logging.FieldProjectID, projectID, "feature", slug, "reason", reason)
|
|
return nil
|
|
}
|
|
|
|
// UnblockFeature removes all blockers from a feature.
|
|
func (s *SDLCService) UnblockFeature(ctx context.Context, projectID, slug string) error {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := exec.UnblockFeature(ctx, target, slug); err != nil {
|
|
return err
|
|
}
|
|
log := logging.FromContext(ctx).WithService("sdlc")
|
|
log.Info("feature unblocked", logging.FieldProjectID, projectID, "feature", slug)
|
|
return nil
|
|
}
|
|
|
|
// DeleteFeature removes a feature entirely.
|
|
func (s *SDLCService) DeleteFeature(ctx context.Context, projectID, slug string) error {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := exec.DeleteFeature(ctx, target, slug); err != nil {
|
|
return err
|
|
}
|
|
log := logging.FromContext(ctx).WithService("sdlc")
|
|
log.Info("feature deleted", logging.FieldProjectID, projectID, "feature", slug)
|
|
return nil
|
|
}
|
|
|
|
// GetArtifactStatus returns artifact statuses for a feature.
|
|
func (s *SDLCService) GetArtifactStatus(ctx context.Context, projectID, slug string) (map[sdlc.ArtifactType]*sdlc.Artifact, error) {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return exec.GetArtifactStatus(ctx, target, slug)
|
|
}
|
|
|
|
// ApproveArtifact approves a feature artifact.
|
|
func (s *SDLCService) ApproveArtifact(ctx context.Context, projectID, slug string, artType sdlc.ArtifactType) error {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := exec.ApproveArtifact(ctx, target, slug, artType); err != nil {
|
|
return err
|
|
}
|
|
log := logging.FromContext(ctx).WithService("sdlc")
|
|
log.Info("artifact approved", logging.FieldProjectID, projectID, "feature", slug, "artifact", string(artType))
|
|
return nil
|
|
}
|
|
|
|
// RejectArtifact rejects a feature artifact.
|
|
func (s *SDLCService) RejectArtifact(ctx context.Context, projectID, slug string, artType sdlc.ArtifactType) error {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := exec.RejectArtifact(ctx, target, slug, artType); err != nil {
|
|
return err
|
|
}
|
|
log := logging.FromContext(ctx).WithService("sdlc")
|
|
log.Info("artifact rejected", logging.FieldProjectID, projectID, "feature", slug, "artifact", string(artType))
|
|
return nil
|
|
}
|
|
|
|
// PassArtifact marks a feature artifact as passed.
|
|
func (s *SDLCService) PassArtifact(ctx context.Context, projectID, slug string, artType sdlc.ArtifactType) error {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := exec.PassArtifact(ctx, target, slug, artType); err != nil {
|
|
return err
|
|
}
|
|
log := logging.FromContext(ctx).WithService("sdlc")
|
|
log.Info("artifact passed", logging.FieldProjectID, projectID, "feature", slug, "artifact", string(artType))
|
|
return nil
|
|
}
|
|
|
|
// FailArtifact marks a feature artifact as failed.
|
|
func (s *SDLCService) FailArtifact(ctx context.Context, projectID, slug string, artType sdlc.ArtifactType) error {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := exec.FailArtifact(ctx, target, slug, artType); err != nil {
|
|
return err
|
|
}
|
|
log := logging.FromContext(ctx).WithService("sdlc")
|
|
log.Info("artifact failed", logging.FieldProjectID, projectID, "feature", slug, "artifact", string(artType))
|
|
return nil
|
|
}
|
|
|
|
// NeedsFixArtifact marks a feature artifact as needing fixes.
|
|
func (s *SDLCService) NeedsFixArtifact(ctx context.Context, projectID, slug string, artType sdlc.ArtifactType) error {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := exec.NeedsFixArtifact(ctx, target, slug, artType); err != nil {
|
|
return err
|
|
}
|
|
log := logging.FromContext(ctx).WithService("sdlc")
|
|
log.Info("artifact needs fix", logging.FieldProjectID, projectID, "feature", slug, "artifact", string(artType))
|
|
return nil
|
|
}
|
|
|
|
// ListTasks returns all tasks for a feature.
|
|
func (s *SDLCService) ListTasks(ctx context.Context, projectID, slug string) ([]sdlc.Task, error) {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return exec.ListTasks(ctx, target, slug)
|
|
}
|
|
|
|
// AddTask adds a new task to a feature.
|
|
func (s *SDLCService) AddTask(ctx context.Context, projectID, slug, title string) (*sdlc.Task, error) {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return exec.AddTask(ctx, target, slug, title)
|
|
}
|
|
|
|
// StartTask marks a task as in-progress.
|
|
func (s *SDLCService) StartTask(ctx context.Context, projectID, slug, taskID string) error {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return exec.StartTask(ctx, target, slug, taskID)
|
|
}
|
|
|
|
// CompleteTask marks a task as complete.
|
|
func (s *SDLCService) CompleteTask(ctx context.Context, projectID, slug, taskID string) error {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return exec.CompleteTask(ctx, target, slug, taskID)
|
|
}
|
|
|
|
// BlockTask marks a task as blocked.
|
|
func (s *SDLCService) BlockTask(ctx context.Context, projectID, slug, taskID string) error {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return exec.BlockTask(ctx, target, slug, taskID)
|
|
}
|
|
|
|
// QueryBlocked returns all blocked features in a project.
|
|
func (s *SDLCService) QueryBlocked(ctx context.Context, projectID string) ([]port.BlockedInfo, error) {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return exec.QueryBlocked(ctx, target)
|
|
}
|
|
|
|
// QueryReady returns features ready for work in a project.
|
|
func (s *SDLCService) QueryReady(ctx context.Context, projectID string) ([]port.ReadyInfo, error) {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return exec.QueryReady(ctx, target)
|
|
}
|
|
|
|
// QueryNeedsApproval returns features awaiting approval in a project.
|
|
func (s *SDLCService) QueryNeedsApproval(ctx context.Context, projectID string) ([]port.ApprovalInfo, error) {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return exec.QueryNeedsApproval(ctx, target)
|
|
}
|
|
|
|
// CreateBranch creates a feature branch and its manifest.
|
|
func (s *SDLCService) CreateBranch(ctx context.Context, projectID, slug string) (*sdlc.BranchManifest, error) {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m, err := exec.CreateBranch(ctx, target, slug)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log := logging.FromContext(ctx).WithService("sdlc")
|
|
log.Info("branch created", logging.FieldProjectID, projectID, "feature", slug, "branch", m.Name)
|
|
return m, nil
|
|
}
|
|
|
|
// GetBranchStatus returns the full branch status including checklist.
|
|
func (s *SDLCService) GetBranchStatus(ctx context.Context, projectID, slug string) (*port.BranchStatus, error) {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return exec.GetBranchStatus(ctx, target, slug)
|
|
}
|
|
|
|
// SyncBranch syncs a feature branch with its base branch.
|
|
func (s *SDLCService) SyncBranch(ctx context.Context, projectID, slug string) error {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := exec.SyncBranch(ctx, target, slug); err != nil {
|
|
return err
|
|
}
|
|
log := logging.FromContext(ctx).WithService("sdlc")
|
|
log.Info("branch synced", logging.FieldProjectID, projectID, "feature", slug)
|
|
return nil
|
|
}
|
|
|
|
// MergeFeature merges a feature branch after all gates pass.
|
|
func (s *SDLCService) MergeFeature(ctx context.Context, projectID, slug, strategy string) error {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log := logging.FromContext(ctx).WithService("sdlc")
|
|
if err := exec.MergeFeature(ctx, target, slug, strategy); err != nil {
|
|
log.Error("merge feature failed", logging.FieldProjectID, projectID, "feature", slug, logging.FieldError, err)
|
|
return err
|
|
}
|
|
log.Info("feature merged", logging.FieldProjectID, projectID, "feature", slug, "strategy", strategy)
|
|
return nil
|
|
}
|
|
|
|
// ArchiveFeature archives a released feature.
|
|
func (s *SDLCService) ArchiveFeature(ctx context.Context, projectID, slug string) error {
|
|
exec, target, err := s.resolveExecutor(ctx, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := exec.ArchiveFeature(ctx, target, slug); err != nil {
|
|
return err
|
|
}
|
|
log := logging.FromContext(ctx).WithService("sdlc")
|
|
log.Info("feature archived", logging.FieldProjectID, projectID, "feature", slug)
|
|
return nil
|
|
}
|