#!/usr/bin/env python3 # To get the user token import os # To interact with github import github from github import Github from github.GithubException import GithubException from github.Organization import Organization from github import UnknownObjectException # To modify the git repository using sane tools from git import Repo import yaml from jinja2 import Environment, FileSystemLoader from pathlib import Path import argparse # We need to screen scrape from selenium import webdriver from selenium.common.exceptions import NoSuchElementException import chromedriver_binary # Adds chromedriver binary to path from tqdm import tqdm import os import pickle from pathlib import Path from time import sleep from time import sleep import requests def load_cookie(driver, path): with open(path, 'rb') as cookiesfile: cookies = pickle.load(cookiesfile) for cookie in cookies: driver.add_cookie(cookie) def enable_repo_on_shippable(token_dir='~/.archiconda/', org_name='Archiconda', repository_names=None): if repository_names is None: return cookie_filename = (Path(token_dir) / 'chromecookies.pickle').expanduser() driver = webdriver.Chrome() # Not really true, but a maybe we are logged in logged_in = True # We want to load the cookies for github and shippable driver.get('https://github.com') driver.get('https://shippable.com') if cookie_filename.exists(): load_cookie(driver, cookie_filename) driver.get(f'https://app.shippable.com/subs/github/{org_name}/enable') # wait for rendering for i in tqdm(range(20), desc='Page rendering'): sleep(2/20) # Try to find the refresh button, it will exist if the user is logged in try: button = driver.find_element_by_css_selector( '#wrapper > div.content-page > div > div > ui-view > div > div.panel.panel-default.panel-border > div.panel-body > div:nth-child(1) > div > div > div > button') except NoSuchElementException as e: print("Unfortunately, the Shippable API doesn't allow free users to automatically enable repos.") print("Therefore, we are resorting to screen-scraping.") print('You must login to Shippable with your GitHub account.') print('Please loging then press enter to continue.') input('') with open(cookie_filename, 'wb+') as f: print('saving') pickle.dump(driver.get_cookies(), f) driver.get(f'https://app.shippable.com/subs/github/{org_name}/enable') # wait for rendering for i in tqdm(range(20), desc='Page rendering'): sleep(2/20) button = driver.find_element_by_css_selector( '#wrapper > div.content-page > div > div > ui-view > div > div.panel.panel-default.panel-border > div.panel-body > div:nth-child(1) > div > div > div > button') button.click() for i in tqdm(range(30), desc='Synchornizing SCMs'): sleep(10/20) search_input = driver.find_element_by_css_selector( '#completedJobs > div > div.row > div > div > input') for repo_name in repository_names: search_input.clear() search_input.send_keys(repo_name) sleep(1) try: reponame_disabled = driver.find_element_by_css_selector( '#completedJobs > div > div.table-responsive > table > tbody > tr.ng-scope > td:nth-child(1) > span') enable_button = driver.find_element_by_css_selector( '#completedJobs > div > div.table-responsive > table > tbody > tr.ng-scope > td:nth-child(2) > span') enable_button.click() print(f'Enabling repo: "{repo_name}"') sleep(1) except NoSuchElementException as e: reponame = driver.find_element_by_css_selector( '#completedJobs > div > div.table-responsive > table > tbody > tr.ng-scope > td:nth-child(1) > a') print(f'repo "{repo_name}" already enabled ') search_input.clear() for i in tqdm(range(15), desc=f'Waiting for repos to get enabled'): sleep(10/20) def create_aarch64_branch(repo, aarch64_default=True): source_branch = 'master' target_branch = 'aarch64' print(f'Creating branch "{target_branch}" from "{source_branch}"') try: repo.get_branch(target_branch) except GithubException as e: if e.status == 404: sb = repo.get_branch(source_branch) repo.create_git_ref(ref='refs/heads/' + target_branch, sha=sb.commit.sha) repo.get_branch(target_branch) if aarch64_default: print(f"setting fork's default branch to {target_branch}") repo.edit(default_branch=target_branch) def fork_repo(gh, *, org, package_name, source_org): repo_full_name = f'{org.login}/{package_name}-feedstock' forked_repo = gh.get_repo(repo_full_name) print(f'Checking to see if {repo_full_name} exists on Github') try: # Check that the repo actually exists # Printing the name or any property of the repo issues this check print(f'{forked_repo.full_name} already exists, not forking it again.') return forked_repo except UnknownObjectException: print(f'{repo_full_name} does not exists on Github, will fork') pass # Else, now try to fork it from the origin feedstock_repo = gh.get_repo(f'{source_org}/{package_name}-feedstock') try: org.create_fork(feedstock_repo) except UnknownObjectException as e: if e.status == 404: raise RuntimeError(f'Repository not found: {e.data["message"]}') else: raise e def get_github_token(token_dir): try: github_token_filename = (token_dir / 'github.token').expanduser() with open(github_token_filename, 'r') as fh: github_token = fh.read().strip() if not github_token: raise ValueError() return github_token except (IOError, ValueError): raise RuntimeError( 'No github token found for archiconda on Github. \n' 'Go to https://github.com/settings/tokens/new and generate\n' f'a token with repo access. Put it in {github_token_filename}') def get_shippable_token(token_dir): try: shippable_token_filename = (token_dir / 'shippable.token').expanduser() with open(shippable_token_filename, 'r') as fh: shippable_token = fh.read().strip() if not shippable_token: raise ValueError() return shippable_token except (IOError, ValueError): raise RuntimeError( 'No shippable token found for archiconda on shippable. \n' 'Go to http://docs.shippable.com/platform/api/api-tokens/\n' 'and follow the instructions to get a token.\n' f'Put it in {shippable_token_filename}') def get_shippable_project_id(token, project_full_name): params = {'sortBy': 'createdAt', 'sortOrder': '-1', 'projectFullNames': project_full_name} headers = {'Authorization': 'apiToken {}'.format(token)} url = 'https://api.shippable.com/projects' result = requests.get(url=url, params=params, headers=headers) n_results = len(result.json()) if not n_results: raise RuntimeError('Could not find the activated repository. Make sure it exists on shippable.') elif n_results > 1: raise RuntimeError("Found multiple repositories, linking to the first one. This really shouldn't happen") # projectId seems like a short name # the real variable we need is id return result.json()[0]['id'] def main(package_names, source_org, org_name, token_dir, aarch64_default): print(f'Will fork the following feedstocks {package_names}') print(f'from: {source_org}') print(f'to: {org_name}') print(f'using tokens found in: {token_dir}') github_token = get_github_token(token_dir) gh = Github(github_token) org = gh.get_user() if org.login != org_name: org = gh.get_organization(org_name) feedstock_names = [f'{package_name}-feedstock' for package_name in package_names] repo_full_names = [f'{org.login}/{feedstock_name}' for feedstock_name in feedstock_names] for package_name in package_names: fork_repo(gh, org=org, package_name=package_name, source_org=source_org) # Github might take a bit of time create the repositories. sleep(1) for repo_full_name in repo_full_names: forked_repo = gh.get_repo(repo_full_name) create_aarch64_branch(forked_repo, aarch64_default=aarch64_default) enable_repo_on_shippable(token_dir, org_name=org_name, repository_names=[f'{p}-feedstock' for p in package_names]) shippable_token = get_shippable_token(token_dir) for package_name in package_names: feedstock_name = f'{package_name}-feedstock' full_repo_name = f'{org.login}/{feedstock_name}' forked_repo = gh.get_repo(f'{org.login}/{feedstock_name}') repo = Repo.clone_from(forked_repo.ssh_url, f'{feedstock_name}') repo.git.checkout('-B', 'aarch64', 'origin/aarch64') feedstock_repo = gh.get_repo(f'{source_org}/{package_name}-feedstock') repo.create_remote('upstream', feedstock_repo.ssh_url) repo.remotes['upstream'].pull('master') with open(f'{feedstock_name}/conda-forge.yml', 'r') as f: y = yaml.load(f) projectId = get_shippable_project_id(shippable_token, full_repo_name) shippable_dict = { 'projectId': str(projectId), 'secure': { 'BINSTAR_TOKEN': 'bkTdATvev7sVFsP62xFV2ck215nXEtH7eWXdhzRRtbzeKquSkNhTGTCoa5FcLDvAVe36w+Sv59/3/oWNyMood8pIWjHLMC5CqqLdc4NRmyyaCKWys4CLhTTurIBPFSWUilxZW1KCKv/WHOe+zQDi2o9R9lf5/MizuwThHSQOIcqeTIn4wtPzbne5MeKSW+mRCsb+l4E/Q1oY2w/mTJ+izDWkxefstZ2t8RqOxH6H20wwNOOj/1WdeztdCOtCAl99r8Aj58odGyfUMAEyw89c5HglAEPurBQs21DZbHp10NmgSLyIbukplulRUm+cQ37loT/hFfTjPUCqLEC3lu6SPw=='}} y['shippable'] = shippable_dict if 'compiler_stack' not in y: print(f'We added the compiler stack to {feedstock_name}') # add the compiler stack all the time because i'm lazy y['compiler_stack'] = 'comp7' y['max_py_ver'] = '37' y['max_r_ver'] = '35' with open(f'{feedstock_name}/conda-forge.yml', 'w') as f: f.write(yaml.dump(y)) repo.index.add(['conda-forge.yml']) repo.index.commit('added shippable secure BINSTAR_TOKEN') origin = repo.remotes['origin'] print('Added the tag shippable secret and projectId, repo ready for rerendering.') # lazy way to take off noarch # can't really use yaml since there is a bunch of jinja syntax with open(f'{feedstock_name}/recipe/meta.yaml', 'r') as f: all_lines = f.readlines() try: noarch_index = [line.strip().startswith('noarch: python') for line in all_lines].index(True) print('found noarch python. removing it') with open(f'{feedstock_name}/recipe/meta.yaml', 'w') as f: f.writelines(all_lines[:noarch_index]) f.writelines(all_lines[noarch_index+1:]) repo.index.add(['recipe/meta.yaml']) repo.index.commit('removed noarch: python') except ValueError: pass import subprocess as sp sp.run(['conda', 'smithy', 'rerender', '--no-check-uptodate', '-e', '../conda_build_config.yaml', '--feedstock_directory', feedstock_name]) repo.index.commit('Rerendered for shippable (aarch64)') origin.push() def render_shippable(forge_dir): content_dir = os.path.abspath(os.path.dirname(__file__)) env = Environment(loader=FileSystemLoader([content_dir])) template = env.get_template('shippable.yml.tmpl') p = Path(forge_dir) / '.ci_support' config = {'configs': [(f.stem, None) for f in p.glob('linux*') if 'toolchain' not in f.stem]} shippable_config_filename = Path(forge_dir) / 'shippable.yml' with open(shippable_config_filename, 'w') as f: f.write(template.render(config)) return shippable_config_filename if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--source_org', type=str, default='conda-forge', help='The source organization to clone from') parser.add_argument('--org', type=str, default='Archiconda', help='The destimation organization (or user) where to fork to.') parser.add_argument('--token-dir', type=Path, default=Path('~/.archiconda'), help='The directory where to find the tokens.') parser.add_argument('package_names', nargs='+', type=str, help='packages to fork and create') parser.add_argument('--no-change-master-branch', dest='aarch64_default', action='store_false', help='If provided, the master branch of the repo will be left unchanged.') parser.set_defaults(aarch64_default=True) args = parser.parse_args() package_names = args.package_names source_org = args.source_org org_name = args.org token_dir = args.token_dir aarch64_default = args.aarch64_default main(package_names, source_org, org_name, token_dir, aarch64_default)