Adds branch support to deployer.
authordsc <dsc@wikimedia.org>
Wed, 16 May 2012 06:27:08 +0000 (23:27 -0700)
committerdsc <dsc@wikimedia.org>
Wed, 16 May 2012 06:27:08 +0000 (23:27 -0700)
fabfile/__init__.py
fabfile/bundle.py
fabfile/deploy.py
fabfile/stages.py
fabfile/util.py

index e13d825..60acf02 100644 (file)
@@ -34,40 +34,51 @@ from util import *
 
 # TODO: env.rcfile = 'fabfile/fabricrc.conf'
 
-# We `update()` here so --set on the commandline will override.
-for k, v in dict(
-    project_name       = 'kraken-ui',
+# We use `defaults()` here so --set on the commandline will override.
+defaults(env, dict(
+    project_name       = 'GraphKit',
     colors             = True,
     use_ssh_config     = True,
     
+    git_origin         = 'git@less.ly:kraken-ui.git',
     dev_server         = 'localhost:8081',
     minify_cmd         = 'uglifyjs',
     
+    supervisor_job     = 'reportcard',
+    
+    ### Paths
     dist               = 'dist',
     local_tmp          = 'tmp',
+    work_dir           = '%(local_tmp)s/%(dist)s',
+    
     browserify_js      = 'vendor/browserify.js',
-    supervisor_job     = 'reportcard',
-).iteritems():
-    env.setdefault(k, v)
-
-for k in ('dist', 'local_tmp',):
+    work_browserify_js = '%(work_dir)s/%(browserify_js)s',
+    
+    vendor_search_dirs = ['static', 'var', '%(work_dir)s']
+    vendor_bundle      = '%(work_dir)s/vendor/vendor-bundle.min.js',
+    app_bundle         = '%(work_dir)s/js/kraken/app-bundle.js',
+))
+
+env_paths = (
+    'dist', 'local_tmp', 'work_dir', 
+    'browserify_js', 'work_browserify_js',
+    'vendor_bundle', 'app_bundle',
+)
+for k in env_paths:
     env[k] = p(env[k])
 
-env.work_dir           = env.local_tmp/env.dist
-env.work_browserify_js = env.work_dir/env.browserify_js
-env.vendor_search_dirs = map(p, ['static', 'var', env.work_dir])
-env.vendor_bundle      = env.work_dir/'vendor/vendor-bundle.min.js'
-env.app_bundle         = env.work_dir/'js/kraken/app-bundle.js'
+env.vendor_search_dirs = [ expand(p(vd)) for vd in env.vendor_search_dirs ]
 env.app_bundle_min     = p(env.app_bundle.replace('.js', '.min.js'))
 
 
+
 ### Setup Staging Environments
 
 # Envs aren't declared with @task in the stages module so that we can
 # decorate them here and have them show up at the top level.
 import stages
 
-for name in stages.NAMES:
+for name in stages.STAGE_NAMES:
     globals()[name] = task(getattr(stages, name))
 
 
index 0884c7f..33956a3 100644 (file)
@@ -9,6 +9,7 @@ from util import *
 
 
 @task(default=True)
+@expand_env
 def bundle_all():
     """ Bundles vendor and application files.
     """
@@ -18,6 +19,7 @@ def bundle_all():
     bundle_app()
 
 @task
+@expand_env
 @msg('Collapsing Serve Trees')
 def collapse_trees():
     """ Collapse the serve trees into one directory.
@@ -37,7 +39,7 @@ def collapse_trees():
     local('rsync -Ca static/ var/ data %(work_dir)s/' % env)
     
     # We copy lib (which contains .co source files) to src to make it easy to link source content
-    # to each other. Finding it in gitweb is a pain. Finding it in gerrit is almost impossible. 
+    # to each other. Finding it in gitweb is a pain. Finding it in gerrit is almost impossible.
     # But this could go away when we move to github.
     local('rsync -Ca lib/ %(work_dir)s/src/' % env)
     
@@ -48,6 +50,7 @@ def collapse_trees():
         f.write( local('curl --silent --fail --url http://%(dev_server)s/%(browserify_js)s' % env, capture=True) )
 
 @task
+@expand_env
 @msg('Building Vendor Bundle')
 def bundle_vendor():
     """ Bundles vendor files.
@@ -68,6 +71,7 @@ def bundle_vendor():
         vendor_bundle.write('\n')
 
 @task
