Skip to content

Commit 0b3a76e

Browse files
committed
refactor(sep-automation): dynamically discover sponsor teams from steering-committee
- Add getChildTeams() and getAllDescendantTeams() to GitHubClient for recursive team traversal - Replace hardcoded SPONSOR_TEAMS array with dynamic discovery from steering-committee - Remove FALLBACK_SPONSORS manual list (~75 usernames) - Remove incomplete getSepByIssue stub from index.ts - Apply formatting changes to processor.ts Claude-Generated-By: Claude Code (cli/claude-opus-4-5=28%) Claude-Steers: 2 Claude-Permission-Prompts: 2 Claude-Escapes: 0
1 parent e75bf11 commit 0b3a76e

3 files changed

Lines changed: 140 additions & 106 deletions

File tree

tools/sep-automation/src/github/client.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,41 @@ export class GitHubClient {
245245
return members.map(m => m.login);
246246
}
247247

248+
/**
249+
* Get child teams of a parent team
250+
*/
251+
async getChildTeams(org: string, parentTeamSlug: string): Promise<string[]> {
252+
await this.ensureInitialized();
253+
const children = await this.octokit.paginate(
254+
this.octokit.teams.listChildInOrg,
255+
{
256+
org,
257+
team_slug: parentTeamSlug,
258+
per_page: 100,
259+
}
260+
);
261+
return children.map(t => t.slug);
262+
}
263+
264+
/**
265+
* Recursively get all descendant teams of a parent team (including the parent itself)
266+
*/
267+
async getAllDescendantTeams(org: string, parentTeamSlug: string): Promise<string[]> {
268+
const allTeams: string[] = [parentTeamSlug];
269+
const queue = [parentTeamSlug];
270+
271+
while (queue.length > 0) {
272+
const current = queue.shift()!;
273+
const children = await this.getChildTeams(org, current);
274+
for (const child of children) {
275+
allTeams.push(child);
276+
queue.push(child);
277+
}
278+
}
279+
280+
return allTeams;
281+
}
282+
248283
/**
249284
* Get a single issue/PR by number
250285
*/

tools/sep-automation/src/maintainers/resolver.ts

Lines changed: 31 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,14 @@
22
* Core maintainer team lookup
33
*/
44

5-
import type { Logger } from 'pino';
6-
import type { Config } from '../config.js';
7-
import type { GitHubClient } from '../github/client.js';
5+
import type { Logger } from "pino";
6+
import type { Config } from "../config.js";
7+
import type { GitHubClient } from "../github/client.js";
88

99
/**
10-
* Subteams of steering-committee whose members can sponsor SEPs.
10+
* The root team whose members (including all subteam members) can sponsor SEPs.
1111
*/
12-
const SPONSOR_TEAMS = [
13-
'core-maintainers',
14-
'moderators',
15-
'working-groups',
16-
'interest-groups',
17-
'sdk-maintainers',
18-
'inspector-maintainers',
19-
'mcpb-maintainers',
20-
'docs-maintainers',
21-
'lead-maintainers',
22-
];
23-
24-
/**
25-
* Fallback list of allowed sponsors.
26-
* Used when the GitHub Teams API is not accessible.
27-
* Keep this in sync with the steering-committee subteams.
28-
*
29-
* Generated from: steering-committee subteams
30-
* Last updated: 2026-01-16
31-
*/
32-
const FALLBACK_SPONSORS = new Set([
33-
// core-maintainers
34-
'jspahrsummers', 'pcarleton', 'CaitieM20', 'pwwpche', 'kurtisvg',
35-
'localden', 'nickcoai', '000-000-000-000-000', 'dsp-ant', 'bhosmer-ant',
36-
// moderators
37-
'jonathanhefner', 'cliffhall', 'evalstate', 'tadasant', 'maheshmurag',
38-
'olaservo', 'jerome3o-anthropic',
39-
// working-groups
40-
'toby', 'aaronpk', 'felixweinberger', 'domdomegg', 'rdimitrov',
41-
'an-dustin', 'LucaButBoring', 'D-McAdams', 'jenn-newton', 'og-ant', 'petery-ant',
42-
// interest-groups
43-
'sambhav', 'PederHP',
44-
// sdk-maintainers
45-
'mattt', 'koic', 'michaelneale', 'fabpot', 'atesgoral', 'halter73',
46-
'nicolas-grekas', 'markpollack', 'ochafik', 'stallent', 'ignatov',
47-
'alexhancock', 'KKonstantinov', 'ansaba', 'pronskiy', 'Nyholm',
48-
'tzolov', 'kpavlov', 'topherbullock', 'movetz', 'chemicL', 'stephentoub',
49-
'eiriktsarpalis', 'chr-hertel', 'maciej-kisiel', 'e5l', 'jamadeo',
50-
// inspector-maintainers (KKonstantinov, cliffhall, olaservo already listed)
51-
// mcpb-maintainers
52-
'felixrieseberg', 'MarshallOfSound', 'asklar', 'joan-anthropic',
53-
// docs-maintainers
54-
'ihrpr', 'a-akimov',
55-
// lead-maintainers (dsp-ant already listed)
56-
]);
12+
const SPONSOR_ROOT_TEAM = "steering-committee";
5713

