Skip to content

Instantly share code, notes, and snippets.

@turboBasic
Last active April 22, 2026 14:38
Show Gist options
  • Select an option

  • Save turboBasic/6b246e446ec0852d1b60afc0f6e5a891 to your computer and use it in GitHub Desktop.

Select an option

Save turboBasic/6b246e446ec0852d1b60afc0f6e5a891 to your computer and use it in GitHub Desktop.
List GitHub teams in an organization #github
#!/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