# 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))
@task(default=True)
+@expand_env
def bundle_all():
""" Bundles vendor and application files.
"""
bundle_app()
@task
+@expand_env
@msg('Collapsing Serve Trees')
def collapse_trees():
""" Collapse the serve trees into one directory.
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)
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.
vendor_bundle.write('\n')
@task
+@expand_env
@msg('Building App Bundle')
def bundle_app():
""" Bundles and minifies app files.
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
@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.
"""
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.
"""
# 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.
"""
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
@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."
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 :(
+
###
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.
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.
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.
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']
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."
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):
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():
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
+