Last active
April 22, 2026 14:38
-
-
Save turboBasic/6b246e446ec0852d1b60afc0f6e5a891 to your computer and use it in GitHub Desktop.
List GitHub teams in an organization #github
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """List GitHub organization teams and print matching team names. | |
| Usage: | |
| export GITHUB_TOKEN="<token-with-read:org>" | |
| python3 list_github_org.py --org your-org-name find-teams-with-members --user-names user1,user2 | |
| Notes: | |
| - For private org/team visibility, the token needs at least `read:org` scope. | |
| - Fine-grained tokens must include organization members/teams read permissions. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| import urllib.error | |
| import urllib.parse | |
| import urllib.request | |
| from typing import Any, Dict, Iterable, List, Optional, Tuple | |
| GITHUB_API = "https://api.github.com" | |
| DEFAULT_ORG = "your-org-name" | |
| TARGET_USERNAMES = {"default-user-name-1", "default-user-name-2"} | |
| def main() -> int: | |
| github_token_env = "GITHUB_TOKEN" | |
| parser = argparse.ArgumentParser( | |
| description="Manage GitHub organization teams and members." | |
| ) | |
| parser.add_argument( | |
| "--token", | |
| default=os.getenv(github_token_env), | |
| help=f"GitHub token (defaults to env var {github_token_env})", | |
| ) | |
| subparsers = parser.add_subparsers(dest="command", help="Available commands") | |
| list_orgs_parser = subparsers.add_parser( | |
| "list-org-teams", help="List all teams in an organization" | |
| ) | |
| list_orgs_parser.add_argument("--org", default=DEFAULT_ORG, help="GitHub organization name") | |
| list_orgs_parser.set_defaults(func=_handle_list_org_teams) | |
| list_members_parser = subparsers.add_parser( | |
| "list-team-members", help="List members of a specific team" | |
| ) | |
| list_members_parser.add_argument("--org", default=DEFAULT_ORG, help="GitHub organization name") | |
| list_members_parser.add_argument("team_slug", help="Team slug") | |
| list_members_parser.set_defaults(func=_handle_list_team_members) | |
| find_teams_parser = subparsers.add_parser( | |
| "find-teams-with-members", | |
| help="Find teams containing target members", | |
| ) | |
| find_teams_parser.add_argument("--org", default=DEFAULT_ORG, help="GitHub organization name") | |
| find_teams_parser.add_argument( | |
| "user_names", | |
| nargs="?", | |
| default=None, | |
| help="Comma-separated GitHub usernames to search for (default: TARGET_USERNAMES)", | |
| ) | |
| find_teams_parser.set_defaults(func=_handle_find_teams_with_members) | |
| args = parser.parse_args() | |
| if not args.token: | |
| print( | |
| "Error: GitHub token is required. Set GITHUB_TOKEN or pass --token.", | |
| file=sys.stderr, | |
| ) | |
| return 1 | |
| if not args.command: | |
| parser.print_help() | |
| return 0 | |
| return args.func(args) | |
| def list_org_teams(org: str, headers: Dict[str, str]) -> List[Dict[str, Any]]: | |
| page_size = 100 | |
| url = f"{GITHUB_API}/orgs/{urllib.parse.quote(org)}/teams?per_page={page_size}" | |
| return list(_paged_get(url, headers)) | |
| def list_team_members(org: str, team_slug: str, headers: Dict[str, str]) -> List[Dict[str, Any]]: | |
| page_size = 100 | |
| encoded_org = urllib.parse.quote(org) | |
| encoded_slug = urllib.parse.quote(team_slug) | |
| url = f"{GITHUB_API}/orgs/{encoded_org}/teams/{encoded_slug}/members?per_page={page_size}" | |
| return list(_paged_get(url, headers)) | |
| def _build_headers(token: str) -> Dict[str, str]: | |
| accept = "application/vnd.github+json" | |
| api_version = "2022-11-28" | |
| user_agent = "github-team-member-lister" | |
| return { | |
| "Accept": accept, | |
| "Authorization": f"Bearer {token}", | |
| "User-Agent": user_agent, | |
| "X-GitHub-Api-Version": api_version, | |
| } | |
| def _github_get_json(url: str, headers: Dict[str, str]) -> Tuple[Any, Optional[str]]: | |
| req = urllib.request.Request(url, headers=headers, method="GET") | |
| try: | |
| with urllib.request.urlopen(req) as response: | |
| body = response.read().decode("utf-8") | |
| data = json.loads(body) | |
| link_header = response.headers.get("Link") | |
| return data, _parse_next_link(link_header) | |
| except urllib.error.HTTPError as exc: | |
| body = exc.read().decode("utf-8", errors="replace") | |
| raise RuntimeError( | |
| f"GitHub API request failed: {exc.code} {exc.reason}\nURL: {url}\nResponse: {body}" | |
| ) from exc | |
| except urllib.error.URLError as exc: | |
| raise RuntimeError(f"Network error while calling GitHub API: {exc}") from exc | |
| def _handle_find_teams_with_members(args: argparse.Namespace) -> int: | |
| try: | |
| user_names = ( | |
| {n.strip() for n in args.user_names.split(",")} | |
| if args.user_names | |
| else TARGET_USERNAMES | |
| ) | |
| headers = _build_headers(args.token) | |
| teams = list_org_teams(args.org, headers) | |
| if not teams: | |
| print(f"No teams found in organization '{args.org}'.") | |
| return 0 | |
| matched_team_count = 0 | |
| for team in teams: | |
| team_name = team.get("name", "<unknown-team-name>") | |
| team_slug = team.get("slug") | |
| if not team_slug: | |
| continue | |
| members = list_team_members(args.org, team_slug, headers) | |
| if not members: | |
| continue | |
| member_logins = { | |
| str(member.get("login", "")).lower() | |
| for member in members | |
| if isinstance(member, dict) | |
| } | |
| if not member_logins.intersection(user_names): | |
| continue | |
| matched_team_count += 1 | |
| print(team_name) | |
| if matched_team_count == 0: | |
| print( | |
| "No teams found containing members: " | |
| + ", ".join(sorted(user_names)) | |
| ) | |
| return 0 | |
| except RuntimeError as exc: | |
| print(str(exc), file=sys.stderr) | |
| return 2 | |
| def _handle_list_org_teams(args: argparse.Namespace) -> int: | |
| try: | |
| headers = _build_headers(args.token) | |
| teams = list_org_teams(args.org, headers) | |
| if not teams: | |
| print(f"No teams found in organization '{args.org}'.") | |
| return 0 | |
| for team in teams: | |
| team_name = team.get("name", "<unknown-team-name>") | |
| team_slug = team.get("slug", "<unknown-slug>") | |
| print(f"{team_name} ({team_slug})") | |
| return 0 | |
| except RuntimeError as exc: | |
| print(str(exc), file=sys.stderr) | |
| return 2 | |
| def _handle_list_team_members(args: argparse.Namespace) -> int: | |
| try: | |
| headers = _build_headers(args.token) | |
| members = list_team_members(args.org, args.team_slug, headers) | |
| if not members: | |
| print(f"No members found in team '{args.team_slug}'.") | |
| return 0 | |
| for member in sorted(members, key=lambda m: m.get("login", "").lower()): | |
| login = member.get("login", "<unknown-login>") | |
| print(login) | |
| return 0 | |
| except RuntimeError as exc: | |
| print(str(exc), file=sys.stderr) | |
| return 2 | |
| def _paged_get(url: str, headers: Dict[str, str]) -> Iterable[Dict[str, Any]]: | |
| next_url: Optional[str] = url | |
| while next_url: | |
| data, next_url = _github_get_json(next_url, headers) | |
| if not isinstance(data, list): | |
| raise RuntimeError(f"Expected list response from {url}, got: {type(data).__name__}") | |
| for item in data: | |
| if isinstance(item, dict): | |
| yield item | |
| def _parse_next_link(link_header: Optional[str]) -> Optional[str]: | |
| if not link_header: | |
| return None | |
| # <https://api.github.com/...&page=2>; rel="next", <...>; rel="last" | |
| parts = [part.strip() for part in link_header.split(",")] | |
| for part in parts: | |
| if 'rel="next"' not in part: | |
| continue | |
| left = part.find("<") | |
| right = part.find(">") | |
| if left != -1 and right != -1 and right > left: | |
| return part[left + 1 : right] | |
| return None | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment