Repository: drdoctr/doctr

Issue:
Ability to do something different on a tag
This tool is mainly designed for uploading dev docs, but it can also be used to upload release docs. In that case, we might need some way to do something different on a tag. 

Or maybe the logic for uploading on a tag should just go in the individual .travis.yml files, in which case we can at least document what it could look like. 


Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index c621f5a3..13a07f42 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -51,6 +51,8 @@ script:
       # GitHub Wiki deploy
       echo "This page was automatically deployed by doctr on $(date)" > deploy-test.md;
       python -m doctr deploy --sync --key-path deploy_key.enc --no-require-master --deploy-repo drdoctr/doctr.wiki . --built-docs deploy-test.md;
+      # Build on tags
+      python -m doctr deploy --sync --key-path deploy_key.enc "tag-$TRAVIS_TAG" --build-tags --branch-whitelist;
     fi
   - if [[ "${TESTS}" == "true" ]]; then
       pyflakes doctr;
diff --git a/docs/recipes.rst b/docs/recipes.rst
index a9459026..756a7c06 100644
--- a/docs/recipes.rst
+++ b/docs/recipes.rst
@@ -41,6 +41,35 @@ For security purposes, it is not possible to deploy from branches on forks
 requests from forks). If you want to deploy the docs for a branch from a pull
 request, you will need to push it up to the main repository.
 
+Deploy docs from git tags
+=========================
+
+Travis CI runs separate builds for git tags that are pushed to your repo. By
+default, doctr does not deploy on these builds, but it can be enabled with the
+``--build-tags`` flag to ``doctr deploy``. This is useful if you want to use
+doctr to deploy versioned docs for releases, for example.
+
+On Travis CI, the tag is set to the environment variable ``$TRAVIS_TAG``,
+which is empty otherwise. The following will deploy the docs to ``dev`` for
+normal ``master`` builds, and ``version-<TAG NAME>`` for tag builds:
+
+.. code:: yaml
+
+  - if [[ -z "$TRAVIS_TAG" ]]; then
+      DEPLOY_DIR=dev;
+    else
+      DEPLOY_DIR="version-$TRAVIS_TAG";
+    fi
+  - doctr deploy --build-tags --built-docs build/ $DEPLOY_DIR
+
+If you want to deploy only on a tag, use ``--branch-whitelist`` with no
+arguments to tell doctr to not deploy from any branch. For instance, to deploy
+only tags to ``latest``:
+
+.. code:: yaml
+
+   - doctr deploy latest --built-docs build/ --build-tags --branch-whitelist
+
 Deploy to a separate repo
 =========================
 
diff --git a/doctr/__main__.py b/doctr/__main__.py
index 524dacc8..befc05a2 100644
--- a/doctr/__main__.py
+++ b/doctr/__main__.py
@@ -145,6 +145,10 @@ options available.
         help=argparse.SUPPRESS)
     deploy_parser_add_argument('--deploy-repo', default=None, help="""Repo to
         deploy the docs to. By default, it deploys to the repo Doctr is run from.""")
