Skip to content
247 linesCodeBlameRaw
1/**
2 * Teams: groups of a workspace's members, given roles on repositories
3 * together, mentioned together and asked to review together. Kept by the
4 * identity service; mirrors `crates/contracts/src/teams.rs`.
5 */
6import type { RepoRole } from "./access";
7import type { User } from "./identity";
8import type { Result } from "./result";
9
10export type TeamVisibility = "visible" | "secret";
11
12/**
13 * Who may create a workspace's teams: any member (the default) or its
14 * owners only. Owners change it in the workspace's settings.
15 */
16export type TeamCreation = "members" | "owners";
17
18/** Whether someone with `role` in a workspace may create its teams. */
19export function mayCreateTeams(setting: TeamCreation | null | undefined, role: "owner" | "member" | null | undefined): boolean {
20 if (!role) return false;
21 return (setting ?? "members") === "members" || role === "owner";
22}
23export type TeamRole = "member" | "maintainer";
24export type ReviewAlgorithm = "round_robin" | "load_balance";
25
26export const TEAM_VISIBILITY_LABELS: Record<TeamVisibility, string> = {
27 visible: "Visible",
28 secret: "Secret",
29};
30
31export const TEAM_VISIBILITY_SUMMARIES: Record<TeamVisibility, string> = {
32 visible: "Every member of the workspace can see it and mention it.",
33 secret: "Only its own people and the workspace's owners can see it. Secret teams cannot be nested.",
34};
35
36export const REVIEW_ALGORITHM_LABELS: Record<ReviewAlgorithm, string> = {
37 round_robin: "Round robin",
38 load_balance: "Load balance",
39};
40
41export const REVIEW_ALGORITHM_SUMMARIES: Record<ReviewAlgorithm, string> = {
42 round_robin: "Whoever this team asked least recently goes first.",
43 load_balance: "Whoever has the fewest pull requests waiting on their review goes first.",
44};
45
46/** The most people review assignment picks for one request. */
47export const MAX_ASSIGNED = 10;
48
49export type ReviewAssignment = {
50 enabled: boolean;
51 algorithm: ReviewAlgorithm;
52 count: number;
53 skip_busy: boolean;
54 busy_at: number;
55 include_child_teams: boolean;
56 excluded: string[];
57 notify_team: boolean;
58};
59
60export const DEFAULT_REVIEW_ASSIGNMENT: ReviewAssignment = {
61 enabled: false,
62 algorithm: "round_robin",
63 count: 1,
64 skip_busy: false,
65 busy_at: 5,
66 include_child_teams: false,
67 excluded: [],
68 notify_team: false,
69};
70
71export type TeamRef = { slug: string; name: string };
72
73/** Who leads a team: a person on it, or an agent on it (by its id in the agents service). */
74export type TeamLead =
75 | { kind: "user"; username: string; name: string | null; avatar: string | null }
76 | { kind: "agent"; agent_id: string };
77
78/** A team's chat channel: its id, and its name (without `#`) when it was chosen. */
79export type TeamChannel = { id: string; name: string };
80
81/** An agent added to a team. */
82export type TeamAgent = {
83 agent_id: string;
84 /** Who added it, by username. */
85 added_by: string | null;
86 created_at: string;
87};
88
89/** A person on a team, as an agent on it is told of them (`agent_teams`). */
90export type RosterPerson = {
91 user_id: string;
92 username: string;
93 name: string | null;
94 title: string | null;
95 /** An IANA time zone, for their local time. */
96 timezone: string | null;
97 owns: string[];
98 /** Who they report to, by username. */
99 manager: string | null;
100 maintainer: boolean;
101};
102
103/** A visible team an agent is on, with everyone on it. For the agents service. */
104export type AgentTeam = {
105 slug: string;
106 name: string;
107 description: string | null;
108 lead: TeamLead | null;
109 channel: TeamChannel | null;
110 budget_micros: number | null;
111 people: RosterPerson[];
112 /** Agents added to it, by id. */
113 agent_ids: string[];
114};
115
116/** How `update_team` takes a lead: `@username`, or `agent:<id>`; `""` for none. */
117export function leadInput(lead: { kind: "user"; username: string } | { kind: "agent"; agent_id: string } | null): string {
118 if (!lead) return "";
119 return lead.kind === "user" ? `@${lead.username}` : `agent:${lead.agent_id}`;
120}
121
122export type Team = {
123 id: string;
124 workspace: string;
125 slug: string;
126 name: string;
127 description: string | null;
128 visibility: TeamVisibility;
129 parent: TeamRef | null;
130 notify: boolean;
131 review_assignment: ReviewAssignment;
132 members_count: number;
133 repos_count: number;
134 child_teams_count: number;
135 /** Agents added to it. Agents whose home team it is are on it too. */
136 agents_count: number;
137 lead: TeamLead | null;
138 channel: TeamChannel | null;
139 /** What its agents may spend together in a calendar month, in micro-dollars; null for none. */
140 budget_micros: number | null;
141 viewer_role: TeamRole | null;
142 can_manage: boolean;
143 created_at: string;
144 updated_at: string;
145};
146
147export type TeamMember = {
148 username: string;
149 name: string | null;
150 avatar: string | null;
151 role: TeamRole;
152 /** The child team they are in, when listed through one. */
153 via: string | null;
154};
155
156export type TeamRepo = {
157 /** `workspace/name`. */
158 repo: string;
159 repo_id: string;
160 role: RepoRole;
161 /** The parent team it comes from, when inherited. */
162 inherited_from: string | null;
163};
164
165/** A team with a role on a repository, as its Access settings list it. */
166export type RepoTeam = {
167 slug: string;
168 name: string;
169 role: RepoRole;
170 members_count: number;
171 visibility: TeamVisibility;
172};
173
174export type NewTeam = {
175 name: string;
176 slug?: string | null;
177 description?: string | null;
178 visibility?: TeamVisibility | null;
179 parent?: string | null;
180 notify?: boolean | null;
181 members?: string[];
182};
183
184/** What changes; the rest stays. `parent: ""` takes the team out from under its parent. */
185export type TeamChanges = {
186 name?: string;
187 slug?: string;
188 description?: string;
189 visibility?: TeamVisibility;
190 parent?: string;
191 notify?: boolean;
192 review_assignment?: ReviewAssignment;
193 /** `@username` or `agent:<id>`, someone on the team (see `leadInput`); `""` for no lead. */
194 lead?: string;
195 /** The chat channel's id; `""` for none. Given with `channel_name`. */
196 channel_id?: string;
197 channel_name?: string;
198 /** What its agents may spend together in a month, in micro-dollars; 0 for no team budget. */
199 budget_micros?: number;
200};
201
202/** `@acme/backend`, the way a team is mentioned. */
203export function teamHandle(team: { workspace: string; slug: string }): string {
204 return `@${team.workspace}/${team.slug}`;
205}
206
207/** A team's slug from its name, as identity makes it. Null when nothing is left. */
208export function teamSlug(name: string): string | null {
209 let slug = "";
210 for (const c of name.trim()) {
211 if (/[A-Za-z0-9]/.test(c)) slug += c.toLowerCase();
212 else if (slug && !slug.endsWith("-")) slug += "-";
213 }
214 slug = slug.replace(/-+$/, "").slice(0, 60).replace(/-+$/, "");
215 return slug ? slug : null;
216}
217
218/** One person's teams in a workspace, for the Members page. */
219export type MemberTeams = { username: string; teams: TeamRef[] };
220
221/** Identity's team methods. */
222export interface TeamsClient {
223 listTeams(viewer: User | null, workspace: string, query?: string | null): Promise<Result<Team[]>>;
224 getTeam(viewer: User | null, workspace: string, team: string): Promise<Result<Team>>;
225 createTeam(actor: User, workspace: string, team: NewTeam): Promise<Result<Team>>;
226 /** Who may create the workspace's teams. Owners only. */
227 setTeamCreation(actor: User, slug: string, setting: TeamCreation): Promise<Result<TeamCreation>>;
228 updateTeam(actor: User, workspace: string, team: string, changes: TeamChanges): Promise<Result<Team>>;
229 deleteTeam(actor: User, workspace: string, team: string): Promise<Result<boolean>>;
230 teamMembers(viewer: User | null, workspace: string, team: string, includeChildTeams?: boolean): Promise<Result<TeamMember[]>>;
231 setTeamMember(actor: User, workspace: string, team: string, username: string, role: TeamRole): Promise<Result<TeamMember>>;
232 removeTeamMember(actor: User, workspace: string, team: string, username: string): Promise<Result<boolean>>;
233 childTeams(viewer: User | null, workspace: string, team: string): Promise<Result<Team[]>>;
234 teamRepos(viewer: User | null, workspace: string, team: string): Promise<Result<TeamRepo[]>>;
235 setTeamRepo(actor: User, workspace: string, team: string, owner: string, name: string, role: RepoRole): Promise<Result<TeamRepo>>;
236 removeTeamRepo(actor: User, workspace: string, team: string, owner: string, name: string): Promise<Result<boolean>>;
237 /** The agents added to a team. Agents whose home team it is are on it too (`agentsOnTeam`). */
238 teamAgents(viewer: User | null, workspace: string, team: string): Promise<Result<TeamAgent[]>>;
239 /** Adds one of the workspace's agents, by id; check it is the workspace's first. Owners and maintainers. */
240 setTeamAgent(actor: User, workspace: string, team: string, agentId: string): Promise<Result<TeamAgent>>;
241 removeTeamAgent(actor: User, workspace: string, team: string, agentId: string): Promise<Result<boolean>>;
242 /** For the agents service: the visible teams an agent is on, with everyone on each. */
243 agentTeams(workspace: string, agentId: string, homeTeam: string | null): Promise<AgentTeam[]>;
244 userTeams(viewer: User | null, workspace: string, username: string): Promise<Result<Team[]>>;
245 /** Each member's teams the viewer can see. Members only. */
246 teamMemberships(viewer: User | null, workspace: string): Promise<Result<MemberTeams[]>>;
247}