github-org-manager 0.5.6__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: github-org-manager
3
- Version: 0.5.6
3
+ Version: 0.6.0
4
4
  Summary: Manage a GitHub Organization, its teams, repository permissions, and more
5
- Home-page: https://github.com/OpenRailAssociation/github-org-manager
6
5
  License: Apache-2.0
7
6
  Keywords: github,github-management,permissions,access-control
8
7
  Author: Max Mehl
@@ -77,7 +76,7 @@ Afterwards, the tool is executable with the command `gh-org-mgr`. The `--help` f
77
76
 
78
77
  Inside [`config/example`](./config/example), you can find an example configuration that shall help you to understand the structure:
79
78
 
80
- * `app.yaml`: Configuration necessary to run this tool
79
+ * `app.yaml`: Configuration necessary to run this tool and controlling some behaviour
81
80
  * `org.yaml`: Organization-wide configuration
82
81
  * `teams/*.yaml`: Configuration concerning the teams of your organization.
83
82
 
@@ -48,7 +48,7 @@ Afterwards, the tool is executable with the command `gh-org-mgr`. The `--help` f
48
48
 
49
49
  Inside [`config/example`](./config/example), you can find an example configuration that shall help you to understand the structure:
50
50
 
51
- * `app.yaml`: Configuration necessary to run this tool
51
+ * `app.yaml`: Configuration necessary to run this tool and controlling some behaviour
52
52
  * `org.yaml`: Organization-wide configuration
53
53
  * `teams/*.yaml`: Configuration concerning the teams of your organization.
54
54
 
@@ -15,10 +15,12 @@ from github import (
15
15
  GithubIntegration,
16
16
  UnknownObjectException,
17
17
  )
18
+ from github.GithubException import BadCredentialsException
18
19
  from github.NamedUser import NamedUser
19
20
  from github.Organization import Organization
20
21
  from github.Repository import Repository
21
22
  from github.Team import Team
23
+ from jwt.exceptions import InvalidKeyError
22
24
 
23
25
  from ._gh_api import get_github_secrets_from_env, run_graphql_query
24
26
 
@@ -65,6 +67,7 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
65
67
  # supported, or multiple spaces etc.
66
68
  return team.replace(" ", "-")
67
69
 
70
+ # amazonq-ignore-next-line
68
71
  def login(
69
72
  self, orgname: str, token: str = "", app_id: str | int = "", app_private_key: str = ""
70
73
  ) -> None:
@@ -81,7 +84,11 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
81
84
  logging.debug("Logging in via app %s", self.gh_app_id)
82
85
  auth = Auth.AppAuth(app_id=self.gh_app_id, private_key=self.gh_app_private_key)
83
86
  app = GithubIntegration(auth=auth)
84
- installation = app.get_org_installation(org=orgname)
87
+ try:
88
+ installation = app.get_org_installation(org=orgname)
89
+ except InvalidKeyError:
90
+ logging.critical("Invalid private key provided for GitHub App")
91
+ sys.exit(1)
85
92
  self.gh = installation.get_github_for_installation()
86
93
  logging.debug("Logged in via app installation %s", installation.id)
87
94
 
@@ -90,7 +97,11 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
90
97
  elif self.gh_token:
91
98
  logging.debug("Logging in as user with PAT")
92
99
  self.gh = Github(auth=Auth.Token(self.gh_token))
93
- logging.debug("Logged in as %s", self.gh.get_user().login)
100
+ try:
101
+ logging.debug("Logged in as %s", self.gh.get_user().login)
102
+ except BadCredentialsException:
103
+ logging.critical("Invalid GitHub token provided")
104
+ sys.exit(1)
94
105
  else:
95
106
  logging.error("No GitHub token or App ID+private key provided")
96
107
  sys.exit(1)
@@ -539,6 +550,10 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
539
550
  open_invitations = [user.login.lower() for user in self.org.invitations()]
540
551
 
541
552
  for team, team_attrs in self.current_teams.items():
553
+ # Ignore any team not being configured locally, will be handled later
554
+ if team.name not in self.configured_teams:
555
+ continue
556
+
542
557
  # Update current team members with dict[NamedUser, str (role)]
543
558
  team_attrs["members"] = self._get_current_team_members(team)
544
559
 
@@ -548,15 +563,6 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
548
563
  user.login.lower(): role for user, role in team_attrs["members"].items()
549
564
  }
550
565
 
551
- # Handle the team not being configured locally
552
- if team.name not in self.configured_teams:
553
- logging.warning(
554
- "Team '%s' does not seem to be configured locally. "
555
- "Taking no action about this team at all",
556
- team.name,
557
- )
558
- continue
559
-
560
566
  # Get configuration from current team
561
567
  if team_configuration := self.configured_teams.get(team.name):
562
568
  pass
@@ -655,8 +661,35 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
655
661
  team.name,
656
662
  )
657
663
 
