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.
- {github_org_manager-0.5.0 → github_org_manager-0.5.2}/PKG-INFO +2 -1
- {github_org_manager-0.5.0 → github_org_manager-0.5.2}/gh_org_mgr/_gh_api.py +18 -3
- {github_org_manager-0.5.0 → github_org_manager-0.5.2}/gh_org_mgr/_gh_org.py +122 -111
- {github_org_manager-0.5.0 → github_org_manager-0.5.2}/pyproject.toml +1 -1
- {github_org_manager-0.5.0 → github_org_manager-0.5.2}/LICENSE.txt +0 -0
- {github_org_manager-0.5.0 → github_org_manager-0.5.2}/LICENSES/Apache-2.0.txt +0 -0
- {github_org_manager-0.5.0 → github_org_manager-0.5.2}/LICENSES/CC-BY-4.0.txt +0 -0
- {github_org_manager-0.5.0 → github_org_manager-0.5.2}/LICENSES/CC0-1.0.txt +0 -0
- {github_org_manager-0.5.0 → github_org_manager-0.5.2}/LICENSES/MIT.txt +0 -0
- {github_org_manager-0.5.0 → github_org_manager-0.5.2}/README.md +0 -0
- {github_org_manager-0.5.0 → github_org_manager-0.5.2}/gh_org_mgr/__init__.py +0 -0
- {github_org_manager-0.5.0 → github_org_manager-0.5.2}/gh_org_mgr/_config.py +0 -0
- {github_org_manager-0.5.0 → github_org_manager-0.5.2}/gh_org_mgr/_setup_team.py +0 -0
- {github_org_manager-0.5.0 → github_org_manager-0.5.2}/gh_org_mgr/manage.py +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: github-org-manager
|
|
3
|
-
Version: 0.5.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|