+    deploy_parser_add_argument('--branch-whitelist', default=None, nargs='*',
+        help="""Branches to deploy from. Pass no arguments to not build on any branch
+        (typically used in conjunction with --build-tags). Note that you can
+        deploy from every branch with --no-require-master.""", type=set, metavar="BRANCH")
     deploy_parser_add_argument('--no-require-master', dest='require_master', action='store_false',
         default=True, help="""Allow docs to be pushed from a branch other than master""")
     deploy_parser_add_argument('--command', default=None,
@@ -163,6 +167,11 @@ options available.
     deploy_parser_add_argument('--no-push', dest='push', action='store_false',
         default=True, help="Run all the steps except the last push step. "
         "Useful for debugging")
+    deploy_parser_add_argument('--build-tags', action='store_true',
+        default=False, help="""Deploy on tag builds. On a tag build,
+        $TRAVIS_TAG is set to the name of the tag. The default is to not
+        deploy on tag builds. Note that this will still build on a branch,
+        unless --branch-whitelist (with no arguments) is passed.""")
     deploy_parser_add_argument('--gh-pages-docs', default=None,
         help="""!!DEPRECATED!! Directory to deploy the html documentation to on gh-pages.
         The default is %(default)r. The deploy directory should be passed as
@@ -273,13 +282,19 @@ def deploy(args, parser):
 
     current_commit = subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8').strip()
     try:
-        branch_whitelist = {'master'} if args.require_master else set(get_travis_branch())
+        branch_whitelist = set() if args.require_master else set(get_travis_branch())
         branch_whitelist.update(set(config.get('branches',set({}))))
+        if args.branch_whitelist is not None:
+            branch_whitelist.update(args.branch_whitelist)
+            if not args.branch_whitelist:
+                branch_whitelist = {'master'}
 
         canpush = setup_GitHub_push(deploy_repo, deploy_branch=deploy_branch,
                                      auth_type='token' if args.token else 'deploy_key',
                                      full_key_path=keypath,
-                                     branch_whitelist=branch_whitelist, env_name=env_name)
+                                     branch_whitelist=branch_whitelist,
+                                     build_tags=args.build_tags,
+                                     env_name=env_name)
 
         if args.sync:
             built_docs = args.built_docs or find_sphinx_build_dir()
diff --git a/doctr/travis.py b/doctr/travis.py
index 557c4a9d..9682022c 100644
--- a/doctr/travis.py
+++ b/doctr/travis.py
@@ -183,8 +183,10 @@ def get_travis_branch():
     else:
         return os.environ.get("TRAVIS_BRANCH", "")
 
-def setup_GitHub_push(deploy_repo, auth_type='deploy_key', full_key_path='github_deploy_key.enc',
-    require_master=None, branch_whitelist=None, deploy_branch='gh-pages', env_name='DOCTR_DEPLOY_ENCRYPTION_KEY'):
+def setup_GitHub_push(deploy_repo, *, auth_type='deploy_key',
+    full_key_path='github_deploy_key.enc', require_master=None,
+    branch_whitelist=None, deploy_branch='gh-pages',
+    env_name='DOCTR_DEPLOY_ENCRYPTION_KEY', build_tags=False):
     """
     Setup the remote to push to GitHub (to be run on Travis).
 
@@ -196,6 +198,8 @@ def setup_GitHub_push(deploy_repo, auth_type='deploy_key', full_key_path='github
 
     For ``auth_type='deploy_key'``, this sets up the remote with ssh access.
     """
+    # Set to the name of the tag for tag builds
+    TRAVIS_TAG = os.environ.get("TRAVIS_TAG", "")
 
     if not branch_whitelist:
         branch_whitelist={'master'}
@@ -213,8 +217,12 @@ def setup_GitHub_push(deploy_repo, auth_type='deploy_key', full_key_path='github
     TRAVIS_BRANCH = os.environ.get("TRAVIS_BRANCH", "")
     TRAVIS_PULL_REQUEST = os.environ.get("TRAVIS_PULL_REQUEST", "")
 
-    canpush = determine_push_rights(branch_whitelist, TRAVIS_BRANCH,
-                                    TRAVIS_PULL_REQUEST)
+    canpush = determine_push_rights(
+        branch_whitelist=branch_whitelist,
+        TRAVIS_BRANCH=TRAVIS_BRANCH,
+        TRAVIS_PULL_REQUEST=TRAVIS_PULL_REQUEST,
+        TRAVIS_TAG=TRAVIS_TAG,
+        build_tags=build_tags)
 
     print("Setting git attributes")
     set_git_user_email()
@@ -441,6 +449,9 @@ def commit_docs(*, added, removed):
     TRAVIS_COMMIT = os.environ.get("TRAVIS_COMMIT", "<unknown>")
     TRAVIS_REPO_SLUG = os.environ.get("TRAVIS_REPO_SLUG", "<unknown>")
     TRAVIS_JOB_ID = os.environ.get("TRAVIS_JOB_ID", "")
+    TRAVIS_TAG = os.environ.get("TRAVIS_TAG", "")
+    branch = "tag" if TRAVIS_TAG else "branch"
+
     DOCTR_COMMAND = ' '.join(map(shlex.quote, sys.argv))
 
     for f in added:
@@ -452,7 +463,7 @@ def commit_docs(*, added, removed):
 Update docs after building Travis build {TRAVIS_BUILD_NUMBER} of
 {TRAVIS_REPO_SLUG}
 
-The docs were built from the branch '{TRAVIS_BRANCH}' against the commit
+The docs were built from the {branch} '{TRAVIS_BRANCH}' against the commit
 {TRAVIS_COMMIT}.
 
 The Travis build that generated this commit is at
@@ -462,6 +473,7 @@ The doctr command that was run is
 
     {DOCTR_COMMAND}
 """.format(
+    branch=branch,
     TRAVIS_BUILD_NUMBER=TRAVIS_BUILD_NUMBER,
     TRAVIS_BRANCH=TRAVIS_BRANCH,
     TRAVIS_COMMIT=TRAVIS_COMMIT,
@@ -504,12 +516,18 @@ def push_docs(deploy_branch='gh-pages', retries=3):
             return
     sys.exit("Giving up...")
 
-def determine_push_rights(branch_whitelist, TRAVIS_BRANCH, TRAVIS_PULL_REQUEST):
+def determine_push_rights(*, branch_whitelist, TRAVIS_BRANCH,
+    TRAVIS_PULL_REQUEST, TRAVIS_TAG, build_tags):
     """Check if Travis is running on ``master`` (or a whitelisted branch) to
     determine if we can/should push the docs to the deploy repo
     """
     canpush = True
 
+    if TRAVIS_TAG:
+        if not build_tags:
+            print("The docs are not pushed on tag builds. To push on future tag builds, use --build-tags")
+        return build_tags
+
     if not any([re.compile(x).match(TRAVIS_BRANCH) for x in branch_whitelist]):
         print("The docs are only pushed to gh-pages from master. To allow pushing from "
         "a non-master branch, use the --no-require-master flag", file=sys.stderr)


Test patch:
diff --git a/doctr/tests/test_travis.py b/doctr/tests/test_travis.py
index 6a4c3bbe..136ddeaf 100644
--- a/doctr/tests/test_travis.py
+++ b/doctr/tests/test_travis.py
@@ -144,16 +144,51 @@ def test_sync_from_log(src, dst):
             os.chdir(old_curdir)
 
 
-@pytest.mark.parametrize("travis_branch, travis_pr, whitelist, canpush",
-                         [('doctr', 'true', 'master', False),
-                          ('doctr', 'false', 'master', False),
-                          ('master', 'true', 'master', False),
-                          ('master', 'false', 'master', True),
-                          ('doctr', 'True', 'doctr', False),
-                          ('doctr', 'false', 'doctr', True),
-                          ('doctr', 'false', 'set()', False),
-                         ])
-def test_determine_push_rights(travis_branch, travis_pr, whitelist, canpush, monkeypatch):
-    branch_whitelist = {whitelist}
+@pytest.mark.parametrize("""branch_whitelist, TRAVIS_BRANCH,
+                         TRAVIS_PULL_REQUEST, TRAVIS_TAG, build_tags,
+                         canpush""",
+                         [
+
+                             ('master', 'doctr', 'true', "", False, False),
+                             ('master', 'doctr', 'false', "", False, False),
+                             ('master', 'master', 'true', "", False, False),
+                             ('master', 'master', 'false', "", False, True),
+                             ('doctr', 'doctr', 'True', "", False, False),
+                             ('doctr', 'doctr', 'false', "", False, True),
+                             ('set()', 'doctr', 'false', "", False, False),
+
+                             ('master', 'doctr', 'true', "tagname", False, False),
+                             ('master', 'doctr', 'false', "tagname", False, False),
+                             ('master', 'master', 'true', "tagname", False, False),
+                             ('master', 'master', 'false', "tagname", False, False),
+                             ('doctr', 'doctr', 'True', "tagname", False, False),
+                             ('doctr', 'doctr', 'false', "tagname", False, False),
+                             ('set()', 'doctr', 'false', "tagname", False, False),
+
+                             ('master', 'doctr', 'true', "", True, False),
+                             ('master', 'doctr', 'false', "", True, False),
+                             ('master', 'master', 'true', "", True, False),
+                             ('master', 'master', 'false', "", True, True),
+                             ('doctr', 'doctr', 'True', "", True, False),
+                             ('doctr', 'doctr', 'false', "", True, True),
+                             ('set()', 'doctr', 'false', "", True, False),
+
+                             ('master', 'doctr', 'true', "tagname", True, True),
+                             ('master', 'doctr', 'false', "tagname", True, True),
+                             ('master', 'master', 'true', "tagname", True, True),
+                             ('master', 'master', 'false', "tagname", True, True),
+                             ('doctr', 'doctr', 'True', "tagname", True, True),
+                             ('doctr', 'doctr', 'false', "tagname", True, True),
+                             ('set()', 'doctr', 'false', "tagname", True, True),
 
-    assert determine_push_rights(branch_whitelist, travis_branch, travis_pr) == canpush
+                         ])
+def test_determine_push_rights(branch_whitelist, TRAVIS_BRANCH,
+    TRAVIS_PULL_REQUEST, TRAVIS_TAG, build_tags, canpush, monkeypatch):
+    branch_whitelist = {branch_whitelist}
+
+    assert determine_push_rights(
+        branch_whitelist=branch_whitelist,
+        TRAVIS_BRANCH=TRAVIS_BRANCH,
+        TRAVIS_PULL_REQUEST=TRAVIS_PULL_REQUEST,
+        TRAVIS_TAG=TRAVIS_TAG,
+        build_tags=build_tags) == canpush