- projectsync: checkout, config 모듈 개선 및 테스트 추가 - workitempipeline: service 개선 및 테스트 추가 - http handlers 테스트 업데이트 - client: proto_socket_branch_event_service.dart 추가 및 테스트 - contracts notes 업데이트 - agent-task: milestone 하위 subtask PLAN/CODE_REVIEW 문서 추가
139 lines
5.2 KiB
Go
139 lines
5.2 KiB
Go
package workitempipeline
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
|
|
"github.com/nomadcode/nomadcode-core/internal/projectsync"
|
|
"github.com/nomadcode/nomadcode-core/internal/storage"
|
|
"github.com/nomadcode/nomadcode-core/internal/workflow"
|
|
"github.com/nomadcode/nomadcode-core/internal/workitem"
|
|
)
|
|
|
|
// ErrProjectSyncNotConfigured is returned when the pipeline has no project sync
|
|
// binder wired. Plane-origin creation must resolve git/workspace bindings and
|
|
// reserve a workspace slot from project sync state, so an unconfigured binder
|
|
// fails explicitly instead of creating a task with an unbound workspace.
|
|
var ErrProjectSyncNotConfigured = errors.New("project sync resolver is not configured")
|
|
|
|
type CreateTaskInput struct {
|
|
Ref workitem.Ref
|
|
StateID string
|
|
Comment string
|
|
}
|
|
|
|
type TaskCreator interface {
|
|
CreateTask(ctx context.Context, input workflow.CreateTaskInput) (storage.Task, error)
|
|
}
|
|
|
|
// ProjectBinding is the resolved project sync binding for a work item: the
|
|
// persisted setting ID plus its normalized config. The pipeline reserves a
|
|
// workspace slot against SettingID and reads git/workspace fields from Config.
|
|
type ProjectBinding struct {
|
|
SettingID int64
|
|
Config projectsync.Config
|
|
}
|
|
|
|
// ProjectBinder resolves the active project sync binding for a work item's
|
|
// provider/project target and reserves an independent workspace slot for it.
|
|
// Implementations must key the binding strictly on the normalized
|
|
// provider/tenant/project so tickets from the same project share one config and
|
|
// never mix with another project's config, and must reserve one available slot
|
|
// atomically so concurrent requests never receive the same slot.
|
|
type ProjectBinder interface {
|
|
ResolveProjectBinding(ctx context.Context, ref workitem.Ref) (ProjectBinding, error)
|
|
ReserveWorkspaceSlot(ctx context.Context, projectSyncSettingID int64) (projectsync.WorkspaceSlot, error)
|
|
}
|
|
|
|
// CheckoutMetadata is the stable JSON contract recorded on a created task under
|
|
// the "checkout" key. git remote and source branch always come from the project
|
|
// sync config; slot id/index/path come from the reserved slot and the checkout
|
|
// plan; branch paths come from the provision plan so downstream sync layers can
|
|
// distinguish the develop scanning base from the slot's independent checkout.
|
|
type CheckoutMetadata struct {
|
|
ProjectSyncSettingID int64 `json:"project_sync_setting_id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
GitRemoteURL string `json:"git_remote_url"`
|
|
SourceBranch string `json:"source_branch"`
|
|
ProjectWorkspaceRoot string `json:"project_workspace_root"`
|
|
MainBranchPath string `json:"main_branch_path"`
|
|
DevelopBranchPath string `json:"develop_branch_path"`
|
|
SlotID int64 `json:"slot_id"`
|
|
SlotIndex int `json:"slot_index"`
|
|
SlotPath string `json:"slot_path"`
|
|
}
|
|
|
|
type Service struct {
|
|
reader workitem.Reader
|
|
binder ProjectBinder
|
|
tasks TaskCreator
|
|
}
|
|
|
|
func New(reader workitem.Reader, binder ProjectBinder, tasks TaskCreator) *Service {
|
|
return &Service{reader: reader, binder: binder, tasks: tasks}
|
|
}
|
|
|
|
func (s *Service) CreateTaskFromWorkItem(ctx context.Context, input CreateTaskInput) (storage.Task, error) {
|
|
// Resolve the project sync binding and reserve a workspace slot before
|
|
// touching the provider so a ticket whose project has no active config,
|
|
// an invalid config, or no available slot never reaches task creation.
|
|
if s.binder == nil {
|
|
return storage.Task{}, ErrProjectSyncNotConfigured
|
|
}
|
|
binding, err := s.binder.ResolveProjectBinding(ctx, input.Ref)
|
|
if err != nil {
|
|
return storage.Task{}, err
|
|
}
|
|
config, err := binding.Config.Normalize()
|
|
if err != nil {
|
|
return storage.Task{}, err
|
|
}
|
|
slot, err := s.binder.ReserveWorkspaceSlot(ctx, binding.SettingID)
|
|
if err != nil {
|
|
return storage.Task{}, err
|
|
}
|
|
plan, err := projectsync.BuildCheckoutPlan(config, slot)
|
|
if err != nil {
|
|
return storage.Task{}, err
|
|
}
|
|
provision, err := projectsync.BuildProvisionPlan(config)
|
|
if err != nil {
|
|
return storage.Task{}, err
|
|
}
|
|
|
|
item, err := s.reader.FetchWorkItem(ctx, input.Ref)
|
|
if err != nil {
|
|
return storage.Task{}, err
|
|
}
|
|
createInput, err := workitem.BuildCreateTaskInput(workitem.TaskCreateInput{
|
|
Ref: input.Ref,
|
|
StateID: input.StateID,
|
|
Comment: input.Comment,
|
|
}, item)
|
|
if err != nil {
|
|
return storage.Task{}, err
|
|
}
|
|
createInput.Metadata, err = checkoutTaskMetadata(binding.SettingID, config, slot, plan, provision)
|
|
if err != nil {
|
|
return storage.Task{}, err
|
|
}
|
|
return s.tasks.CreateTask(ctx, createInput)
|
|
}
|
|
|
|
func checkoutTaskMetadata(settingID int64, config projectsync.Config, slot projectsync.WorkspaceSlot, plan projectsync.CheckoutPlan, provision projectsync.ProvisionPlan) (json.RawMessage, error) {
|
|
return json.Marshal(map[string]any{
|
|
"checkout": CheckoutMetadata{
|
|
ProjectSyncSettingID: settingID,
|
|
WorkspaceID: config.WorkspaceID,
|
|
GitRemoteURL: plan.RemoteURL,
|
|
SourceBranch: plan.SourceBranch,
|
|
ProjectWorkspaceRoot: plan.ProjectWorkspaceRoot,
|
|
MainBranchPath: provision.MainBranchPath,
|
|
DevelopBranchPath: provision.DevelopBranchPath,
|
|
SlotID: slot.ID,
|
|
SlotIndex: int(plan.SlotIndex),
|
|
SlotPath: plan.SlotPath,
|
|
},
|
|
})
|
|
}
|