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. diff --git a/config/example/app.yaml b/config/example/app.yaml index f721c23..0d40f41 100644 --- a/config/example/app.yaml +++ b/config/example/app.yaml @@ -38,3 +38,10 @@ 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 + +# 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 94575e0..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,8 +661,35 @@ 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_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: + """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 +707,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..5f04688 100644 --- a/gh_org_mgr/manage.py +++ b/gh_org_mgr/manage.py @@ -129,8 +129,16 @@ 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 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, + 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