658
- def get_members_without_team(self) -> None:
659
- """Get all organisation members without any team membership"""
664
+ def get_unconfigured_teams(
665
+ self, dry: bool = False, delete_unconfigured_teams: bool = False
666
+ ) -> None:
667
+ """Get all teams that are not configured locally and optionally remove them"""
668
+ # Get all teams that are not configured locally
669
+ unconfigured_teams: list[Team] = []
670
+ for team in self.current_teams:
671
+ if team.name not in self.configured_teams:
672
+ unconfigured_teams.append(team)
673
+
674
+ if unconfigured_teams:
675
+ if delete_unconfigured_teams:
676
+ for team in unconfigured_teams:
677
+ logging.info("Deleting team '%s' as it is not configured locally", team.name)
678
+ if not dry:
679
+ team.delete()
680
+ else:
681
+ unconfigured_teams_str = [team.name for team in unconfigured_teams]
682
+ logging.warning(
683
+ "The following teams of your GitHub organisation are not "
684
+ "configured locally: %s. Taking no action about these teams.",
685
+ ", ".join(unconfigured_teams_str),
686
+ )
687
+
688
+ def get_members_without_team(
689
+ self, dry: bool = False, remove_members_without_team: bool = False
690
+ ) -> None:
691
+ """Get all organisation members without any team membership, and
692
+ optionally remove them"""
660
693
  # Combine org owners and org members
661
694
  all_org_members = set(self.org_members + self.current_org_owners)
662
695
 
@@ -665,18 +698,30 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
665
698
  for _, team_attrs in self.current_teams.items():
666
699
  for member in team_attrs.get("members", {}):
667
700
  all_team_members_lst.append(member)
668
- # Also add users that have just been added to a team, and unify them
669
- all_team_members: set[NamedUser] = set(all_team_members_lst + self.newly_added_users)
701
+ # Also add org owners and users that have just been added to a team, and unify them
702
+ all_team_members: set[NamedUser] = set(
703
+ all_team_members_lst + self.newly_added_users + self.current_org_owners
704
+ )
670
705
 
671
706
  # Find members that are in org_members but not team_members
672
707
  members_without_team = all_org_members.difference(all_team_members)
673
708
 
674
709
  if members_without_team:
675
- members_without_team_str = [user.login for user in members_without_team]
676
- logging.warning(
677
- "The following members of your GitHub organisation are not member of any team: %s",
678
- ", ".join(members_without_team_str),
679
- )
710
+ if remove_members_without_team:
711
+ for user in members_without_team:
712
+ logging.info(
713
+ "Removing user '%s' from organisation as they are not member of any team",
714
+ user.login,
715
+ )
716
+ if not dry:
717
+ self.org.remove_from_membership(user)
718
+ else:
719
+ members_without_team_str = [user.login for user in members_without_team]
720
+ logging.warning(
721
+ "The following members of your GitHub organisation are not "
722
+ "member of any team: %s",
723
+ ", ".join(members_without_team_str),
724
+ )
680
725
 
681
726
  # --------------------------------------------------------------------------
682
727
  # Repos
@@ -129,8 +129,16 @@ def main():
129
129
  org.sync_current_teams_settings(dry=args.dry)
130
130
  # Synchronise the team memberships
131
131
  org.sync_teams_members(dry=args.dry)
132
- # Report about organisation members that do not belong to any team
133
- org.get_members_without_team()
132
+ # Report and act on teams that are not configured locally
133
+ org.get_unconfigured_teams(
134
+ dry=args.dry,
135
+ delete_unconfigured_teams=cfg_app.get("delete_unconfigured_teams", False),
136
+ )
137
+ # Report and act on organisation members that do not belong to any team
138
+ org.get_members_without_team(
139
+ dry=args.dry,
140
+ remove_members_without_team=cfg_app.get("remove_members_without_team", False),
141
+ )
134
142
  # Synchronise the permissions of teams for all repositories
135
143
  org.sync_repo_permissions(dry=args.dry, ignore_archived=args.ignore_archived)
136
144
  # Remove individual collaborator permissions if they are higher than the one
@@ -4,7 +4,7 @@
4
4
 
5
5
  [tool.poetry]
6
6
  name = "github-org-manager"
7
- version = "0.5.6"
7
+ version = "0.6.0"
8
8
  description = "Manage a GitHub Organization, its teams, repository permissions, and more"
9
9
  authors = ["Max Mehl <max.mehl@deutschebahn.com>"]
10
10
  readme = "README.md"
@@ -33,13 +33,13 @@ requests = "^2.32.3"
33
33
  python-slugify = "^8.0.4"
34
34
 
35
35
  [tool.poetry.group.dev.dependencies]
36
- black = "^24.3.0"
37
- isort = "^5.13.2"
36
+ black = "^25.1.0"
37
+ isort = ">=5.13.2,<7.0.0"
38
38
  mypy = "^1.9.0"
39
39
  pylint = "^3.1.0"
40
40
  types-pyyaml = "^6.0.12.20240311"
41
41
  types-requests = "^2.32.0.20240712"
42
- bump-my-version = "^0.26.0"
42
+ bump-my-version = "^0.32.0"
43
43
 
44
44
  [build-system]
45
45
  requires = ["poetry-core"]