+@expand_env
 @msg('Building App Bundle')
 def bundle_app():
     """ Bundles and minifies app files.
index 4111088..6f7e6cb 100644 (file)
@@ -4,6 +4,7 @@
 
 from fabric.api import *
 from fabric.colors import white, blue, cyan, green, yellow, red, magenta
+from fabric.contrib.files import exists
 from fabric.contrib.project import rsync_project
 
 from stages import ensure_stage
@@ -12,18 +13,21 @@ from util import *
 
 
 @task(default=True)
+@expand_env
 @ensure_stage
 def deploy_and_update():
     """ Deploy the project.
     """
     fix_permissions()
-    pull()
+    update_branch()
     sync_files()
     fix_permissions()
     restart_node()
 
 @task
+@expand_env
 @ensure_stage
+@msg('Fixing Permissions')
 def fix_permissions(user=None, group=None):
     """ Recursively fixes permissions on the deployment host.
     """
@@ -33,14 +37,42 @@ def fix_permissions(user=None, group=None):
     sudo('chown -R %s:%s %s' % (user, group, env.target_dir))
 
 @task
+@expand_env
 @ensure_stage
-def pull():
+@msg('Cloning Origin')
+def clone():
+    """ Clones source on deployment host if not present.
+    """
+    if exists(env.target_dir): return
+    with cd(env.target_dir.dirname()):
+        run('git clone %(git_origin)s' % env)
+
+@task
+@expand_env
+@ensure_stage
+def checkout():
+    """ Checks out proper branch on deployment host.
+    """
+    # TODO: Locally saved data files will cause yelling?
+    with cd(env.target_dir):
+        run('git fetch --all')
+        run('git checkout --track origin/%(git_branch)s' % env)
+
+@task
+@expand_env
+@ensure_stage
+@msg('Updating Branch')
+def update_branch():
     """ Runs git pull on the deployment host.
     """
-    with cd(env.target_dir): run('git pull')
+    with cd(env.target_dir):
+        execute(checkout)
+        run('git pull origin %(git_branch)s' % env)
 
 @task
+@expand_env
 @ensure_stage
+@msg('Syncing Files')
 def sync_files():
     """ Copies `dist` package to deployment host.
     """
@@ -49,7 +81,9 @@ def sync_files():
     # rsync_project(local_dir=env.work_dir, remote_dir="%(user)s@%(host)s:%(target_dir)s/" % env)
 
 @task
+@expand_env
 @ensure_stage
+@msg('Restarting Node.js')
 def restart_node():
     """ Restarts node.js server on the deployment host.
     """
index 2b58c72..21ba7d2 100644 (file)
@@ -6,23 +6,31 @@ import sys
 from functools import wraps
 from fabric.api import env, abort, prompt, execute
 from fabric.colors import white, blue, cyan, green, yellow, red, magenta
+from fabric.contrib.console import confirm
 
-__all__ = ['NAMES', 'validate_stage', 'prompt_for_stage', 'ensure_stage', 'stage',]
 
-# (otto) There should be a way to do this using stages.
-# See: http://tav.espians.com/fabric-python-with-cleaner-api-and-parallel-deployment-support.html
-# (dsc) ...except that most of those changes are not actually in Fabric :(
+__all__ = [
+    'STAGE_NAMES', 'prompt_for_stage', 'ensure_stage', 
+    'working_branch', 'check_branch',
+]
 
-# env.config_file = False
-# env.stages = ['prod', 'staging']
 
+STAGE_NAMES = []
 
-NAMES = []
+def stage(fn):
+    """ Decorator indicating this function sets a stage environment.
+    """
+    STAGE_NAMES.append(fn.__name__)
+    __all__.append(fn.__name__)
+    return fn
 
 def validate_stage(name):
-    "Tests whether given name is a valid staging environment."
+    """ Tests whether given name is a valid staging environment.
+        
+            name = fabric.api.prompt(msg, validate=validate_stage)
+    """
     name = name.strip()
-    if name not in NAMES:
+    if name not in STAGE_NAMES:
         raise Exception("%r is not a valid staging environment!" % name)
     return name
 
@@ -33,14 +41,13 @@ def prompt_for_stage(fn):
     @wraps(fn)
     def wrapper(*args, **kwargs):
         if 'deploy_env' not in env:
-            name = prompt(white('Please select a staging target %s:' % NAMES, bold=True), validate=validate_stage)
+            name = prompt(white('Please select a staging target %s:' % STAGE_NAMES, bold=True), validate=validate_stage)
             execute(name)
         # Must call `execute` on the supplied task, otherwise the host-lists won't be updated.
         execute(fn, *args, **kwargs)
     
     return wrapper
 
-
 def ensure_stage(fn):
     "Decorator that ensures a stage is set."
     
@@ -52,18 +59,33 @@ def ensure_stage(fn):
     
     return wrapper
 
-def stage(fn):
-    """ Decorator indicating this function sets a stage environment:
-        
-            @stage
-            def mystage():
-                env.deploy_env = 'mystage'
-                ...
-        
-    """
-    NAMES.append(fn.__name__)
-    __all__.append(fn.__name__)
-    return fn
+
+def working_branch():
+    "Determines the working branch."
+    return [ branch.split()[1] for branch in local('git branch', capture=True).split('\n') if '*' in branch ][0]
+
+def check_branch(fn):
+    "Decorator that ensures deploy branch matches current branch."
+    
+    @wraps(fn)
+    def wrapper(*args, **kwargs):
+        env.working_branch = working_branch()
+        if env.working_branch != env.git_branch:
+            question = 'Your working branch %(working_branch)r does not match the expected branch %(git_branch)r for %(deploy_env)s. Proceed anyway?' % env
+            confirm(yellow(question, bold=True))
+        return fn(*args, **kwargs)
+    
+    return wrapper
+
+
+### Stages
+
+# env.stages = ['prod', 'staging']
+
+# (otto) There should be a way to do this using stages.
+# See: http://tav.espians.com/fabric-python-with-cleaner-api-and-parallel-deployment-support.html
+# (dsc) ...except that most of those changes are not actually in Fabric :(
+
 
 
 ###
@@ -81,9 +103,11 @@ def prod():
     env.hosts      = ['reportcard2.pmtpa.wmflabs']
     env.gateway    = 'bastion.wmflabs.org'
     env.target_dir = '/srv/reportcard/kraken-ui'
+    env.git_branch = 'master'
     env.owner      = 'www-data'
     env.group      = 'www'
 
+
 @stage
 def test():
     """ Set deploy environment to test.
