From e99658ddc0d73114637766ed9350faf38f6432ed Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 28 Jun 2026 02:00:17 -0500 Subject: [PATCH] feat(orgs): auto-create default teams on org creation (#513) New organizations now get three default teams in addition to Owners: - Developers (write: code, issues, PRs, wiki, projects; read: releases) - Reviewers (read: code, issues, PRs, releases, wiki) - CI/CD (write: actions, packages, releases; read: code) Teams are defined in DefaultOrgTeams and created inside the same transaction as the org, so creation is atomic. Claude-Session: https://claude.ai/code/session_011AAFzotGMf3ayvXhEmStCd --- CHANGELOG.md | 1 + models/organization/org.go | 87 +++++++++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4db6fd594a..21568974ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] ### Added +- Auto-create default teams on org creation: Developers (write), Reviewers (read), CI/CD (actions+packages) (#513) - Branch protection delete allowlist: configurable per-user/team/deploy-key allowlist for deleting protected branches (#696) - Workflow subdirectory discovery: workflows in subdirectories of `.mokogitea/workflows/` are now auto-discovered (#693) - API token scope `read:licensing` / `write:licensing` for licensing endpoints (#697) diff --git a/models/organization/org.go b/models/organization/org.go index ce174e1656..ef5d212b3b 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -323,6 +323,60 @@ func (org *Organization) UnitPermission(ctx context.Context, doer *user_model.Us } // CreateOrganization creates record of a new organization. +// DefaultTeamSpec defines a team to auto-create when a new organization is created. +type DefaultTeamSpec struct { + Name string + Description string + AccessMode perm.AccessMode + IncludesAllRepositories bool + CanCreateOrgRepo bool + Units map[unit.Type]perm.AccessMode +} + +// DefaultOrgTeams is the list of teams created for every new organization +// (in addition to the mandatory Owners team). Override in tests or via init. +var DefaultOrgTeams = []DefaultTeamSpec{ + { + Name: "Developers", + Description: "Members with write access to code, issues, and pull requests", + AccessMode: perm.AccessModeWrite, + IncludesAllRepositories: true, + Units: map[unit.Type]perm.AccessMode{ + unit.TypeCode: perm.AccessModeWrite, + unit.TypeIssues: perm.AccessModeWrite, + unit.TypePullRequests: perm.AccessModeWrite, + unit.TypeReleases: perm.AccessModeRead, + unit.TypeWiki: perm.AccessModeWrite, + unit.TypeProjects: perm.AccessModeWrite, + }, + }, + { + Name: "Reviewers", + Description: "Members with read access for code review", + AccessMode: perm.AccessModeRead, + IncludesAllRepositories: true, + Units: map[unit.Type]perm.AccessMode{ + unit.TypeCode: perm.AccessModeRead, + unit.TypeIssues: perm.AccessModeRead, + unit.TypePullRequests: perm.AccessModeRead, + unit.TypeReleases: perm.AccessModeRead, + unit.TypeWiki: perm.AccessModeRead, + }, + }, + { + Name: "CI/CD", + Description: "Members with write access to actions and packages", + AccessMode: perm.AccessModeWrite, + IncludesAllRepositories: true, + Units: map[unit.Type]perm.AccessMode{ + unit.TypeCode: perm.AccessModeRead, + unit.TypeActions: perm.AccessModeWrite, + unit.TypePackages: perm.AccessModeWrite, + unit.TypeReleases: perm.AccessModeWrite, + }, + }, +} + func CreateOrganization(ctx context.Context, org *Organization, owner *user_model.User) (err error) { if !owner.CanCreateOrganization() { return ErrUserNotAllowedCreateOrg{} @@ -348,7 +402,7 @@ func CreateOrganization(ctx context.Context, org *Organization, owner *user_mode } org.UseCustomAvatar = true org.MaxRepoCreation = -1 - org.NumTeams = 1 + org.NumTeams = 1 + len(DefaultOrgTeams) org.NumMembers = 1 org.Type = user_model.UserTypeOrganization @@ -413,6 +467,37 @@ func CreateOrganization(ctx context.Context, org *Organization, owner *user_mode }); err != nil { return fmt.Errorf("insert team-user relation: %w", err) } + + for _, spec := range DefaultOrgTeams { + dt := &Team{ + OrgID: org.ID, + LowerName: strings.ToLower(spec.Name), + Name: spec.Name, + Description: spec.Description, + AccessMode: spec.AccessMode, + IncludesAllRepositories: spec.IncludesAllRepositories, + CanCreateOrgRepo: spec.CanCreateOrgRepo, + } + if err = db.Insert(ctx, dt); err != nil { + return fmt.Errorf("insert default team %q: %w", spec.Name, err) + } + + dtUnits := make([]TeamUnit, 0, len(spec.Units)) + for tp, am := range spec.Units { + dtUnits = append(dtUnits, TeamUnit{ + OrgID: org.ID, + TeamID: dt.ID, + Type: tp, + AccessMode: am, + }) + } + if len(dtUnits) > 0 { + if err = db.Insert(ctx, &dtUnits); err != nil { + return fmt.Errorf("insert default team %q units: %w", spec.Name, err) + } + } + } + return nil }) } -- 2.52.0