package bootstrap import ( "errors" "fmt" "os" "path/filepath" "github.com/Pingu-Studio/MaidnCLI/internal/config" "github.com/Pingu-Studio/MaidnCLI/internal/forgejo" ) type FreshOrganizationOptions struct { Organization string CreateOrganization bool EnableDelivery bool Mode Mode ConfirmRebuild bool } type FreshOrganizationPlan struct { Phases []string } var ensureFreshTemplateRevisions = EnsureTemplateRevisions var newFreshRepoManager = forgejo.NewRepoManager var runFreshLifecycle = reconcileFreshOrganization // PlanFreshOrganization validates the fresh, reversible setup phases before // any Forgejo or Git boundary is reached. func PlanFreshOrganization(cfg config.Config, options FreshOrganizationOptions) (config.Config, FreshOrganizationPlan, error) { mode, err := resolveLifecycleMode(options.Mode, options.ConfirmRebuild) if err != nil { return cfg, FreshOrganizationPlan{}, err } resolved, err := config.ResolveFreshBootstrap(cfg, options.Organization, options.EnableDelivery) if err != nil { return cfg, FreshOrganizationPlan{}, err } if !options.CreateOrganization { return cfg, FreshOrganizationPlan{}, errors.New("--create-organization is required for fresh organization bootstrap") } if err := validateFreshWorkspace(resolved); err != nil { return cfg, FreshOrganizationPlan{}, err } phases := []string{ "validate isolated workspace and configuration", "lock template revisions in the isolated workspace", "ensure the Forgejo organization", "ensure baseline Flux and manifests repositories", } if options.EnableDelivery { phases = append(phases, "initialize the user-managed Tekton catalog repository") } phases = append(phases, fmt.Sprintf("%s the CI/CD cluster", mode)) return resolved, FreshOrganizationPlan{Phases: phases}, nil } // RunFreshOrganization completes a fresh bootstrap through the selected lifecycle. func RunFreshOrganization(cfg config.Config, options FreshOrganizationOptions) (FreshOrganizationPlan, error) { resolved, plan, err := PlanFreshOrganization(cfg, options) if err != nil { return FreshOrganizationPlan{}, err } if err := ensureFreshTemplateRevisions(resolved); err != nil { return plan, fmt.Errorf("lock template revisions: %w", err) } manager := newFreshRepoManager(resolved.Git.BaseURL, resolved.Git.Token, resolved.Git.Owner, resolved.Git.Username, "", "", resolved.Flux.Branch, "") if _, err := manager.EnsureOrganization(options.CreateOrganization); err != nil { return plan, fmt.Errorf("ensure Forgejo organization: %w", err) } if err := runFreshLifecycle(resolved, options); err != nil { return plan, fmt.Errorf("run fresh CI/CD bootstrap: %w", err) } return plan, nil } func reconcileFreshOrganization(cfg config.Config, options FreshOrganizationOptions) error { return freshLifecycleRunner(cfg, options).Run() } func freshLifecycleRunner(cfg config.Config, options FreshOrganizationOptions) Runner { mode := options.Mode if mode == "" { mode = Reconcile } return Runner{ Config: cfg, Mode: mode, ConfirmRebuild: options.ConfirmRebuild, EnableDelivery: options.EnableDelivery, SkipDeliveryScaffolding: !options.EnableDelivery, } } func validateFreshWorkspace(cfg config.Config) error { cloneRelative, err := filepath.Rel(cfg.WorkspaceDir, cfg.Git.CloneParent) if err != nil || filepath.Dir(cloneRelative) != "." { return errors.New("git cloneParent must be a direct child of isolated workspaceDir") } entries, err := os.ReadDir(cfg.WorkspaceDir) if os.IsNotExist(err) { return nil } if err != nil { return fmt.Errorf("inspect workspaceDir: %w", err) } if len(entries) == 0 { return nil } lock, err := readTemplateRevisionLock(filepath.Join(cfg.WorkspaceDir, "maidn-template-revisions.yaml")) if err != nil || !sameTemplateSource(lock.CICD, templateCheckout{Repository: cfg.Templates.CICDRepoURL, Ref: cfg.Templates.CICDRepoRef}) || !sameTemplateSource(lock.Manifests, templateCheckout{Repository: cfg.Templates.ManifestsRepoURL, Ref: cfg.Templates.ManifestsRepoRef}) || !sameTemplateSource(lock.Talos, templateCheckout{Repository: cfg.Templates.TalosRepoURL, Ref: cfg.Templates.TalosRepoRef}) { return errors.New("workspaceDir contains ambiguous state; use a new empty isolated workspaceDir") } allowed := map[string]bool{ "maidn-template-revisions.yaml": true, "maidn-cicd-cluster-template": true, "cicd-deployment-manifests-template": true, cloneRelative: true, } for _, entry := range entries { if !allowed[entry.Name()] { return errors.New("workspaceDir contains ambiguous state; use a new empty isolated workspaceDir") } } return nil }