Files
ai-workflow/inbox/internal/app/workspaceprovision/service.go
T

153 lines
4.8 KiB
Go

package workspaceprovision
import (
"context"
"database/sql"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"inbox/internal/app/workspaceruntime"
"inbox/internal/base/slug"
"inbox/internal/domain/workspace"
)
type store interface {
GetProject(context.Context, string) (workspace.Project, error)
GetProjectByRootPath(context.Context, string) (workspace.Project, error)
GetProjectBySlug(context.Context, string) (workspace.Project, error)
CreateProject(context.Context, workspace.Project) (workspace.Project, error)
UpdateProjectDefaultBranch(context.Context, string, string) error
GetWorkspace(context.Context, string) (workspace.Workspace, error)
GetWorkspaceByProjectAndSlug(context.Context, string, string) (workspace.Workspace, error)
CreateWorkspace(context.Context, workspace.Workspace) (workspace.Workspace, error)
UpdateWorkspace(context.Context, workspace.Workspace) error
}
type Service struct {
store store
workspacesDir string
runtime runtimeManager
}
type ProvisionRequest struct {
ProjectDir string
Name string
}
type runtimeManager interface {
EnsureRepository(ctx context.Context, projectDir string) (string, error)
Ensure(ctx context.Context, workspaceID string) (workspace.Workspace, workspaceruntime.Runtime, error)
}
func NewService(store store, workspacesDir string, runtime runtimeManager) *Service {
return &Service{
store: store,
workspacesDir: strings.TrimSpace(workspacesDir),
runtime: runtime,
}
}
func (s *Service) Provision(ctx context.Context, req ProvisionRequest) (workspace.Workspace, workspaceruntime.Runtime, error) {
if s.runtime == nil {
return workspace.Workspace{}, workspaceruntime.Runtime{}, fmt.Errorf("workspace runtime is not configured")
}
projectDir, err := filepath.Abs(strings.TrimSpace(req.ProjectDir))
if err != nil {
return workspace.Workspace{}, workspaceruntime.Runtime{}, fmt.Errorf("resolve project dir: %w", err)
}
info, err := os.Stat(projectDir)
if err != nil {
return workspace.Workspace{}, workspaceruntime.Runtime{}, fmt.Errorf("stat project dir: %w", err)
}
if !info.IsDir() {
return workspace.Workspace{}, workspaceruntime.Runtime{}, fmt.Errorf("project dir is not a directory: %s", projectDir)
}
projectName := filepath.Base(projectDir)
projectSlug := slug.Normalize(projectName)
if projectSlug == "" {
projectSlug = "project"
}
workspaceSlug := slug.Normalize(firstNonEmpty(req.Name, projectName))
if workspaceSlug == "" {
workspaceSlug = projectSlug
}
baseBranch, err := s.runtime.EnsureRepository(ctx, projectDir)
if err != nil {
return workspace.Workspace{}, workspaceruntime.Runtime{}, err
}
project, err := s.ensureProject(ctx, projectDir, projectName, projectSlug, baseBranch)
if err != nil {
return workspace.Workspace{}, workspaceruntime.Runtime{}, err
}
ws, err := s.store.GetWorkspaceByProjectAndSlug(ctx, project.ID, workspaceSlug)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return workspace.Workspace{}, workspaceruntime.Runtime{}, err
}
ws = workspace.NormalizeWorkspaceForCreate(workspace.Workspace{
ProjectID: project.ID,
Slug: workspaceSlug,
Name: workspaceSlug,
RootPath: filepath.Join(s.workspacesDir, workspaceSlug),
BaseBranch: baseBranch,
})
ws, err = s.store.CreateWorkspace(ctx, ws)
if err != nil {
return workspace.Workspace{}, workspaceruntime.Runtime{}, err
}
}
ws = workspace.ApplyManagedRuntimeConfig(ws, s.workspacesDir, baseBranch)
if err := s.store.UpdateWorkspace(ctx, ws); err != nil {
return workspace.Workspace{}, workspaceruntime.Runtime{}, err
}
return s.runtime.Ensure(ctx, ws.ID)
}
func (s *Service) ensureProject(ctx context.Context, projectDir, projectName, projectSlug, baseBranch string) (workspace.Project, error) {
project, err := s.store.GetProjectByRootPath(ctx, projectDir)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return workspace.Project{}, err
}
project = workspace.NormalizeProjectForCreate(workspace.Project{
Slug: projectSlug,
Name: projectName,
RootPath: projectDir,
DefaultBranch: baseBranch,
})
created, createErr := s.store.CreateProject(ctx, project)
if createErr == nil {
return created, nil
}
if existing, lookupErr := s.store.GetProjectBySlug(ctx, projectSlug); lookupErr == nil && existing.RootPath == projectDir {
project = existing
} else {
return workspace.Project{}, createErr
}
}
if project.DefaultBranch != baseBranch {
if err := s.store.UpdateProjectDefaultBranch(ctx, project.ID, baseBranch); err != nil {
return workspace.Project{}, err
}
project.DefaultBranch = baseBranch
}
return project, nil
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}