@@ -290,6 +290,194 @@ async def fetch_github_team_members(access_token: str, org_name: str, team_slug:
290290 return members
291291
292292
293+ def _build_github_team_members_graphql_query (team_count : int ) -> str :
294+ variable_defs = ["$org: String!" ]
295+ team_fields = []
296+ for index in range (team_count ):
297+ variable_defs .extend ([f"$slug{ index } : String!" , f"$after{ index } : String" ])
298+ team_fields .append (
299+ f"""
300+ team{ index } : team(slug: $slug{ index } ) {{
301+ members(first: 100, after: $after{ index } ) {{
302+ nodes {{
303+ login
304+ }}
305+ pageInfo {{
306+ hasNextPage
307+ endCursor
308+ }}
309+ }}
310+ }}"""
311+ )
312+
313+ return f"""
314+ query({ ", " .join (variable_defs )} ) {{
315+ organization(login: $org) {{
316+ { "" .join (team_fields )}
317+ }}
318+ }}
319+ """
320+
321+
322+ _GITHUB_ORG_TEAMS_GRAPHQL_QUERY = """
323+ query($org: String!, $after: String) {
324+ organization(login: $org) {
325+ teams(first: 100, after: $after) {
326+ nodes {
327+ name
328+ slug
329+ }
330+ pageInfo {
331+ hasNextPage
332+ endCursor
333+ }
334+ }
335+ }
336+ }
337+ """
338+
339+
340+ async def fetch_github_org_team_slugs_graphql (access_token : str , org_name : str ) -> set [str ] | None :
341+ """Fetch all team slugs that actually exist in a GitHub organization."""
342+ if not access_token or not org_name :
343+ return set ()
344+
345+ headers = {
346+ "Authorization" : f"Bearer { access_token } " ,
347+ "Accept" : "application/vnd.github+json" ,
348+ "X-GitHub-Api-Version" : "2022-11-28" ,
349+ }
350+ team_slugs : set [str ] = set ()
351+ after : str | None = None
352+
353+ try :
354+ async with aiohttp .ClientSession () as session :
355+ while True :
356+ async with session .post (
357+ "https://api.github.com/graphql" ,
358+ headers = headers ,
359+ json = {"query" : _GITHUB_ORG_TEAMS_GRAPHQL_QUERY , "variables" : {"org" : org_name , "after" : after }},
360+ ) as resp :
361+ if resp .status != 200 :
362+ log .warning ("GitHub GraphQL API returned status %d when fetching org teams" , resp .status )
363+ return None
364+ data = await resp .json ()
365+
366+ if data .get ("errors" ):
367+ log .warning ("GitHub GraphQL API returned errors when fetching org teams: %s" , data ["errors" ])
368+ return None
369+
370+ organization = data .get ("data" , {}).get ("organization" )
371+ if organization is None :
372+ log .warning ("GitHub GraphQL API did not return organization %s" , org_name )
373+ return None
374+
375+ teams = organization .get ("teams" ) or {}
376+ for team in teams .get ("nodes" ) or []:
377+ slug = team .get ("slug" ) if isinstance (team , dict ) else None
378+ if isinstance (slug , str ) and slug :
379+ team_slugs .add (slug )
380+
381+ page_info = teams .get ("pageInfo" ) or {}
382+ if not page_info .get ("hasNextPage" ):
383+ break
384+ after = page_info .get ("endCursor" )
385+ except Exception as e :
386+ log .warning ("Error fetching GitHub org teams with GraphQL for org %s: %s" , org_name , e )
387+ return None
388+
389+ return team_slugs
390+
391+
392+ async def fetch_github_team_members_table_graphql (
393+ access_token : str ,
394+ org_name : str ,
395+ team_keys : list [str ],
396+ ) -> dict [str , list [str ]] | None :
397+ """Fetch a login -> configured team-key table with batched GraphQL team queries."""
398+ if not access_token or not org_name or not team_keys :
399+ return {}
400+
401+ headers = {
402+ "Authorization" : f"Bearer { access_token } " ,
403+ "Accept" : "application/vnd.github+json" ,
404+ "X-GitHub-Api-Version" : "2022-11-28" ,
405+ }
406+ teams_by_login : dict [str , list [str ]] = {}
407+ actual_team_slugs = await fetch_github_org_team_slugs_graphql (access_token , org_name )
408+ if actual_team_slugs is None :
409+ return None
410+
411+ existing_team_keys = []
412+ for team_key in team_keys :
413+ api_slug = _github_team_api_slug (team_key )
414+ if api_slug in actual_team_slugs :
415+ existing_team_keys .append (team_key )
416+ else :
417+ log .warning ("GitHub org %s does not have configured team %s" , org_name , api_slug )
418+
419+ pending = dict .fromkeys (existing_team_keys )
420+
421+ try :
422+ async with aiohttp .ClientSession () as session :
423+ while pending :
424+ batch_keys = list (pending )
425+ variables : dict [str , str | None ] = {"org" : org_name }
426+ for index , team_key in enumerate (batch_keys ):
427+ variables [f"slug{ index } " ] = _github_team_api_slug (team_key )
428+ variables [f"after{ index } " ] = pending [team_key ]
429+
430+ async with session .post (
431+ "https://api.github.com/graphql" ,
432+ headers = headers ,
433+ json = {
434+ "query" : _build_github_team_members_graphql_query (len (batch_keys )),
435+ "variables" : variables ,
436+ },
437+ ) as resp :
438+ if resp .status != 200 :
439+ log .warning ("GitHub GraphQL API returned status %d when fetching team members" , resp .status )
440+ return None
441+ data = await resp .json ()
442+
443+ if data .get ("errors" ):
444+ log .warning ("GitHub GraphQL API returned errors when fetching team members: %s" , data ["errors" ])
445+ return None
446+
447+ organization = data .get ("data" , {}).get ("organization" )
448+ if organization is None :
449+ log .warning ("GitHub GraphQL API did not return organization %s" , org_name )
450+ return None
451+
452+ next_pending : dict [str , str | None ] = {}
453+ for index , team_key in enumerate (batch_keys ):
454+ team = organization .get (f"team{ index } " )
455+ if team is None :
456+ log .warning (
457+ "GitHub GraphQL API did not return team %s in org %s" ,
458+ _github_team_api_slug (team_key ),
459+ org_name ,
460+ )
461+ continue
462+
463+ members = team .get ("members" ) or {}
464+ for member in members .get ("nodes" ) or []:
465+ login = member .get ("login" ) if isinstance (member , dict ) else None
466+ if isinstance (login , str ) and login :
467+ teams_by_login .setdefault (login .lower (), []).append (team_key )
468+
469+ page_info = members .get ("pageInfo" ) or {}
470+ if page_info .get ("hasNextPage" ):
471+ next_pending [team_key ] = page_info .get ("endCursor" )
472+
473+ pending = next_pending
474+ except Exception as e :
475+ log .warning ("Error fetching GitHub team members with GraphQL for org %s: %s" , org_name , e )
476+ return None
477+
478+ return teams_by_login
479+
480+
293481async def fetch_github_team_members_table (
294482 app_id : str ,
295483 installation_id : str ,
@@ -329,13 +517,13 @@ async def fetch_github_team_members_table(
329517 if not installation_token :
330518 return None
331519
332- teams_by_login : dict [ str , list [ str ]] = {}
333- for team_key in github_team_keys :
334- members = await fetch_github_team_members ( installation_token , org_name , _github_team_api_slug ( team_key ))
335- if members is None :
336- return None
337- for login in members :
338- teams_by_login . setdefault ( login , []). append ( team_key )
520+ teams_by_login = await fetch_github_team_members_table_graphql (
521+ installation_token ,
522+ org_name ,
523+ github_team_keys ,
524+ )
525+ if teams_by_login is None :
526+ return None
339527
340528 _GITHUB_TEAM_MEMBERS_CACHE [cache_key ] = (now , teams_by_login )
341529 return {login : list (teams ) for login , teams in teams_by_login .items ()}
0 commit comments