github-org-manager 0.5.0__tar.gz → 0.5.2__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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: github-org-manager
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Manage a GitHub Organization, its teams, repository permissions, and more
5
5
  Home-page: https://github.com/OpenRailAssociation/github-org-manager
6
6
  License: Apache-2.0
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
20
21
  Classifier: Topic :: Software Development :: Version Control :: Git
21
22
  Classifier: Topic :: Utilities
22
23
  Requires-Dist: pygithub (>=2.3.0,<3.0.0)
@@ -39,7 +39,22 @@ def run_graphql_query(query, variables, token):
39
39
  headers=headers,
40
40
  timeout=10,
41
41
  )
42
- if request.status_code == 200:
43
- return request.json()
44
42
 
45
- sys.exit(f"Query failed to run by returning code of {query}: {request.status_code}")
43
+ # Get JSON result
44
+ json_return = "No valid JSON return"
45
+ try:
46
+ json_return = request.json()
47
+ except requests.exceptions.JSONDecodeError:
48
+ pass
49
+
50
+ if request.status_code == 200:
51
+ return json_return
52
+
53
+ # Debug information in case of errors
54
+ print(
55
+ f"Query failed with HTTP error code '{request.status_code}' when running "
56
+ f"this query: {query}\n"
57
+ f"Return: {json_return}\n"
58
+ f"Headers: {request.headers}"
59
+ )
60
+ sys.exit(1)
@@ -30,6 +30,7 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
30
30
  org_members: list[NamedUser] = field(default_factory=list)
31
31
  current_teams: dict[Team, dict] = field(default_factory=dict)
32
32
  configured_teams: dict[str, dict | None] = field(default_factory=dict)
33
+ newly_added_users: list[NamedUser] = field(default_factory=list)
33
34
  current_repos_teams: dict[Repository, dict[Team, str]] = field(default_factory=dict)
34
35
  current_repos_collaborators: dict[Repository, dict[str, str]] = field(default_factory=dict)
35
36
  configured_repos_collaborators: dict[str, dict[str, str]] = field(default_factory=dict)
@@ -197,6 +198,112 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
197
198
 
198
199
  logging.debug("Configuration for team '%s' consolidated to: %s", team_name, team_config)
199
200
 
201
+ # --------------------------------------------------------------------------
202
+ # Owners
203
+ # --------------------------------------------------------------------------
204
+ def _get_current_org_owners(self) -> None:
205
+ """Get all owners of the org"""
206
+ # Reset the user list, then build up new list
207
+ self.current_org_owners = []
208
+ for member in self.org.get_members(role="admin"):
209
+ self.current_org_owners.append(member)
210
+
211
+ def _check_configured_org_owners(self) -> bool:
212
+ """Check configured owners and make them lower-case for better
213
+ comparison. Returns True if owners are well configured."""
214
+ # Add configured owners if they are a list
215
+ if isinstance(self.configured_org_owners, list):
216
+ # Make all configured users lower-case
217
+ self.configured_org_owners = [user.lower() for user in self.configured_org_owners]
218
+ else:
219
+ logging.warning(
220
+ "The organisation owners are not configured as a proper list. Will not handle them."
221
+ )
222
+ self.configured_org_owners = []
223
+
224
+ if not self.configured_org_owners:
225
+ logging.warning(
226
+ "No owners for your GitHub organisation configured. Will not make any "
227
+ "change regarding the ownership, and continue with the current owners: %s",
228
+ ", ".join([user.login for user in self.current_org_owners]),
229
+ )
230
+ return False
231
+
232
+ return True
233
+
234
+ def _is_user_authenticated_user(self, user: NamedUser) -> bool:
235
+ """Check if a given NamedUser is the authenticated user"""
236
+ if user.login == self.gh.get_user().login:
237
+ return True
238
+ return False
239
+
240
+ def sync_org_owners(self, dry: bool = False, force: bool = False) -> None:
241
+ """Synchronise the organization owners"""
242
+ # Get current and configured owners
243
+ self._get_current_org_owners()
244
+
245
+ # Abort owner synchronisation if no owners are configured, or badly
246
+ if not self._check_configured_org_owners():
247
+ return
248
+
249
+ # Get differences between the current and configured owners
250
+ owners_remove, owners_ok, owners_add = self.compare_two_lists(
251
+ self.configured_org_owners, [user.login for user in self.current_org_owners]
252
+ )
253
+ # Compare configured (lower-cased) owners with lower-cased list of current owners
254
+ if not owners_remove and not owners_add:
255
+ logging.info("Organization owners are in sync, no changes")
256
+ return
257
+
258
+ logging.debug(
259
+ "Organization owners are not in sync. Config: '%s' vs. Current: '%s'",
260
+ self.configured_org_owners,
261
+ self.current_org_owners,
262
+ )
263
+ logging.debug(
264
+ "Will remove %s, will not change %s, will add %s", owners_remove, owners_ok, owners_add
265
+ )
266
+
267
+ # Add the missing owners
268
+ for user in owners_add:
269
+ if gh_user := self._resolve_gh_username(user, "<org owners>"):
270
+ logging.info("Adding user '%s' as organization owner", gh_user.login)
271
+ if not dry:
272
+ self.org.add_to_members(gh_user, "admin")
273
+
274
+ # Remove the surplus owners
275
+ for user in owners_remove:
276
+ if gh_user := self._resolve_gh_username(user, "<org owners>"):
277
+ logging.info(
278
+ "User '%s' is not configured as organization owners. "
279
+ "Will make them a normal member",
280
+ gh_user.login,
281
+ )
282
+ # Handle authenticated user being the same as the one you want to degrade
283
+ if self._is_user_authenticated_user(gh_user):
284
+ logging.warning(
285
+ "The user '%s' you want to remove from owners is the one you "
286
+ "authenticated with. This may disrupt all further operations. "
287
+ "Unless you run the program with --force, "
288
+ "this operation will not be executed.",
289
+ gh_user.login,
290
+ )
291
+ # Check if user forced this operation
292
+ if force:
293
+ logging.info(
294
+ "You called the program with --force, "
295
+ "so it will remove yourself from the owners"
296
+ )
297
+ else:
298
+ continue
299
+
300
+ # Execute the degradation of the owner
301
+ if not dry:
302
+ self.org.add_to_members(gh_user, "member")
303
+
304
+ # Update the current organisation owners
305
+ self._get_current_org_owners()
306
+
200
307
  # --------------------------------------------------------------------------
