Skip to content

Commit 49d1f41

Browse files
committed
CBD-5212: Add github group replacer
Change-Id: I4d922e966bd53ff9f92f8d4ed3a196ce9cf90e34 Reviewed-on: https://review.couchbase.org/c/build-tools/+/183134 Tested-by: Blair Watt <[email protected]> Reviewed-by: Ming Ho <[email protected]>
1 parent fd4c9f8 commit 49d1f41

File tree

3 files changed

+191
-0
lines changed

3 files changed

+191
-0
lines changed

github/group-replacer/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Github group replacer
2+
3+
The purpose of this helper script is to replace an existing 'from' team with
4+
a given 'to' team for all repositories in a specified GitHub organisation.
5+
6+
It requires an access token with repo read/write access, present in the
7+
environment as GITHUB_TOKEN, once present, run with
8+
9+
./app.py --org [org] --from-team-slug=[from] --to-team-slug=[to] [--dry-run]

github/group-replacer/app.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env python3
2+
3+
"""This script is used to switch out one team for another across all the
4+
repositories in a given Github organisation.
5+
6+
Note: Only the source team's permissions are migrated (members are not added
7+
or removed) and the destination team must already exist
8+
"""
9+
10+
import argparse
11+
import sys
12+
from github import Github
13+
from os import environ
14+
15+
16+
class GitHubTeamMover():
17+
def __init__(self, org, dry_run):
18+
self.connect()
19+
self.get_org(org)
20+
self.dry_run = dry_run
21+
22+
def connect(self):
23+
try:
24+
self.g = Github(environ["GITHUB_TOKEN"])
25+
except KeyError:
26+
print("ERROR: Ensure GITHUB_TOKEN is present in environment")
27+
exit(1)
28+
29+
def get_org(self, org):
30+
self.org = self.g.get_organization(org)
31+
32+
def get_repos_team_present_in(self, team_slug):
33+
repos = []
34+
for repo in self.org.get_repos():
35+
if(team_slug in [t.slug for t in repo.get_teams()]):
36+
repos.append(repo)
37+
return repos
38+
39+
def remove_from_repo(self, team_slug, repo):
40+
team = self.org.get_team_by_slug(team_slug)
41+
print("Removing", team.slug, "from", repo.name, "...", end=" ")
42+
if not self.dry_run:
43+
team.remove_from_repos(repo)
44+
print("ok!")
45+
else:
46+
print("no action (dry run)")
47+
48+
def add_to_repo(self, team_slug, repo, role):
49+
team = self.org.get_team_by_slug(team_slug)
50+
print("Adding", team.slug, "to", repo.name,
51+
f"({role} access) ...", end=" ")
52+
if not self.dry_run:
53+
team.add_to_repos(repo)
54+
self.set_repo_role(team, repo, role)
55+
print("ok!")
56+
else:
57+
print("no action (dry run)")
58+
59+
def get_repo_role(self, team_slug, repo):
60+
team = self.org.get_team_by_slug(team_slug)
61+
permission = team.get_repo_permission(repo)
62+
if permission.admin:
63+
role = "admin"
64+
elif permission.maintain:
65+
role = "maintain"
66+
elif permission.push:
67+
role = "push" # this is called 'write' in UI
68+
elif permission.triage:
69+
role = "triage"
70+
elif permission.pull:
71+
role = "read"
72+
else:
73+
print(
74+
f"Couldn't determine permission for {team_slug} on ",
75+
repo.full_name)
76+
sys.exit(1)
77+
return role
78+
79+
def set_repo_role(self, team, repo, role):
80+
return team.update_team_repository(repo, role)
81+
82+
def switch_teams(self, from_team, to_team):
83+
repos_present_in = self.get_repos_team_present_in(from_team)
84+
for repo in repos_present_in:
85+
role = self.get_repo_role(from_team, repo.full_name)
86+
self.add_to_repo(to_team, repo, role)
87+
self.remove_from_repo(from_team, repo)
88+
89+
90+
parser = argparse.ArgumentParser(description=__doc__,
91+
formatter_class=argparse.RawTextHelpFormatter)
92+
parser.add_argument('--org', action='store', required=True)
93+
parser.add_argument('--from-team-slug', action='store', required=True)
94+
parser.add_argument('--to-team-slug', action='store', required=True)
95+
parser.add_argument('--dry-run', action='store_true')
96+
args = parser.parse_args()
97+
98+
99+
team = GitHubTeamMover(args.org, args.dry_run)
100+
team.switch_teams(args.from_team_slug, args.to_team_slug)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
amqp==5.0.6
2+
appdirs==1.4.4
3+
astroid==2.11.6
4+
attrs==21.4.0
5+
autopep8==1.5.6
6+
billiard==3.6.4.0
7+
boltons==21.0.0
8+
bracex==2.3.post1
9+
celery==5.1.2
10+
certifi==2021.10.8
11+
cffi==1.15.0
12+
charset-normalizer==2.0.12
13+
click==8.1.3
14+
click-didyoumean==0.0.3
15+
click-option-group==0.5.3
16+
click-plugins==1.1.1
17+
click-repl==0.2.0
18+
colorama==0.4.5
19+
coverage==5.5
20+
dd-import==1.0.6
21+
defusedxml==0.7.1
22+
Deprecated==1.2.13
23+
dill==0.3.5.1
24+
distlib==0.3.1
25+
face==20.1.1
26+
filelock==3.0.12
27+
Flask==2.0.1
28+
Flask-Cors==3.0.10
29+
glom==22.1.0
30+
idna==3.3
31+
importlib-metadata==4.11.3
32+
iniconfig==1.1.1
33+
isort==5.10.1
34+
itsdangerous==2.0.1
35+
Jinja2==3.0.1
36+
jsonschema==4.7.2
37+
kombu==5.1.0
38+
lazy-object-proxy==1.7.1
39+
Mako==1.2.0
40+
Markdown==3.3.7
41+
MarkupSafe==2.0.1
42+
mccabe==0.7.0
43+
packaging==21.0
44+
pdoc3==0.10.0
45+
peewee==3.15.1
46+
platformdirs==2.5.2
47+
pluggy==1.0.0
48+
prompt-toolkit==3.0.19
49+
py==1.10.0
50+
pycodestyle==2.7.0
51+
pycparser==2.21
52+
PyGithub==1.55
53+
PyJWT==2.3.0
54+
pylint==2.14.4
55+
PyNaCl==1.5.0
56+
pyparsing==2.4.7
57+
pyrsistent==0.18.1
58+
pytest==6.2.5
59+
pytest-cov==2.12.1
60+
python-lsp-jsonrpc==1.0.0
61+
pytz==2021.1
62+
PyYAML==5.4.1
63+
redis==3.5.3
64+
requests==2.27.1
65+
ruamel.yaml==0.17.21
66+
ruamel.yaml.clib==0.2.6
67+
semgrep==0.104.0
68+
six==1.15.0
69+
toml==0.10.2
70+
tomli==2.0.1
71+
tomlkit==0.11.1
72+
tqdm==4.64.0
73+
typing_extensions==4.3.0
74+
ujson==5.4.0
75+
urllib3==1.26.8
76+
vine==5.0.0
77+
virtualenv==20.4.4
78+
wcmatch==8.4
79+
wcwidth==0.2.5
80+
Werkzeug==2.0.1
81+
wrapt==1.13.3
82+
zipp==3.8.0

0 commit comments

Comments
 (0)