Reorganizing GitHub Teams

This is old. See for updates.

This is a proposal for pruning & renaming many of the 180+ teams in the openedx GitHub org.



Axim will make a spreadsheet enumerating all GitHub teams. We will ask people to vouch for the teams that they actually use, and suggest a good new name for the teams based on this naming guide. All teams without vouchers will be deleted.


  • We have two main uses for teams today:

    1. Mentioning groups of people on PRs or issues. Examples:

      • Mentions in a PR: @openedx/blah-devs, please review this change.”

      • Automatically request a review from a team on PRs, notably via .github/CODEOWNERS or upgrade-python-requirements.yml.

      • @openedx/cla-problems is used to alert a couple Axim employees about CLA checker issues.

      • @openedx/axim-oncall is used to alert the Axim on-call engineer to incoming requests.

    2. Conferring permissions on one or more repository to one or more person.

      • Example: push-pull-all currently grandfathers in write access to edX developers for edX-contributed repos.

      • Example:

  • Most of the current openedx org teams were copied over from the edx GitHub org in Jan 2022. Various people have the ability to make teams and rename them, so they have grown and changed a bit since then.

  • There is currently no naming scheme or process for keeping them up-to-date.

    • Exception: the runbooks for onboarding and offboarding Coding Core Contributors specify a team name and structure for granting CCs commit rights in a consistent way: @openedx/ccp-committer-USERNAME .

  • Axim recently tried making a plan to rename all existing teams based on , but without cleaning up the list of teams first, it seemed to risky and time-consuming.


  • Since teams were copied over from the edx org, many of the names are 2U/edX specific, but are not prefixed with “2U” or “edX”. This is confusing for the community.

    • Example: @openedx/ecommerce refers to 2U/edX employees working on ecommerce, not a general Open edX e-commerce guild/group.

  • Team membership and names are often out-of-date. The teams names often refer to real-life teams that no longer exist. This is both confusing for the community and concerning from an access control standpoint.

  • Several “read access” teams now have no effect whatsoever because the vast majority of openedx repositories are public, whereas the edx org had many private repositories.


We propose a plan to delete unused teams and update the names of the remaining ones. Completing all steps will involve small amounts of work over a 6+ week period.

Step 1: List all teams and ask for vouchers

  • Axim will make a spreadsheet listing every remaining team in the org, structured like this:


Grants access?

Member count


Proposed name

Other notes


Grants access?

Member count


Proposed name

Other notes






probably safe to delete -J




Feanil Patel














@bob what do you think? -K




Kyle McCormick








Axim will circulate the spreadsheet and ask folks to fill out the Voucher and Proposed Name columns:

  • Team will show the team slug (that is: the team name in lowercase, with special characters replaced with dashes).

  • Grants Access? will indicate whether the teams actually grants anyone access to anything. Granting read access to public repositories does not count.

  • Member count will indicate the number members.

  • Voucher will initially be empty. Folks should fill their name in if they would like to “vouch” that a team is useful. We plan to delete any teams that do not have a voucher.

  • Proposed name will initially be empty. If a repo is vouched for, the voucher can fill in a proposed name for the team based on . If they don’t, Axim will choose a name. If the original name meets the naming conventions, then it can be used as the proposed name.

  • Other notes for any other conversation. Keep in mind that, no matter what the notes say, the team will be deleted if there is no voucher.

Axim will provide a two week waiting period to allow folks to vouch for teams.

Step 2: Circulate final plan

We will make sure each team with a voucher has a proposed name that fits , and will mark each team without a voucher as TO BE DELETED. We will circulate a finalized version of the spreadsheet, again with a two week waiting period for folks to get ready to make any downstream updates. Changes that should be prepared:







Create team backup

@Kyle McCormick

Back-up existing team names, membership, and permissions into a JSON document so that we can restore them if anything goes awry.


@Kyle McCormick

Some repos have CODEOWNERS files which reference teams by their names. We should open PRs against each CODEOWNER file in order to apply the team name updates.



Most repos have a upgrade-python-requirements.yml GitHub Action workflow, any many of them specify a team for PR review. We should open PRs against those repos to apply the team name updates.




(Add any more known downstream changes as rows)



Step 3: Deleted un-vouched teams

Axim will delete unvouched teams. We will wait two more weeks to allow any issues to surface.

Step 4: Rename remaining teams

Axim will rename the existing teams based on the “Proposed Name” column.

Open Questions

  • Are there downstream effects of the plan that we’re not accounting for? (see Step 2)

  • Should we do something about “team maintainers” (the GitHub feature, not the Open edX thing)? A “team maintainer” is someone on a team who is able to rename the team and add/remove members. Currently they’re assigned in a fairly arbitrary way.


Here’s the Python script we used to generate the initial spreadsheet. We ran this, copied its output, and pasted it into the spreadsheet.

Requires ghapi and Python ~3.7+

#!/usr/bin/env python3 import json, subprocess query='''\ query($endCursor: String) { organization(login: "openedx") { teams(first:100, after: $endCursor) { nodes { name url description members { totalCount } repositories { edges { permission } } } pageInfo { hasNextPage endCursor } } } }''' def call_query(end_cursor): args = ['gh', 'api', 'graphql', '-f', f'query={query}'] if end_cursor: args += ['-F', f'endCursor={end_cursor}'] output =, capture_output=True) return json.loads(output.stdout) end_cursor = None teams = [] while True: response = call_query(end_cursor) end_cursor = response['data']['organization']['teams']["pageInfo"].get("endCursor") teams += response['data']['organization']['teams']['nodes'] if not end_cursor: break for team in teams: url = team['url'] name = team['name'] description = team['description'] num_members = team['members']['totalCount'] perms = [ repo_perm['permission'] for repo_perm in team['repositories']['edges'] ] num_admin_repos = len([1 for perm in perms if perm == 'ADMIN']) num_maintain_repos = len([1 for perm in perms if perm == 'MAINTAIN']) num_write_repos = len([1 for perm in perms if perm == 'WRITE']) num_triage_repos = len([1 for perm in perms if perm == 'TRIAGE']) print( f'=hyperlink("{url}", "{name}")', f'"{description}"', f'"{num_members}"', f'"{num_admin_repos}"', f'"{num_maintain_repos}"', f'"{num_write_repos}"', f'"{num_triage_repos}"', sep='\t', )