From c4d757da4e4e40b532e354e7d178bc3617a92121 Mon Sep 17 00:00:00 2001 From: David Moreau Simard Date: Wed, 20 Mar 2019 11:04:02 -0400 Subject: [PATCH] Add script to automate GitHub organization transfers This script requires GITHUB_USERNAME and the GITHUB_PASSWORD env variables to be set and lets users with sufficient privileges initiate a transfer from a GitHub organization to another by specifying two arguments, for example: ./github-org-transfer.py oldorg/repo neworg/repo Change-Id: I2383d256958c028efe81b235ff8641d131bbb3a7 --- tools/github-org-transfer.py | 130 +++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100755 tools/github-org-transfer.py diff --git a/tools/github-org-transfer.py b/tools/github-org-transfer.py new file mode 100755 index 0000000000..d3142d7446 --- /dev/null +++ b/tools/github-org-transfer.py @@ -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()