201
308
  # Teams
202
309
  # --------------------------------------------------------------------------
@@ -342,112 +449,6 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
342
449
  else:
343
450
  logging.info("Team '%s' settings are in sync, no changes", team.name)
344
451
 
345
- # --------------------------------------------------------------------------
346
- # Owners
347
- # --------------------------------------------------------------------------
348
- def _get_current_org_owners(self) -> None:
349
- """Get all owners of the org"""
350
- # Reset the user list, then build up new list
351
- self.current_org_owners = []
352
- for member in self.org.get_members(role="admin"):
353
- self.current_org_owners.append(member)
354
-
355
- def _check_configured_org_owners(self) -> bool:
356
- """Check configured owners and make them lower-case for better
357
- comparison. Returns True if owners are well configured."""
358
- # Add configured owners if they are a list
359
- if isinstance(self.configured_org_owners, list):
360
- # Make all configured users lower-case
361
- self.configured_org_owners = [user.lower() for user in self.configured_org_owners]
362
- else:
363
- logging.warning(
364
- "The organisation owners are not configured as a proper list. Will not handle them."
365
- )
366
- self.configured_org_owners = []
367
-
368
- if not self.configured_org_owners:
369
- logging.warning(
370
- "No owners for your GitHub organisation configured. Will not make any "
371
- "change regarding the ownership, and continue with the current owners: %s",
372
- ", ".join([user.login for user in self.current_org_owners]),
373
- )
374
- return False
375
-
376
- return True
377
-
378
- def _is_user_authenticated_user(self, user: NamedUser) -> bool:
379
- """Check if a given NamedUser is the authenticated user"""
380
- if user.login == self.gh.get_user().login:
381
- return True
382
- return False
383
-
384
- def sync_org_owners(self, dry: bool = False, force: bool = False) -> None:
385
- """Synchronise the organization owners"""
386
- # Get current and configured owners
387
- self._get_current_org_owners()
388
-
389
- # Abort owner synchronisation if no owners are configured, or badly
390
- if not self._check_configured_org_owners():
391
- return
392
-
393
- # Get differences between the current and configured owners
394
- owners_remove, owners_ok, owners_add = self.compare_two_lists(
395
- self.configured_org_owners, [user.login for user in self.current_org_owners]
396
- )
397
- # Compare configured (lower-cased) owners with lower-cased list of current owners
398
- if not owners_remove and not owners_add:
399
- logging.info("Organization owners are in sync, no changes")
400
- return
401
-
402
- logging.debug(
403
- "Organization owners are not in sync. Config: '%s' vs. Current: '%s'",
404
- self.configured_org_owners,
405
- self.current_org_owners,
406
- )
407
- logging.debug(
408
- "Will remove %s, will not change %s, will add %s", owners_remove, owners_ok, owners_add
409
- )
410
-
411
- # Add the missing owners
412
- for user in owners_add:
413
- if gh_user := self._resolve_gh_username(user, "<org owners>"):
414
- logging.info("Adding user '%s' as organization owner", gh_user.login)
415
- if not dry:
416
- self.org.add_to_members(gh_user, "admin")
417
-
418
- # Remove the surplus owners
419
- for user in owners_remove:
420
- if gh_user := self._resolve_gh_username(user, "<org owners>"):
421
- logging.info(
422
- "User '%s' is not configured as organization owners. "
423
- "Will make them a normal member",
424
- gh_user.login,
425
- )
426
- # Handle authenticated user being the same as the one you want to degrade
427
- if self._is_user_authenticated_user(gh_user):
428
- logging.warning(
429
- "The user '%s' you want to remove from owners is the one you "
430
- "authenticated with. This may disrupt all further operations. "
431
- "Unless you run the program with --force, "
432
- "this operation will not be executed.",
433
- gh_user.login,
434
- )
435
- # Check if user forced this operation
436
- if force:
437
- logging.info(
438
- "You called the program with --force, "
439
- "so it will remove yourself from the owners"
440
- )
441
- else:
442
- continue
443
-
444
- # Execute the degradation of the owner
445
- if not dry:
446
- self.org.add_to_members(gh_user, "member")
447
-
448
- # Update the current organisation owners
449
- self._get_current_org_owners()
450
-
451
452
  # --------------------------------------------------------------------------
