github-org-manager 0.3.1__tar.gz → 0.4.1__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.3.1 → github_org_manager-0.4.1}/PKG-INFO +11 -5
- {github_org_manager-0.3.1 → github_org_manager-0.4.1}/README.md +9 -4
- {github_org_manager-0.3.1 → github_org_manager-0.4.1}/gh_org_mgr/_gh_org.py +23 -13
- github_org_manager-0.4.1/gh_org_mgr/_setup_team.py +118 -0
- github_org_manager-0.4.1/gh_org_mgr/manage.py +131 -0
- {github_org_manager-0.3.1 → github_org_manager-0.4.1}/pyproject.toml +16 -1
- github_org_manager-0.3.1/gh_org_mgr/manage.py +0 -75
- {github_org_manager-0.3.1 → github_org_manager-0.4.1}/LICENSE.txt +0 -0
- {github_org_manager-0.3.1 → github_org_manager-0.4.1}/LICENSES/Apache-2.0.txt +0 -0
- {github_org_manager-0.3.1 → github_org_manager-0.4.1}/LICENSES/CC-BY-4.0.txt +0 -0
- {github_org_manager-0.3.1 → github_org_manager-0.4.1}/LICENSES/CC0-1.0.txt +0 -0
- {github_org_manager-0.3.1 → github_org_manager-0.4.1}/LICENSES/MIT.txt +0 -0
- {github_org_manager-0.3.1 → github_org_manager-0.4.1}/gh_org_mgr/__init__.py +0 -0
- {github_org_manager-0.3.1 → github_org_manager-0.4.1}/gh_org_mgr/_config.py +0 -0
- {github_org_manager-0.3.1 → github_org_manager-0.4.1}/gh_org_mgr/_gh_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: github-org-manager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
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
|
|
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
20
20
|
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
21
21
|
Classifier: Topic :: Utilities
|
|
22
22
|
Requires-Dist: pygithub (>=2.3.0,<3.0.0)
|
|
23
|
+
Requires-Dist: python-slugify (>=8.0.4,<9.0.0)
|
|
23
24
|
Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
|
|
24
25
|
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
25
26
|
Project-URL: Repository, https://github.com/OpenRailAssociation/github-org-manager
|
|
@@ -77,11 +78,16 @@ You may also be interested in the [live configuration of the OpenRail Associatio
|
|
|
77
78
|
|
|
78
79
|
You can execute the program using the command `gh-org-mgr`. `gh-org-mgr --help` shows all available arguments and options.
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
Synchronisation examples:
|
|
81
82
|
|
|
82
|
-
* `gh-org-mgr -c myorgconf`: synchronize the settings of the GitHub organization with your local configuration in the given configuration path (`myorgconf`). This may create new teams, remove/add members, and change permissions.
|
|
83
|
-
* `gh-org-mgr -c myorgconf --dry`: as above, but do not make any modification. Perfect for testing your local configuration and see its potential effects.
|
|
84
|
-
* `gh-org-mgr -c myorgconf --debug`: the first example, but show full debugging information.
|
|
83
|
+
* `gh-org-mgr sync -c myorgconf`: synchronize the settings of the GitHub organization with your local configuration in the given configuration path (`myorgconf`). This may create new teams, remove/add members, and change permissions.
|
|
84
|
+
* `gh-org-mgr sync -c myorgconf --dry`: as above, but do not make any modification. Perfect for testing your local configuration and see its potential effects.
|
|
85
|
+
* `gh-org-mgr sync -c myorgconf --debug`: the first example, but show full debugging information.
|
|
86
|
+
|
|
87
|
+
Setup team examples:
|
|
88
|
+
|
|
89
|
+
* `gh-org-mgr setup-team -n "My Team Name" -c myorgconf`: Bootstrap a team configuration for this team name. Will create a file `myorgconf/teams/my-team-name.yaml`, or provide options if this file already exists.
|
|
90
|
+
* `gh-org-mgr setup-team -n "My Team Name" -f path/to/myteam.yaml`: Bootstrap a team configuration for this team name and will force to write it in the given file. If the file already exists, offer some options.
|
|
85
91
|
|
|
86
92
|
## License
|
|
87
93
|
|
|
@@ -50,11 +50,16 @@ You may also be interested in the [live configuration of the OpenRail Associatio
|
|
|
50
50
|
|
|
51
51
|
You can execute the program using the command `gh-org-mgr`. `gh-org-mgr --help` shows all available arguments and options.
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
Synchronisation examples:
|
|
54
54
|
|
|
55
|
-
* `gh-org-mgr -c myorgconf`: synchronize the settings of the GitHub organization with your local configuration in the given configuration path (`myorgconf`). This may create new teams, remove/add members, and change permissions.
|
|
56
|
-
* `gh-org-mgr -c myorgconf --dry`: as above, but do not make any modification. Perfect for testing your local configuration and see its potential effects.
|
|
57
|
-
* `gh-org-mgr -c myorgconf --debug`: the first example, but show full debugging information.
|
|
55
|
+
* `gh-org-mgr sync -c myorgconf`: synchronize the settings of the GitHub organization with your local configuration in the given configuration path (`myorgconf`). This may create new teams, remove/add members, and change permissions.
|
|
56
|
+
* `gh-org-mgr sync -c myorgconf --dry`: as above, but do not make any modification. Perfect for testing your local configuration and see its potential effects.
|
|
57
|
+
* `gh-org-mgr sync -c myorgconf --debug`: the first example, but show full debugging information.
|
|
58
|
+
|
|
59
|
+
Setup team examples:
|
|
60
|
+
|
|
61
|
+
* `gh-org-mgr setup-team -n "My Team Name" -c myorgconf`: Bootstrap a team configuration for this team name. Will create a file `myorgconf/teams/my-team-name.yaml`, or provide options if this file already exists.
|
|
62
|
+
* `gh-org-mgr setup-team -n "My Team Name" -f path/to/myteam.yaml`: Bootstrap a team configuration for this team name and will force to write it in the given file. If the file already exists, offer some options.
|
|
58
63
|
|
|
59
64
|
## License
|
|
60
65
|
|
|
@@ -182,16 +182,16 @@ class GHorg: # pylint: disable=too-many-instance-attributes
|
|
|
182
182
|
self._get_org_members()
|
|
183
183
|
|
|
184
184
|
# Get open invitations
|
|
185
|
-
open_invitations = [user.login for user in self.org.invitations()]
|
|
185
|
+
open_invitations = [user.login.lower() for user in self.org.invitations()]
|
|
186
186
|
|
|
187
187
|
for team, team_attrs in self.current_teams.items():
|
|
188
188
|
# Update current team members with dict[NamedUser, str (role)]
|
|
189
189
|
team_attrs["members"] = self._get_current_team_members(team)
|
|
190
190
|
|
|
191
191
|
# For the rest of the function however, we use just the login name
|
|
192
|
-
# for each current user
|
|
192
|
+
# for each current user. All lower-case
|
|
193
193
|
current_team_members = {
|
|
194
|
-
user.login: role for user, role in team_attrs["members"].items()
|
|
194
|
+
user.login.lower(): role for user, role in team_attrs["members"].items()
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
# Handle the team not being configured locally
|
|
@@ -209,16 +209,17 @@ class GHorg: # pylint: disable=too-many-instance-attributes
|
|
|
209
209
|
else:
|
|
210
210
|
team_configuration = {}
|
|
211
211
|
|
|
212
|
-
# Analog to team_attrs["members"], add members and maintainers to
|
|
213
|
-
# dict with respective role, while maintainer role dominates
|
|
212
|
+
# Analog to team_attrs["members"], add members and maintainers to
|
|
213
|
+
# shared dict with respective role, while maintainer role dominates.
|
|
214
|
+
# All user names shall be lower-case to ease comparison
|
|
214
215
|
configured_users: dict[str, str] = {}
|
|
215
216
|
for config_role in ("member", "maintainer"):
|
|
216
217
|
team_members = self._get_configured_team_members(
|
|
217
218
|
team_configuration, team.name, config_role
|
|
218
219
|
)
|
|
219
220
|
for team_member in team_members:
|
|
220
|
-
# Add user with role to dict
|
|
221
|
-
configured_users.update({team_member: config_role})
|
|
221
|
+
# Add user with role to dict, in lower-case
|
|
222
|
+
configured_users.update({team_member.lower(): config_role})
|
|
222
223
|
|
|
223
224
|
# Consider all GitHub organisation team maintainers if they are member of the team
|
|
224
225
|
# This is because GitHub API returns them as maintainers even if they are just members
|
|
@@ -227,7 +228,7 @@ class GHorg: # pylint: disable=too-many-instance-attributes
|
|
|
227
228
|
logging.debug(
|
|
228
229
|
"Overriding role of organisation owner '%s' to maintainer", user.login
|
|
229
230
|
)
|
|
230
|
-
configured_users[user.login] = "maintainer"
|
|
231
|
+
configured_users[user.login.lower()] = "maintainer"
|
|
231
232
|
|
|
232
233
|
# Only make edits to the team membership if the current state differs from config
|
|
233
234
|
if configured_users == current_team_members:
|
|
@@ -278,8 +279,11 @@ class GHorg: # pylint: disable=too-many-instance-attributes
|
|
|
278
279
|
# Loop through all current members. Remove them if they are not configured
|
|
279
280
|
for current_user in current_team_members:
|
|
280
281
|
if current_user not in configured_users:
|
|
282
|
+
logging.debug("User '%s' not found within configured users", current_user)
|
|
281
283
|
# Turn user to GitHub object, trying to find them
|
|
282
284
|
if not (gh_user := self._resolve_gh_username(current_user, team.name)):
|
|
285
|
+
# If the user cannot be found for some reason, log an
|
|
286
|
+
# error and skip this loop
|
|
283
287
|
continue
|
|
284
288
|
if team.has_in_members(gh_user):
|
|
285
289
|
logging.info(
|
|
@@ -471,7 +475,13 @@ class GHorg: # pylint: disable=too-many-instance-attributes
|
|
|
471
475
|
"""Combine multiple lists into one while removing duplicates"""
|
|
472
476
|
complete = []
|
|
473
477
|
for single_list in lists:
|
|
474
|
-
|
|
478
|
+
if single_list is not None:
|
|
479
|
+
complete.extend(single_list)
|
|
480
|
+
else:
|
|
481
|
+
logging.debug(
|
|
482
|
+
"A list that we attempted to extend to another was None. "
|
|
483
|
+
"This probably happened because a 'member:' or 'maintainer:' key was left empty"
|
|
484
|
+
)
|
|
475
485
|
|
|
476
486
|
return list(set(complete))
|
|
477
487
|
|
|
@@ -561,7 +571,7 @@ class GHorg: # pylint: disable=too-many-instance-attributes
|
|
|
561
571
|
)
|
|
562
572
|
)
|
|
563
573
|
else:
|
|
564
|
-
self.configured_repos_collaborators[repo][team_member] = perm
|
|
574
|
+
self.configured_repos_collaborators[repo][team_member.lower()] = perm
|
|
565
575
|
|
|
566
576
|
def _convert_graphql_perm_to_rest(self, permission: str) -> str:
|
|
567
577
|
"""Convert a repo permission coming from the GraphQL API to the ones
|
|
@@ -627,12 +637,12 @@ class GHorg: # pylint: disable=too-many-instance-attributes
|
|
|
627
637
|
|
|
628
638
|
# Extract relevant data
|
|
629
639
|
for collaborator in collaborators:
|
|
630
|
-
login = collaborator["node"]["login"]
|
|
640
|
+
login: str = collaborator["node"]["login"]
|
|
631
641
|
# Skip entry if collaborator is org owner, which is "admin" anyway
|
|
632
|
-
if login in [user.login for user in self.org_owners]:
|
|
642
|
+
if login.lower() in [user.login.lower() for user in self.org_owners]:
|
|
633
643
|
continue
|
|
634
644
|
permission = self._convert_graphql_perm_to_rest(collaborator["permission"])
|
|
635
|
-
self.current_repos_collaborators[repo][login] = permission
|
|
645
|
+
self.current_repos_collaborators[repo][login.lower()] = permission
|
|
636
646
|
|
|
637
647
|
def _get_current_repos_and_user_perms(self):
|
|
638
648
|
"""Get all repos, their current collaborators and their permissions"""
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 DB Systel GmbH
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Functions to help with setting up new team"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from os.path import isfile, join
|
|
9
|
+
from string import Template
|
|
10
|
+
|
|
11
|
+
from slugify import slugify
|
|
12
|
+
|
|
13
|
+
TEAM_TEMPLATE = """
|
|
14
|
+
${team_name}:
|
|
15
|
+
# parent:
|
|
16
|
+
# repos:
|
|
17
|
+
# maintainer:
|
|
18
|
+
member:
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _sanitize_two_exclusive_options(option1: str | None, option2: str | None) -> bool:
|
|
23
|
+
"""Only of of these two options must be provided (not None, empty string is
|
|
24
|
+
OK). Returns True if no error ocourred"""
|
|
25
|
+
# There must not be a file_path and config_path provided at the same time
|
|
26
|
+
if option1 is not None and option2 is not None:
|
|
27
|
+
logging.critical("The two options must not be provided at the same time. Choose only one.")
|
|
28
|
+
return False
|
|
29
|
+
# There must at least be config_path or file_path configured
|
|
30
|
+
if option1 is None and option2 is None:
|
|
31
|
+
logging.critical("One of the two options must be provided")
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _fill_template(template: str, **fillers) -> str:
|
|
38
|
+
"""Fill a template using a dicts with keys and their values. The function
|
|
39
|
+
looks for the keys starting with '$' characters"""
|
|
40
|
+
return Template(template).substitute(fillers).lstrip()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _ask_user_action(question: str, *options: str) -> str:
|
|
44
|
+
"""Ask the user a question and for an action. Return the chosen action."""
|
|
45
|
+
option_dict: dict[str, str] = {}
|
|
46
|
+
option_questions: list[str] = []
|
|
47
|
+
for option in options:
|
|
48
|
+
# Get first unique characters from the option
|
|
49
|
+
short = ""
|
|
50
|
+
i = 0
|
|
51
|
+
while not short:
|
|
52
|
+
i += 1
|
|
53
|
+
short_try = option[:i]
|
|
54
|
+
if short_try not in option_dict:
|
|
55
|
+
short = short_try
|
|
56
|
+
option_questions.append(option.replace(short, f"[{short}]", 1))
|
|
57
|
+
option_dict[option] = option
|
|
58
|
+
option_dict[short] = option
|
|
59
|
+
|
|
60
|
+
response = ""
|
|
61
|
+
while response not in option_dict:
|
|
62
|
+
response = input(f"{question} ({'/'.join(option_questions)}): ")
|
|
63
|
+
|
|
64
|
+
return option_dict[response]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def write_file(file: str, content: str, append: bool = False) -> None:
|
|
68
|
+
"""Write to a file. Overrides by default, but can also append"""
|
|
69
|
+
mode = "a" if append else "w"
|
|
70
|
+
try:
|
|
71
|
+
with open(file, mode=mode, encoding="UTF-8") as writer:
|
|
72
|
+
# Add linebreak if using append mode
|
|
73
|
+
if mode == "a":
|
|
74
|
+
writer.write("\n")
|
|
75
|
+
|
|
76
|
+
# Add content to file
|
|
77
|
+
writer.write(content)
|
|
78
|
+
except FileNotFoundError as exc:
|
|
79
|
+
logging.critical("File %s could not be written: %s", file, exc)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def setup_team(
|
|
83
|
+
team_name: str, config_path: str | None = None, file_path: str | None = None
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Set up a new team inside the config dir with a given name"""
|
|
86
|
+
_sanitize_two_exclusive_options(config_path, file_path)
|
|
87
|
+
|
|
88
|
+
# Come up with file name based on team name in the given config directory
|
|
89
|
+
if not file_path:
|
|
90
|
+
# Combine config dir and file name
|
|
91
|
+
file_path = join(config_path, "teams", slugify(team_name) + ".yaml") # type: ignore
|
|
92
|
+
logging.debug("Derived file path: %s", file_path)
|
|
93
|
+
|
|
94
|
+
# Fill template
|
|
95
|
+
yaml_content = _fill_template(TEAM_TEMPLATE, team_name=team_name)
|
|
96
|
+
|
|
97
|
+
# If file already exists, ask if file should be extended or overridden, or abort
|
|
98
|
+
if isfile(file_path):
|
|
99
|
+
options = ("override", "append", "print", "skip")
|
|
100
|
+
action = _ask_user_action(
|
|
101
|
+
f"The file {file_path} exists, what would you like to do?", *options
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
logging.debug("Chosen action: %s", action)
|
|
105
|
+
|
|
106
|
+
if action == "skip":
|
|
107
|
+
print("No action taken")
|
|
108
|
+
elif action == "print":
|
|
109
|
+
print()
|
|
110
|
+
print(yaml_content)
|
|
111
|
+
elif action in ("override", "append"):
|
|
112
|
+
append = action == "append"
|
|
113
|
+
write_file(file=file_path, content=yaml_content, append=append)
|
|
114
|
+
|
|
115
|
+
# File does not exist, write file
|
|
116
|
+
else:
|
|
117
|
+
print(f"Writing team configuration into {file_path}")
|
|
118
|
+
write_file(file=file_path, content=yaml_content)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 DB Systel GmbH
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Manage a GitHub Organization, its teams, repository permissions, and more"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import logging
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from . import __version__, configure_logger
|
|
12
|
+
from ._config import parse_config_files
|
|
13
|
+
from ._gh_org import GHorg
|
|
14
|
+
from ._setup_team import setup_team
|
|
15
|
+
|
|
16
|
+
# Main parser with root-level flags
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--version", action="version", version="GitHub Organization Manager " + __version__
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Initiate first-level subcommands
|
|
25
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands", required=True)
|
|
26
|
+
|
|
27
|
+
# Common flags, usable for all effective subcommands
|
|
28
|
+
common_flags = argparse.ArgumentParser(add_help=False) # No automatic help to avoid duplication
|
|
29
|
+
common_flags.add_argument("--debug", action="store_true", help="Get verbose logging output")
|
|
30
|
+
|
|
31
|
+
# Sync commands
|
|
32
|
+
parser_sync = subparsers.add_parser(
|
|
33
|
+
"sync",
|
|
34
|
+
help="Synchronise GitHub organization settings and teams",
|
|
35
|
+
parents=[common_flags],
|
|
36
|
+
)
|
|
37
|
+
parser_sync.add_argument(
|
|
38
|
+
"-c",
|
|
39
|
+
"--config",
|
|
40
|
+
required=True,
|
|
41
|
+
help="Path to the directory in which the configuration of an GitHub organisation is located",
|
|
42
|
+
)
|
|
43
|
+
parser_sync.add_argument("--dry", action="store_true", help="Do not make any changes at GitHub")
|
|
44
|
+
parser_sync.add_argument(
|
|
45
|
+
"-A",
|
|
46
|
+
"--ignore-archived",
|
|
47
|
+
action="store_true",
|
|
48
|
+
help="Do not take any action in ignored repositories",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Setup Team
|
|
52
|
+
parser_create_team = subparsers.add_parser(
|
|
53
|
+
"setup-team",
|
|
54
|
+
help="Helps with setting up a new team using a base template",
|
|
55
|
+
parents=[common_flags],
|
|
56
|
+
)
|
|
57
|
+
parser_create_team.add_argument(
|
|
58
|
+
"-n",
|
|
59
|
+
"--name",
|
|
60
|
+
required=True,
|
|
61
|
+
help="Name of the team that shall be created",
|
|
62
|
+
)
|
|
63
|
+
parser_create_team_file = parser_create_team.add_mutually_exclusive_group(required=True)
|
|
64
|
+
parser_create_team_file.add_argument(
|
|
65
|
+
"-c",
|
|
66
|
+
"--config",
|
|
67
|
+
help=(
|
|
68
|
+
"Path to the directory in which the configuration of an GitHub organisation is located. "
|
|
69
|
+
"If this option is used, the tool will automatically come up with a file name"
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
parser_create_team_file.add_argument(
|
|
73
|
+
"-f",
|
|
74
|
+
"--file",
|
|
75
|
+
help="Path to the file in which the team shall be added",
|
|
76
|
+
)
|
|
77
|
+
# parser_create_team.add_argument(
|
|
78
|
+
# "-a",
|
|
79
|
+
# "--file-exists-action",
|
|
80
|
+
# help="Define which action shall be taken when the requested output file already exists",
|
|
81
|
+
# choices=["override", "extend", "skip"]
|
|
82
|
+
# )
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main():
|
|
86
|
+
"""Main function"""
|
|
87
|
+
|
|
88
|
+
# Process arguments
|
|
89
|
+
args = parser.parse_args()
|
|
90
|
+
|
|
91
|
+
configure_logger(args.debug)
|
|
92
|
+
|
|
93
|
+
# Sync command
|
|
94
|
+
if args.command == "sync":
|
|
95
|
+
if args.dry:
|
|
96
|
+
logging.info("Dry-run mode activated, will not make any changes at GitHub")
|
|
97
|
+
|
|
98
|
+
org = GHorg()
|
|
99
|
+
|
|
100
|
+
# Parse configuration folder, and do sanity check
|
|
101
|
+
cfg_org, cfg_app, org.configured_teams = parse_config_files(args.config)
|
|
102
|
+
if not cfg_org.get("org_name"):
|
|
103
|
+
logging.critical(
|
|
104
|
+
"No GitHub organisation name configured in organisation settings. Cannot continue"
|
|
105
|
+
)
|
|
106
|
+
sys.exit(1)
|
|
107
|
+
|
|
108
|
+
# Login to GitHub with token, get GitHub organisation
|
|
109
|
+
org.login(cfg_org.get("org_name", ""), cfg_app.get("github_token", ""))
|
|
110
|
+
# Get current rate limit
|
|
111
|
+
org.ratelimit()
|
|
112
|
+
|
|
113
|
+
# Create teams that aren't present at Github yet
|
|
114
|
+
org.create_missing_teams(dry=args.dry)
|
|
115
|
+
# Synchronise the team memberships
|
|
116
|
+
org.sync_teams_members(dry=args.dry)
|
|
117
|
+
# Report about organisation members that do not belong to any team
|
|
118
|
+
org.get_members_without_team()
|
|
119
|
+
# Synchronise the permissions of teams for all repositories
|
|
120
|
+
org.sync_repo_permissions(dry=args.dry, ignore_archived=args.ignore_archived)
|
|
121
|
+
# Remove individual collaborator permissions if they are higher than the one
|
|
122
|
+
# from team membership (or if they are in no configured team at all)
|
|
123
|
+
org.sync_repo_collaborator_permissions(dry=args.dry)
|
|
124
|
+
|
|
125
|
+
# Debug output
|
|
126
|
+
logging.debug("Final dataclass:\n%s", org.df2json())
|
|
127
|
+
org.ratelimit()
|
|
128
|
+
|
|
129
|
+
# Setup Team command
|
|
130
|
+
elif args.command == "setup-team":
|
|
131
|
+
setup_team(team_name=args.name, config_path=args.config, file_path=args.file)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "github-org-manager"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.1"
|
|
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"
|
|
@@ -30,6 +30,7 @@ python = "^3.10"
|
|
|
30
30
|
pygithub = "^2.3.0"
|
|
31
31
|
pyyaml = "^6.0.1"
|
|
32
32
|
requests = "^2.32.3"
|
|
33
|
+
python-slugify = "^8.0.4"
|
|
33
34
|
|
|
34
35
|
[tool.poetry.group.dev.dependencies]
|
|
35
36
|
black = "^24.3.0"
|
|
@@ -38,6 +39,7 @@ mypy = "^1.9.0"
|
|
|
38
39
|
pylint = "^3.1.0"
|
|
39
40
|
types-pyyaml = "^6.0.12.20240311"
|
|
40
41
|
types-requests = "^2.32.0.20240712"
|
|
42
|
+
bump-my-version = "^0.26.0"
|
|
41
43
|
|
|
42
44
|
[build-system]
|
|
43
45
|
requires = ["poetry-core"]
|
|
@@ -58,3 +60,16 @@ files = ["gh_org_mgr/*.py"]
|
|
|
58
60
|
# Pylint
|
|
59
61
|
[tool.pylint.'MESSAGES CONTROL']
|
|
60
62
|
disable = "fixme"
|
|
63
|
+
|
|
64
|
+
# Bump-My-Version
|
|
65
|
+
[tool.bumpversion]
|
|
66
|
+
commit = true
|
|
67
|
+
tag = true
|
|
68
|
+
allow_dirty = false
|
|
69
|
+
tag_name = "v{new_version}"
|
|
70
|
+
|
|
71
|
+
[[tool.bumpversion.files]]
|
|
72
|
+
filename = "pyproject.toml"
|
|
73
|
+
regex = true
|
|
74
|
+
search = "^version = \"{current_version}\""
|
|
75
|
+
replace = "version = \"{new_version}\""
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: 2024 DB Systel GmbH
|
|
2
|
-
#
|
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
|
|
5
|
-
"""Manage a GitHub Organization, its teams, repository permissions, and more"""
|
|
6
|
-
|
|
7
|
-
import argparse
|
|
8
|
-
import logging
|
|
9
|
-
import sys
|
|
10
|
-
|
|
11
|
-
from . import __version__, configure_logger
|
|
12
|
-
from ._config import parse_config_files
|
|
13
|
-
from ._gh_org import GHorg
|
|
14
|
-
|
|
15
|
-
parser = argparse.ArgumentParser(
|
|
16
|
-
description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
17
|
-
)
|
|
18
|
-
parser.add_argument(
|
|
19
|
-
"-c",
|
|
20
|
-
"--config",
|
|
21
|
-
required=True,
|
|
22
|
-
help="Path to the directory in which the configuration of an GitHub organisation is located",
|
|
23
|
-
)
|
|
24
|
-
parser.add_argument("--debug", action="store_true", help="Get verbose logging output")
|
|
25
|
-
parser.add_argument("--dry", action="store_true", help="Do not make any changes at GitHub")
|
|
26
|
-
parser.add_argument(
|
|
27
|
-
"-A",
|
|
28
|
-
"--ignore-archived",
|
|
29
|
-
action="store_true",
|
|
30
|
-
help="Do not take any action in ignored repositories",
|
|
31
|
-
)
|
|
32
|
-
parser.add_argument("--version", action="version", version="GitHub Team Manager " + __version__)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def main():
|
|
36
|
-
"""Main function"""
|
|
37
|
-
|
|
38
|
-
# Process arguments
|
|
39
|
-
args = parser.parse_args()
|
|
40
|
-
|
|
41
|
-
configure_logger(args.debug)
|
|
42
|
-
|
|
43
|
-
if args.dry:
|
|
44
|
-
logging.info("Dry-run mode activated, will not make any changes at GitHub")
|
|
45
|
-
|
|
46
|
-
org = GHorg()
|
|
47
|
-
|
|
48
|
-
# Parse configuration folder, and do sanity check
|
|
49
|
-
cfg_org, cfg_app, org.configured_teams = parse_config_files(args.config)
|
|
50
|
-
if not cfg_org.get("org_name"):
|
|
51
|
-
logging.critical(
|
|
52
|
-
"No GitHub organisation name configured in organisation settings. Cannot continue"
|
|
53
|
-
)
|
|
54
|
-
sys.exit(1)
|
|
55
|
-
|
|
56
|
-
# Login to GitHub with token, get GitHub organisation
|
|
57
|
-
org.login(cfg_org.get("org_name", ""), cfg_app.get("github_token", ""))
|
|
58
|
-
# Get current rate limit
|
|
59
|
-
org.ratelimit()
|
|
60
|
-
|
|
61
|
-
# Create teams that aren't present at Github yet
|
|
62
|
-
org.create_missing_teams(dry=args.dry)
|
|
63
|
-
# Synchronise the team memberships
|
|
64
|
-
org.sync_teams_members(dry=args.dry)
|
|
65
|
-
# Report about organisation members that do not belong to any team
|
|
66
|
-
org.get_members_without_team()
|
|
67
|
-
# Synchronise the permissions of teams for all repositories
|
|
68
|
-
org.sync_repo_permissions(dry=args.dry, ignore_archived=args.ignore_archived)
|
|
69
|
-
# Remove individual collaborator permissions if they are higher than the one
|
|
70
|
-
# from team membership (or if they are in no configured team at all)
|
|
71
|
-
org.sync_repo_collaborator_permissions(dry=args.dry)
|
|
72
|
-
|
|
73
|
-
# Debug output
|
|
74
|
-
logging.debug("Final dataclass:\n%s", org.df2json())
|
|
75
|
-
org.ratelimit()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|