From dba72610103dac34a274b82fe8c7b0c2253363ad Mon Sep 17 00:00:00 2001 From: Max Mehl Date: Thu, 20 Feb 2025 09:23:07 +0100 Subject: [PATCH 1/3] optionally remove members from organization who are not part of any team --- config/example/app.yaml | 4 ++++ gh_org_mgr/_gh_org.py | 27 ++++++++++++++++++++------- gh_org_mgr/manage.py | 7 +++++-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/config/example/app.yaml b/config/example/app.yaml index f721c23..8ca5790 100644 --- a/config/example/app.yaml +++ b/config/example/app.yaml @@ -38,3 +38,7 @@ github_app_private_key: | D9zgrlJ8D4bxPrwDrCuXHY7s/1/uCX3K+mS7CWpybOcJY4XzsNznOYcMQzw22fxl u1ioG/s3Ahhd778VIjj5d32Xbjj8vbSFj8vJe5bBNblYbelWfETg -----END RSA PRIVATE KEY----- + +# Remove members from organisation who are not member of any team or +# organisation owners. Default: false +remove_members_without_team: false diff --git a/gh_org_mgr/_gh_org.py b/gh_org_mgr/_gh_org.py index 94575e0..8c2004c 100644 --- a/gh_org_mgr/_gh_org.py +++ b/gh_org_mgr/_gh_org.py @@ -666,8 +666,11 @@ def sync_teams_members(self, dry: bool = False) -> None: # pylint: disable=too- team.name, ) - def get_members_without_team(self) -> None: - """Get all organisation members without any team membership""" + def get_members_without_team( + self, dry: bool = False, remove_members_without_team: bool = False + ) -> None: + """Get all organisation members without any team membership, and + optionally remove them""" # Combine org owners and org members all_org_members = set(self.org_members + self.current_org_owners) @@ -685,11 +688,21 @@ def get_members_without_team(self) -> None: members_without_team = all_org_members.difference(all_team_members) if members_without_team: - members_without_team_str = [user.login for user in members_without_team] - logging.warning( - "The following members of your GitHub organisation are not member of any team: %s", - ", ".join(members_without_team_str), - ) + if remove_members_without_team: + for user in members_without_team: + logging.info( + "Removing user '%s' from organisation as they are not member of any team", + user.login, + ) + if not dry: + self.org.remove_from_membership(user) + else: + members_without_team_str = [user.login for user in members_without_team] + logging.warning( + "The following members of your GitHub organisation are not " + "member of any team: %s", + ", ".join(members_without_team_str), + ) # -------------------------------------------------------------------------- # Repos diff --git a/gh_org_mgr/manage.py b/gh_org_mgr/manage.py index f3339e6..27ee15e 100644 --- a/gh_org_mgr/manage.py +++ b/gh_org_mgr/manage.py @@ -129,8 +129,11 @@ def main(): org.sync_current_teams_settings(dry=args.dry) # Synchronise the team memberships org.sync_teams_members(dry=args.dry) - # Report about organisation members that do not belong to any team - org.get_members_without_team() + # Report and act on organisation members that do not belong to any team + org.get_members_without_team( + dry=args.dry, + remove_members_without_team=cfg_app.get("remove_members_without_team", False), + ) # Synchronise the permissions of teams for all repositories org.sync_repo_permissions(dry=args.dry, ignore_archived=args.ignore_archived) # Remove individual collaborator permissions if they are higher than the one From a7318ed0d8dbd0ca0ad29b6739b803eb0e85e779 Mon Sep 17 00:00:00 2001 From: Max Mehl Date: Thu, 20 Feb 2025 09:44:36 +0100 Subject: [PATCH 2/3] optionally delete teams from organization that are not configured --- config/example/app.yaml | 3 +++ gh_org_mgr/_gh_org.py | 37 ++++++++++++++++++++++++++++--------- gh_org_mgr/manage.py | 5 +++++ 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/config/example/app.yaml b/config/example/app.yaml index 8ca5790..0d40f41 100644 --- a/config/example/app.yaml +++ b/config/example/app.yaml @@ -42,3 +42,6 @@ github_app_private_key: | # Remove members from organisation who are not member of any team or # organisation owners. Default: false remove_members_without_team: false + +# Delete teams that are not configured. Default: false +delete_unconfigured_teams: false diff --git a/gh_org_mgr/_gh_org.py b/gh_org_mgr/_gh_org.py index 8c2004c..556257e 100644 --- a/gh_org_mgr/_gh_org.py +++ b/gh_org_mgr/_gh_org.py @@ -550,6 +550,10 @@ def sync_teams_members(self, dry: bool = False) -> None: # pylint: disable=too- open_invitations = [user.login.lower() for user in self.org.invitations()] for team, team_attrs in self.current_teams.items(): + # Ignore any team not being configured locally, will be handled later + if team.name not in self.configured_teams: + continue + # Update current team members with dict[NamedUser, str (role)] team_attrs["members"] = self._get_current_team_members(team) @@ -559,15 +563,6 @@ def sync_teams_members(self, dry: bool = False) -> None: # pylint: disable=too- user.login.lower(): role for user, role in team_attrs["members"].items() } - # Handle the team not being configured locally - if team.name not in self.configured_teams: - logging.warning( - "Team '%s' does not seem to be configured locally. " - "Taking no action about this team at all", - team.name, - ) - continue - # Get configuration from current team if team_configuration := self.configured_teams.get(team.name): pass @@ -666,6 +661,30 @@ def sync_teams_members(self, dry: bool = False) -> None: # pylint: disable=too- team.name, ) + def get_unconfigured_teams( + self, dry: bool = False, delete_unconfigured_teams: bool = False + ) -> None: + """Get all teams that are not configured locally and optionally remove them""" + # Get all teams that are not configured locally + unconfigured_teams: list[Team] = [] + for team in self.current_teams: + if team.name not in self.configured_teams: + unconfigured_teams.append(team) + + if unconfigured_teams: + if delete_unconfigured_teams: + for team in unconfigured_teams: + logging.info("Deleting team '%s' as it is not configured locally", team.name) + if not dry: + team.delete() + else: + unconfigured_teams_str = [team.name for team in unconfigured_teams] + logging.warning( + "The following teams of your GitHub organisation are not " + "configured locally: %s. Taking no action about these teams.", + ", ".join(unconfigured_teams_str), + ) + def get_members_without_team( self, dry: bool = False, remove_members_without_team: bool = False ) -> None: diff --git a/gh_org_mgr/manage.py b/gh_org_mgr/manage.py index 27ee15e..5f04688 100644 --- a/gh_org_mgr/manage.py +++ b/gh_org_mgr/manage.py @@ -129,6 +129,11 @@ def main(): org.sync_current_teams_settings(dry=args.dry) # Synchronise the team memberships org.sync_teams_members(dry=args.dry) + # Report and act on teams that are not configured locally + org.get_unconfigured_teams( + dry=args.dry, + delete_unconfigured_teams=cfg_app.get("delete_unconfigured_teams", False), + ) # Report and act on organisation members that do not belong to any team org.get_members_without_team( dry=args.dry, From d091001a76da920df0fd8877deffc42cdd82a020 Mon Sep 17 00:00:00 2001 From: Max Mehl Date: Thu, 20 Feb 2025 09:48:44 +0100 Subject: [PATCH 3/3] update README with info on what's configured in app.yaml --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0283ab5..124e81b 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Afterwards, the tool is executable with the command `gh-org-mgr`. The `--help` f Inside [`config/example`](./config/example), you can find an example configuration that shall help you to understand the structure: -* `app.yaml`: Configuration necessary to run this tool +* `app.yaml`: Configuration necessary to run this tool and controlling some behaviour * `org.yaml`: Organization-wide configuration * `teams/*.yaml`: Configuration concerning the teams of your organization.