5814
export class MaintainerResolver {
5915
private readonly config: Config;
@@ -69,59 +25,70 @@ export class MaintainerResolver {
6925
}
7026

7127
/**
72-
* Load allowed sponsors from the API (all steering-committee subteams),
73-
* falling back to static list on error.
28+
* Load allowed sponsors from the API by recursively traversing
29+
* all subteams of steering-committee.
7430
*/
7531
private async ensureSponsorsLoaded(): Promise<Set<string>> {
7632
if (this.maintainerSet) {
7733
return this.maintainerSet;
7834
}
7935

8036
if (this.loadAttempted) {
81-
// Already tried and failed, use fallback
82-
return FALLBACK_SPONSORS;
37+
// Already tried and failed, return empty set
38+
return new Set();
8339
}
8440

8541
this.loadAttempted = true;
8642

8743
try {
44+
// Discover all teams recursively from the root team
45+
const allTeams = await this.github.getAllDescendantTeams(
46+
this.config.targetOwner,
47+
SPONSOR_ROOT_TEAM,
48+
);
49+
50+
this.logger?.debug(
51+
{ teams: allTeams },
52+
"Discovered sponsor teams from steering-committee",
53+
);
54+
8855
const allMembers = new Set<string>();
8956

90-
// Fetch members from all sponsor teams
91-
for (const team of SPONSOR_TEAMS) {
57+
// Fetch members from all discovered teams
58+
for (const team of allTeams) {
9259
try {
9360
const members = await this.github.getTeamMembers(
9461
this.config.targetOwner,
95-
team
62+
team,
9663
);
9764
for (const member of members) {
9865
allMembers.add(member);
9966
}
10067
} catch (error) {
10168
this.logger?.debug(
10269
{ team, error: String(error) },
103-
'Failed to load team, continuing with others'
70+
"Failed to load team members, continuing with others",
10471
);
10572
}
10673
}
10774

10875
if (allMembers.size > 0) {
10976
this.maintainerSet = allMembers;
11077
this.logger?.info(
111-
{ count: allMembers.size },
112-
'Loaded allowed sponsors from API'
78+
{ count: allMembers.size, teamCount: allTeams.length },
79+
"Loaded allowed sponsors from API",
11380
);
11481
return this.maintainerSet;
11582
}
11683

117-
// All teams failed, use fallback
118-
throw new Error('No team members loaded from any team');
84+
throw new Error("No team members loaded from any team");
11985
} catch (error) {
120-
this.logger?.warn(
86+
this.logger?.error(
12187
{ error: String(error) },
122-
'Failed to load sponsors from API, using fallback list'
88+
"Failed to load sponsors from API",
12389
);
124-
this.maintainerSet = FALLBACK_SPONSORS;
90+
// No fallback - return empty set
91+
this.maintainerSet = new Set();
12592
return this.maintainerSet;
12693
}
12794
}
@@ -135,13 +102,6 @@ export class MaintainerResolver {
135102
return sponsors.has(username);
136103
}
137104

138-
/**
139-
* Alias for canSponsor (for backward compatibility)
140-
*/
141-
async isCoreMaintainer(username: string): Promise<boolean> {
142-
return this.canSponsor(username);
143-
}
144-
145105
/**
146106
* Get the sponsor (allowed assignee) for a SEP
147107
*/

0 commit comments

Comments
 (0)