Moving from Fabric 1.x to Fabric 2.x

I’ve long used Fabric for simple deploy scripts to essentially ssh in, git pull, composer install or bundle, restart apps, clear caches, etc. It’s also handy for local dev shortcuts like fab assets as a stand-in for the more wordy npx gulp --production or fab dev to simultaneously fire up sphinx, a tornado websocket app, gulp, etc.

With a new M1 MacBook Air, I ran into issues getting fabric@1.4 to play nicely with homebrew, and decided to once again try my hand at converting my simple deploy scripts to the new v2 syntax. Information is scarce on sample Fabric 2 scripts, but I did find an aptly titled post “Why Is Fabric 2 so Hard?” which helped me get started.

There’s an upgrade guide, but my god it’s complicated, until it’s not complicated enough with the sample v1->v2 migration at the end of the page.

Here’s my simple v1 fabfile.py:

from fabric.api import *

env.hosts = ['natebeaty.opalstacked.com']
env.user = 'deploy'
env.remotepath = '/home/natebeaty/apps/nb-craft-staging'
env.git_branch = 'master'
env.forward_agent = True
env.php_binary = 'php74'

def production():
  env.remotepath = '/home/natebeaty/apps/nb-craft-staging'
  env.hosts = ['natebeaty.com']

def assets():
  local('npx gulp --production')

def deploy(composer='y'):
  update()
  if composer == 'y':
    composer_install()
  clear_cache()

def update():
  with cd(env.remotepath):
    run('git pull origin {0}'.format(env.git_branch))

def composer_install():
  with cd(env.remotepath):
    run('%s ~/bin/composer.phar install' % env.php_binary)

def clear_cache():
  with cd(env.remotepath):
    run('./craft clear-caches/compiled-templates')
    run('./craft clear-caches/data')

And this is where I’m at so far with a functional v2 version:

from fabric import task
from invoke import run as local

remote_path = "/home/natebeaty/apps/nb-craft-staging"
remote_hosts = ["deploy@natebeaty.opalstacked.com"]
php_command = "php74"

# set to production
@task
def production(c):
    global remote_hosts, remote_path
    remote_hosts = ["deploy@natebeaty.com"]
    remote_path = "/home/natebeaty/apps/nb-craft-production"

# deploy
@task(hosts=remote_hosts)
def deploy(c):
    update(c)
    composer_update(c)
    clear_cache(c)

def update(c):
    c.run("cd {} && git pull".format(remote_path))

def composer_update(c):
    c.run("cd {} && {} ~/bin/composer.phar install".format(remote_path, php_command))

def clear_cache(c):
    c.run("cd {} && ./craft clear-caches/compiled-templates".format(remote_path))
    c.run("cd {} && ./craft clear-caches/data".format(remote_path))

# local commands
@task
def assets(c):
    local("npx gulp --production")

Still a few things to port over, but it’s working well enough. The part that feels ugly is the use of global in production(), but I couldn’t find any examples of how folks are overriding connection configs in a Fabric v2 file.

Using c.local() I was getting errors finding npx because my $PATH was different, missing the M1 homebrew /opt/homebrew/bin. This StackOverflow comment pointed me to using from invoke import run as local then local() instead of c.local(). This fixed my $PATH issues and npx gulp worked fine.