Merge "Add script to automate GitHub organization transfers"
This commit is contained in:
commit
c01a9eeccb
130
tools/github-org-transfer.py
Executable file
130
tools/github-org-transfer.py
Executable file
@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright 2019 Red Hat
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
# Script that automates the transfer of a repository from an organization to
|
||||
# another on GitHub.
|
||||
# Relevant docs:
|
||||
# - https://developer.github.com/v3/orgs/members/#get-your-organization-membership
|
||||
# - https://developer.github.com/v3/repos/#get
|
||||
# - https://developer.github.com/v3/repos/#transfer-a-repository
|
||||
|
||||
import argparse
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
GITHUB_API = "https://api.github.com"
|
||||
GITHUB_USERNAME = os.environ.get("GITHUB_USERNAME", None)
|
||||
|
||||
# The password can be the account's password or a personal access token created
|
||||
# at https://github.com/settings/tokens
|
||||
# TODO: Add support for two factor authentication by passing the "x-github-otp"
|
||||
# header. See: https://developer.github.com/v3/auth/#working-with-two-factor-authentication
|
||||
GITHUB_PASSWORD = os.environ.get("GITHUB_PASSWORD", None)
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("src_repo", help="Source org and repo (ex: oldorg/repo)")
|
||||
parser.add_argument("dst_repo", help="Destination org and repo (ex: neworg/repo)")
|
||||
parser.add_argument("--dry-run", action="store_true",
|
||||
help="Check repositories and privileges but don't intiate any transfers."
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def is_organization_admin(session, repository):
|
||||
"""
|
||||
Returns true if the current user has admin privileges for the repository's
|
||||
organization.
|
||||
"""
|
||||
org = repository.split("/")[0]
|
||||
memberships = session.get("%s/user/memberships/orgs/%s" % (GITHUB_API, org))
|
||||
if memberships.status_code == 404:
|
||||
return False
|
||||
elif memberships.status_code != 200:
|
||||
raise Exception("Could not retrieve organization memberships: %s" % json.dumps(memberships.json(), indent=2))
|
||||
|
||||
role = memberships.json()["role"]
|
||||
if role == "admin":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
|
||||
if GITHUB_USERNAME is None or GITHUB_PASSWORD is None:
|
||||
raise Exception("Environment variables GITHUB_USERNAME and GITHUB_PASSWORD must be set.")
|
||||
|
||||
session = requests.Session()
|
||||
session.auth = (GITHUB_USERNAME, GITHUB_PASSWORD)
|
||||
session.headers.update({
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/vnd.github.nightshade-preview+json"
|
||||
})
|
||||
|
||||
# Ensure we have sufficient privileges to initiate the transfer
|
||||
for repository in [args.src_repo, args.dst_repo]:
|
||||
if not is_organization_admin(session, repository):
|
||||
raise Exception("Insufficient privileges for %s or it is a personal namespace" % repository)
|
||||
|
||||
# Get source repository
|
||||
src_repo = session.get("%s/repos/%s" % (GITHUB_API, args.src_repo))
|
||||
# The source repository should exist. Raise an exception if it doesn't.
|
||||
src_repo.raise_for_status()
|
||||
src_repo = src_repo.json()
|
||||
|
||||
# Check if the provided source repo name matches the GitHub repo name.
|
||||
# This is important because GitHub will have a different "full_name" if a
|
||||
# repo has been moved in the past. For example, if "oldorg/repo" had been
|
||||
# moved to "neworg/repo" in the past, querying "oldorg/repo" would yield
|
||||
# the full_name "neworg/repo" instead of the expected "oldorg/repo".
|
||||
if args.src_repo != src_repo["full_name"]:
|
||||
if src_repo["full_name"] == args.dst_repo:
|
||||
print("Nothing to do: repository has already been moved.")
|
||||
sys.exit(0)
|
||||
else:
|
||||
raise Exception("Source repository exists but as %s" % src_repo["full_name"])
|
||||
|
||||
# Get destination repository
|
||||
dst_repo = session.get("%s/repos/%s" % (GITHUB_API, args.dst_repo))
|
||||
# The destination repository shouldn't exist. If it does, try to be helpful about it.
|
||||
if dst_repo.status_code == 200:
|
||||
dst_repo = dst_repo.json()
|
||||
if dst_repo["full_name"] == args.dst_repo:
|
||||
print("Nothing to do: repository has already been moved.")
|
||||
sys.exit(0)
|
||||
else:
|
||||
raise Exception("Destination repository exists but as %s" % dst_repo["full_name"])
|
||||
|
||||
# Initiate transfer request
|
||||
payload = {
|
||||
"new_owner": args.dst_repo.split('/')[0]
|
||||
}
|
||||
|
||||
if not args.dry_run:
|
||||
data = session.post("%s/repos/%s/transfer" % (GITHUB_API, args.src_repo), data=json.dumps(payload))
|
||||
if data.status_code != 202:
|
||||
raise Exception("Failed to request transfer: %s" % json.dumps(data.json(), indent=2))
|
||||
print("Sent transfer request for %s to %s." % (args.src_repo, args.dst_repo))
|
||||
else:
|
||||
print("Not requesting transfer (dry-run enabled)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Reference in New Issue
Block a user