diff --git a/doctr/__main__.py b/doctr/__main__.py index 87b7782a..bf38b131 100644 --- a/doctr/__main__.py +++ b/doctr/__main__.py @@ -9,7 +9,8 @@ on your local machine. This will prompt for your GitHub credentials and the name of the repo you want to deploy docs for. This will generate a secure key, -which you should insert into your .travis.yml. +which you should insert into your .travis.yml (or set as a secure environment +variable in your TravisCI repository settings if using the --dkenv option). Then, on Travis, for the build where you build your docs, add:: @@ -134,6 +135,10 @@ def get_parser(config=None): if we do not appear to be on Travis.""") deploy_parser_add_argument('deploy_directory', type=str, nargs='?', help="""Directory to deploy the html documentation to on gh-pages.""") + deploy_parser_add_argument('--dkenv', type=str, metavar="ENVVAR", + help="""Push to GitHub using a deployment key stored in the named + environment variable via your TravisCI repository settings. + Use this if you used 'doctr configure --dkenv ENVVAR'.""") deploy_parser_add_argument('--token', action='store_true', default=False, help="""Push to GitHub using a personal access token. Use this if you used 'doctr configure --token'.""") @@ -193,6 +198,11 @@ def get_parser(config=None): configure_parser.set_defaults(func=configure) configure_parser.add_argument('--force', action='store_true', help="""Run the configure command even if we appear to be on Travis.""") + configure_parser.add_argument('--dkenv', type=str, metavar="ENVVAR", + help="""Generate a deployment key to push to GitHub. The public key + will be added to your GitHub repository settings. The private key + should be added to you TravisCI repository settings as a protected + environment variable.""") configure_parser.add_argument('--token', action="store_true", default=False, help="""Generate a personal access token to push to GitHub. The default is to use a deploy key. WARNING: This will grant read/write access to all the @@ -275,6 +285,9 @@ def deploy(args, parser): config = get_config() + if args.token and args.dkenv: + parser.error("The --token and --dkenv settings are incompatible.") + if args.tmp_dir: parser.error("The --tmp-dir flag has been removed (doctr no longer uses a temporary directory when deploying).") @@ -310,10 +323,10 @@ def deploy(args, parser): canpush = setup_GitHub_push(deploy_repo, deploy_branch=deploy_branch, auth_type='token' if args.token else 'deploy_key', - full_key_path=keypath, + full_key_path=None if args.dkenv else keypath, branch_whitelist=branch_whitelist, build_tags=args.build_tags, - env_name=env_name) + env_name=args.dkenv if args.dkenv else env_name) if args.sync: built_docs = args.built_docs or find_sphinx_build_dir() @@ -383,6 +396,13 @@ def configure(args, parser): parser.error(red("doctr appears to be running on Travis. Use " "doctr configure --force to run anyway.")) + if args.token and args.dkenv: + parser.error("The --token and --dkenv settings are incompatible.") + + if len(args.dkenv.split()) != 1: + # Not going to repeat this sanity test in the deploy command: + parser.error("The --dkenv setting should be one word only, e.g. DOC_KEY.") + if not args.authenticate: args.upload_key = False @@ -484,11 +504,17 @@ def configure(args, parser): deploy_key_repo, env_name, keypath = get_deploy_key_repo(deploy_repo, args.key_path) private_ssh_key, public_ssh_key = generate_ssh_key() - key = encrypt_to_file(private_ssh_key, keypath + '.enc') - del private_ssh_key # Prevent accidental use below + if args.dkenv: + key = None # don't need it on disk + encrypted_variable = None # not applicable + private_ssh_key = private_ssh_key.decode('ASCII') # Will print this later! + else: + key = encrypt_to_file(private_ssh_key, keypath + '.enc') + encrypted_variable = encrypt_variable(env_name.encode('utf-8') + b"=" + key, + build_repo=build_repo, tld=tld, + travis_token=travis_token, **login_kwargs) + private_ssh_key = None # Prevent accidental use below public_ssh_key = public_ssh_key.decode('ASCII') - encrypted_variable = encrypt_variable(env_name.encode('utf-8') + b"=" + key, - build_repo=build_repo, tld=tld, travis_token=travis_token, **login_kwargs) deploy_keys_url = 'https://github.com/{deploy_repo}/settings/keys'.format(deploy_repo=deploy_key_repo) @@ -509,16 +535,17 @@ def configure(args, parser): and add the following as a new key:{RESET} {ssh_key} + {BOLD_MAGENTA}Be sure to allow write access for the key.{RESET} """.format(ssh_key=public_ssh_key, deploy_keys_url=deploy_keys_url, N=N, BOLD_MAGENTA=BOLD_MAGENTA, RESET=RESET))) + if not args.dkenv: + print(dedent("""\ + {N}. {BOLD_MAGENTA}Add the file {keypath}.enc to be staged for commit:{RESET} - print(dedent("""\ - {N}. {BOLD_MAGENTA}Add the file {keypath}.enc to be staged for commit:{RESET} - - git add {keypath}.enc - """.format(keypath=keypath, N=N, BOLD_MAGENTA=BOLD_MAGENTA, RESET=RESET))) + git add {keypath}.enc + """.format(keypath=keypath, N=N, BOLD_MAGENTA=BOLD_MAGENTA, RESET=RESET))) options = '--built-docs ' + bold_black('') if args.key_path: @@ -531,23 +558,36 @@ def configure(args, parser): options += ' --token' key_type = "personal access token" + if args.dkenv: + options += ' --dkenv ' + args.dkenv + print(dedent("""\ + {N}. {BOLD_MAGENTA}Add the following private deployment key to your TravisCI + repository settings as environment variable {env_name}:{RESET} + """.format(N=N, BOLD_MAGENTA=BOLD_MAGENTA, RESET=RESET, + env_name=args.dkenv, private_ssh_key=private_ssh_key))) + print(private_ssh_key) + print(dedent("""\ {N}. {BOLD_MAGENTA}Add these lines to your `.travis.yml` file:{RESET} + """.format(N=N, BOLD_MAGENTA=BOLD_MAGENTA, RESET=RESET))) - env: - global: - # Doctr {key_type} for {deploy_repo} - - secure: "{encrypted_variable}" + if not args.dkenv: + print(dedent("""\ + env: + global: + # Doctr {key_type} for {deploy_repo} + - secure: "{encrypted_variable}" + """.format(key_type=key_type, + encrypted_variable=encrypted_variable.decode('utf-8')))) + print(dedent("""\ script: - set -e - {BOLD_BLACK}{RESET} - pip install doctr - doctr deploy {options} {BOLD_BLACK}{RESET} - """.format(options=options, N=N, key_type=key_type, - encrypted_variable=encrypted_variable.decode('utf-8'), - deploy_repo=deploy_repo, BOLD_MAGENTA=BOLD_MAGENTA, - BOLD_BLACK=BOLD_BLACK, RESET=RESET))) + """.format(options=options, deploy_repo=deploy_repo, + BOLD_BLACK=BOLD_BLACK, RESET=RESET))) print(dedent("""\ Replace the text in {BOLD_BLACK}{RESET} with the relevant diff --git a/doctr/travis.py b/doctr/travis.py index 0f15eca5..76389650 100644 --- a/doctr/travis.py +++ b/doctr/travis.py @@ -20,6 +20,20 @@ from .common import red, blue, yellow DOCTR_WORKING_BRANCH = '__doctr_working_branch' +def write_private_key(filename, private_key_bytes): + """ + Helper function to record (decrpted) private key to as file for ssh. + """ + with open(filename, 'wb') as f: + f.write(filename) + os.chmod(filename, 0o600) + +def write_key_from_env_var(filename, env_var): + """ + Write private key from environment variable named in --dkenv to disk. + """ + write_private_key(filename, os.environ[env_var]) + def decrypt_file(file, key): """ Decrypts the file ``file``. @@ -41,10 +55,7 @@ def decrypt_file(file, key): with open(file, 'rb') as f: decrypted_file = fer.decrypt(f.read()) - with open(file[:-4], 'wb') as f: - f.write(decrypted_file) - - os.chmod(file[:-4], 0o600) + write_private_key(file[:-4], decrypted_file) def setup_deploy_key(keypath='github_deploy_key', key_ext='.enc', env_name='DOCTR_DEPLOY_ENCRYPTION_KEY'): """ @@ -165,8 +176,13 @@ def get_current_repo(): 'remote.origin.url']).decode('utf-8') # Travis uses the https clone url - _, org, git_repo = remote_url.rsplit('.git', 1)[0].rsplit('/', 2) - return (org + '/' + git_repo) + # e.g. https://github.com//.git + # If run outside Travis using --force, might have: + # e.g. git@github.com:/.git + if remote_url.endswith(".git"): + remote_url = remote_url[:-4] + _, owner, repo = remote_url.replace(":", "/").rsplit("/", 2) + return owner + '/' + repo def get_travis_branch(): """Get the name of the branch that the PR is from. @@ -189,12 +205,22 @@ def setup_GitHub_push(deploy_repo, *, auth_type='deploy_key', """ Setup the remote to push to GitHub (to be run on Travis). - ``auth_type`` should be either ``'deploy_key'`` or ``'token'``. + ``auth_type`` should be ``'deploy_key'`` (encrpted deplyoment key + on disk), ``'dkenv'`` (deployment key in environment variable), or + ``'token'``. - For ``auth_type='token'``, this sets up the remote with the token and - checks out the gh-pages branch. The token to push to GitHub is assumed to be in the ``GH_TOKEN`` environment + For ``auth_type='deploy_key'``, this sets up the remote with ssh access. + assuming the private deploement key is in the ``full_key_path`` file + and can be decrypted with the ``env_name`` environment variable. + + For ``auth_type='dkenv'``, this sets up the remote with ssh access + assuming the private deployment key is in the ``env_name`` environment variable. + For ``auth_type='token'``, this sets up the remote with the token and + checks out the gh-pages branch. The token to push to GitHub is assumed + to be in the ``GH_TOKEN`` environment variable. + For ``auth_type='deploy_key'``, this sets up the remote with ssh access. """ # Set to the name of the tag for tag builds @@ -210,8 +236,8 @@ def setup_GitHub_push(deploy_repo, *, auth_type='deploy_key', stacklevel=2) branch_whitelist.add('master') - if auth_type not in ['deploy_key', 'token']: - raise ValueError("auth_type must be 'deploy_key' or 'token'") + if auth_type not in ['deploy_key', 'dkenv', 'token']: + raise ValueError("auth_type must be 'deploy_key', 'dkenv', or 'token'") TRAVIS_BRANCH = os.environ.get("TRAVIS_BRANCH", "") TRAVIS_PULL_REQUEST = os.environ.get("TRAVIS_PULL_REQUEST", "") @@ -220,7 +246,15 @@ def setup_GitHub_push(deploy_repo, *, auth_type='deploy_key', TRAVIS_REPO_SLUG = os.environ["TRAVIS_REPO_SLUG"] REPO_URL = 'https://api.github.com/repos/{slug}' r = requests.get(REPO_URL.format(slug=TRAVIS_REPO_SLUG)) - fork = r.json().get('fork', False) + + if auth_type == 'dkenv': + # Here we don't care if we are on a fork or not - one of the reasons + # for putting the key in a TravisCI secure environment variable is + # to allow things like setting up test source and deployment repos + # under a personal fork. + fork = False + else: + fork = r.json().get('fork', False) canpush = determine_push_rights( branch_whitelist=branch_whitelist, @@ -230,6 +264,14 @@ def setup_GitHub_push(deploy_repo, *, auth_type='deploy_key', TRAVIS_TAG=TRAVIS_TAG, build_tags=build_tags) + if auth_type == 'dkenv': + if args.dkenv not in os.environ: + print("WARNING: Environment variable {dkenv} not set".format(dpenv=args.dkenv)) + canpush = False + elif not os.environ[args.dkenv]: + print("WARNING: Environment variable {dkenv} empty".format(dpenv=args.dkenv)) + canpush = False + print("Setting git attributes") set_git_user_email() @@ -244,6 +286,11 @@ def setup_GitHub_push(deploy_repo, *, auth_type='deploy_key', run(['git', 'remote', 'add', 'doctr_remote', 'https://{token}@github.com/{deploy_repo}.git'.format(token=token.decode('utf-8'), deploy_repo=deploy_repo)]) + elif auth_type == 'dkenv': + # TODO - setup the key + write_key_from_env_var(full_key_path.rsplit('.', 1)[0], args.dkenv) + run(['git', 'remote', 'add', 'doctr_remote', + 'git@github.com:{deploy_repo}.git'.format(deploy_repo=deploy_repo)]) else: keypath, key_ext = full_key_path.rsplit('.', 1) key_ext = '.' + key_ext