452
453
  # Members
453
454
  # --------------------------------------------------------------------------
@@ -485,6 +486,13 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
485
486
 
486
487
  return current_users
487
488
 
489
+ def _add_or_update_user_in_team(self, team: Team, user: NamedUser, role: str):
490
+ """Add or update membership of a user in a team"""
491
+ team.add_membership(member=user, role=role)
492
+ # Document that the user has just been added to a team. Relevant when we
493
+ # will later find users without team membership
494
+ self.newly_added_users.append(user)
495
+
488
496
  def sync_teams_members(self, dry: bool = False) -> None: # pylint: disable=too-many-branches
489
497
  """Check the configured members of each team, add missing ones and delete unconfigured"""
490
498
  logging.debug("Starting to sync team members")
@@ -571,7 +579,7 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
571
579
  config_role,
572
580
  )
573
581
  if not dry:
574
- team.add_membership(member=gh_user, role=config_role)
582
+ self._add_or_update_user_in_team(team=team, user=gh_user, role=config_role)
575
583
 
576
584
  # Update roles if they differ from old role
577
585
  elif config_role != current_team_members.get(config_user, ""):
@@ -585,7 +593,7 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
585
593
  config_role,
586
594
  )
587
595
  if not dry:
588
- team.add_membership(member=gh_user, role=config_role)
596
+ self._add_or_update_user_in_team(team=team, user=gh_user, role=config_role)
589
597
 
590
598
  # Loop through all current members. Remove them if they are not configured
591
599
  for current_user in current_team_members:
@@ -618,11 +626,12 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
618
626
  all_org_members = set(self.org_members + self.current_org_owners)
619
627
 
620
628
  # Get all members of all teams
621
- all_team_members_lst = []
629
+ all_team_members_lst: list[NamedUser] = []
622
630
  for _, team_attrs in self.current_teams.items():
623
631
  for member in team_attrs.get("members", {}):
624
632
  all_team_members_lst.append(member)
625
- all_team_members = set(all_team_members_lst)
633
+ # Also add users that have just been added to a team, and unify them
634
+ all_team_members: set[NamedUser] = set(all_team_members_lst + self.newly_added_users)
626
635
 
627
636
  # Find members that are in org_members but not team_members
628
637
  members_without_team = all_org_members.difference(all_team_members)
@@ -867,6 +876,8 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
867
876
 
868
877
  # Add team member to repo with their repo permissions
869
878
  for team_member in team_members:
879
+ # Lower-case team member
880
+ team_member = team_member.lower()
870
881
  # Check if permissions already exist
871
882
  if self.configured_repos_collaborators[repo].get(team_member, {}):
872
883
  logging.debug(
@@ -882,7 +893,7 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
882
893
  )
883
894
  )
884
895
  else:
885
- self.configured_repos_collaborators[repo][team_member.lower()] = perm
896
+ self.configured_repos_collaborators[repo][team_member] = perm
886
897
 
887
898
  def _convert_graphql_perm_to_rest(self, permission: str) -> str:
888
899
  """Convert a repo permission coming from the GraphQL API to the ones
@@ -4,7 +4,7 @@
4
4
 
5
5
  [tool.poetry]
6
6
  name = "github-org-manager"
7
- version = "0.5.0"
7
+ version = "0.5.2"
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"
@@ -116,12 +116,12 @@ def main():
116
116
  # Get current rate limit
117
117
  org.ratelimit()
118
118
 
119
+ # Synchronise organisation owners
120
+ org.sync_org_owners(dry=args.dry, force=args.force)
119
121
  # Create teams that aren't present at Github yet
120
122
  org.create_missing_teams(dry=args.dry)
121
123
  # Configure general settings of teams
122
124
  org.sync_current_teams_settings(dry=args.dry)
123
- # Synchronise organisation owners
124
- org.sync_org_owners(dry=args.dry, force=args.force)
125
125
  # Synchronise the team memberships
126
126
  org.sync_teams_members(dry=args.dry)
127
127
  # Report about organisation members that do not belong to any team