Skip to content

Commit 1725c92

Browse files
committed
fix(sep-automation): check sponsor membership directly instead of enumerating teams
Use getMembershipForUserInOrg API to check if a user is a member of the steering-committee team, instead of recursively enumerating all child teams and their members. This avoids the need for admin permissions to list child teams, while still correctly handling nested team membership (the GitHub API does this automatically).
1 parent 39f4178 commit 1725c92

1 file changed

Lines changed: 25 additions & 74 deletions

File tree

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

Lines changed: 25 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ import type { GitHubClient } from "../github/client.js";
88

99
/**
1010
* The root team whose members (including all subteam members) can sponsor SEPs.
11+
* The GitHub API's getMembershipForUserInOrg checks nested team membership automatically.
1112
*/
1213
const SPONSOR_ROOT_TEAM = "steering-committee";
1314

1415
export class MaintainerResolver {
1516
private readonly config: Config;
1617
private readonly github: GitHubClient;
1718
private readonly logger: Logger | undefined;
18-
private maintainerSet: Set<string> | null = null;
19-
private loadAttempted = false;
19+
// Cache of username -> canSponsor result
20+
private readonly sponsorCache: Map<string, boolean> = new Map();
2021

2122
constructor(config: Config, github: GitHubClient, logger?: Logger) {
2223
this.config = config;
@@ -25,81 +26,32 @@ export class MaintainerResolver {
2526
}
2627

2728
/**
28-
* Load allowed sponsors from the API by recursively traversing
29-
* all subteams of steering-committee.
29+
* Check if a user can sponsor SEPs.
30+
* Uses getMembershipForUserInOrg which automatically checks nested team membership,
31+
* avoiding the need for admin permissions to enumerate child teams.
3032
*/
31-
private async ensureSponsorsLoaded(): Promise<Set<string>> {
32-
if (this.maintainerSet) {
33-
return this.maintainerSet;
34-
}
35-
36-
if (this.loadAttempted) {
37-
// Already tried and failed, return empty set
38-
return new Set();
33+
async canSponsor(username: string): Promise<boolean> {
34+
// Check cache first
35+
const cached = this.sponsorCache.get(username);
36+
if (cached !== undefined) {
37+
return cached;
3938
}
4039

41-
this.loadAttempted = true;
42-
43-
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-
55-
const allMembers = new Set<string>();
56-
57-
// Fetch members from all discovered teams
58-
for (const team of allTeams) {
59-
try {
60-
const members = await this.github.getTeamMembers(
61-
this.config.targetOwner,
62-
team,
63-
);
64-
for (const member of members) {
65-
allMembers.add(member);
66-
}
67-
} catch (error) {
68-
this.logger?.debug(
69-
{ team, error: String(error) },
70-
"Failed to load team members, continuing with others",
71-
);
72-
}
73-
}
40+
const isMember = await this.github.isTeamMember(
41+
this.config.targetOwner,
42+
SPONSOR_ROOT_TEAM,
43+
username,
44+
);
7445

75-
if (allMembers.size > 0) {
76-
this.maintainerSet = allMembers;
77-
this.logger?.info(
78-
{ count: allMembers.size, teamCount: allTeams.length },
79-
"Loaded allowed sponsors from API",
80-
);
81-
return this.maintainerSet;
82-
}
46+
// Cache the result
47+
this.sponsorCache.set(username, isMember);
8348

84-
throw new Error("No team members loaded from any team");
85-
} catch (error) {
86-
this.logger?.error(
87-
{ error: String(error) },
88-
"Failed to load sponsors from API",
89-
);
90-
// No fallback - return empty set
91-
this.maintainerSet = new Set();
92-
return this.maintainerSet;
93-
}
94-
}
49+
this.logger?.debug(
50+
{ username, isMember, team: SPONSOR_ROOT_TEAM },
51+
"Checked sponsor eligibility",
52+
);
9553

96-
/**
97-
* Check if a user can sponsor SEPs.
98-
* Any member of a steering-committee subteam can sponsor.
99-
*/
100-
async canSponsor(username: string): Promise<boolean> {
101-
const sponsors = await this.ensureSponsorsLoaded();
102-
return sponsors.has(username);
54+
return isMember;
10355
}
10456

10557
/**
@@ -115,10 +67,9 @@ export class MaintainerResolver {
11567
}
11668

11769
/**
118-
* Clear the cached sponsor list (useful for testing)
70+
* Clear the cached sponsor results (useful for testing)
11971
*/
12072
clearCache(): void {
121-
this.maintainerSet = null;
122-
this.loadAttempted = false;
73+
this.sponsorCache.clear();
12374
}
12475
}

0 commit comments

Comments
 (0)