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 } type FreshOrganizationPlan struct { Phases []string } // 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) { 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") } return resolved, FreshOrganizationPlan{Phases: phases}, nil } // RunFreshOrganization executes only the reversible source-control setup plan. // Infrastructure, credentials, secret material, and cluster actions remain gated. func RunFreshOrganization(cfg config.Config, options FreshOrganizationOptions) (FreshOrganizationPlan, error) { resolved, plan, err := PlanFreshOrganization(cfg, options) if err != nil { return FreshOrganizationPlan{}, err } if err := EnsureTemplateRevisions(resolved); err != nil { return plan, fmt.Errorf("lock template revisions: %w", err) } manager := forgejo.NewRepoManager(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) } for _, repository := range []struct{ name, description string }{ {resolved.Flux.ManifestsRepo, "Centralized deployment manifests for Flux CD"}, {resolved.Flux.RepoName, "Flux CD cluster configurations"}, } { if _, err := manager.EnsureInitializedRepository(repository.name, repository.description); err != nil { return plan, fmt.Errorf("ensure Forgejo repository %q: %w", repository.name, err) } } if options.EnableDelivery { if _, err := manager.EnsureRepositoryCopy(resolved.Flux.TektonCatalogRepo, "User-managed Tekton pipeline catalog", resolved.Templates.TektonCatalogRepoURL); err != nil { return plan, fmt.Errorf("initialize Tekton catalog repository: %w", err) } } return plan, nil } 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 }