@@ -92,10 +116,12 @@ def test():
     env.hosts      = ['kripke.pmtpa.wmflabs']
     env.gateway    = 'bastion.wmflabs.org'
     env.target_dir = '/srv/test-reportcard.wmflabs.org/kraken-ui'
+    env.git_branch = 'rc'
     env.owner      = 'www-data'
     env.group      = 'www'
     env.supervisor_job = 'test-reportcard'
 
+
 @stage
 def dev():
     """ Set deploy environment to dev.
@@ -104,10 +130,12 @@ def dev():
     env.hosts      = ['kripke.pmtpa.wmflabs']
     env.gateway    = 'bastion.wmflabs.org'
     env.target_dir = '/srv/dev-reportcard.wmflabs.org/kraken-ui'
+    env.git_branch = 'develop'
     env.owner      = 'www-data'
     env.group      = 'www'
     env.supervisor_job = 'dev-reportcard'
 
+
 @stage
 def lessly():
     """ Set deploy environment to lessly.
@@ -115,6 +143,7 @@ def lessly():
     env.deploy_env = 'lessly'
     env.hosts      = ['less.ly']
     env.target_dir = '/home/wmf/projects/kraken-ui'
+    env.git_branch = 'develop'
     env.owner      = 'wmf'
     env.group      = 'www'
     del env['gateway']
index 736cbb4..c5e7ef5 100644 (file)
@@ -4,13 +4,19 @@
 from __future__ import with_statement
 from contextlib import contextmanager
 from functools import wraps
+from path import path as p # renamed to avoid conflict w/ fabric.api.path
 
 from fabric.api import *
 from fabric.colors import white, blue, cyan, green, yellow, red, magenta
 
-__all__ = ('quietly', 'msg', 'coke', 'update_version',)
+__all__ = (
+    'quietly', 'msg', 'coke', 'update_version',
+    'defaults', 'expand', 'expand_env', 'format', 'expand_env',
+)
 
 
+### Context Managers
+
 @contextmanager
 def quietly(txt):
     "Wrap a block in a message, suppressing other output."
@@ -19,6 +25,8 @@ def quietly(txt):
     puts("woo.", show_prefix=False, flush=True)
 
 
+### Decorators
+
 def msg(txt, quiet=False):
     "Decorator to wrap a task in a message, optionally suppressing all output."
     def outer(fn):
@@ -38,13 +46,12 @@ def msg(txt, quiet=False):
     return outer
 
 
-
-### Misc
+### Coke Integration
 
 def coke(args, capture=False):
     """ Invokes project Cokefile.
     """
-    local('coke %s' % args, capture=capture)
+    return local('coke %s' % args, capture=capture)
 
 @runs_once
 def update_version():
@@ -54,3 +61,48 @@ def update_version():
     print ''
 
 
+
+### Misc
+
+def defaults(target, *sources):
+    "Update target dict using `setdefault()` for each key in each source."
+    for source in sources:
+        for k, v in source.iteritems():
+            target.setdefault(k, v)
+    return target
+
+def expand(s):
+    "Recursively expands given string using the `env` dict."
+    is_path = isinstance(s, p)
+    prev = None
+    while prev != s:
+        prev = s
+        s = s % env
+    return p(s) if is_path else s
+
+def format(s):
+    "Recursively formats string using the `env` dict."
+    is_path = isinstance(s, p)
+    prev = None
+    while prev != s:
+        prev = s
+        s = s.format(**env)
+    return p(s) if is_path else s
+
+
+@runs_once
+def _expand_env():
+    for k, v in env.iteritems():
+        if not isinstance(v, basestring): continue
+        env[k] = expand(env[k])
+
+def expand_env(fn):
+    "Decorator expands all strings in `env`."
+    
+    @wraps(fn)
+    def wrapper(*args, **kwargs):
+        _expand_env()
+        return fn(*args, **kwargs)
+    
+    return wrapper
+