Skip to content

Instantly share code, notes, and snippets.

@artemave
Last active April 25, 2019 20:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save artemave/3856d66b8fd0009962f36824ebdac501 to your computer and use it in GitHub Desktop.
Save artemave/3856d66b8fd0009962f36824ebdac501 to your computer and use it in GitHub Desktop.
Deploying rails app to heroku

This title is so 2009 - I couldn't resist it. And even back then everybody already knew that all it takes is git push heroku master. And an occasional heroku run rails db:migrate. The latter was a bit manual, so eventually heroku introduced a release task.

So... What is there to write about?

The problem is that migrating database in the release task seems to leave a small opportunity for things to go wrong. Only a teensy one, but when you're in charge of deploying changes to the real production system, this might be enough to keep you uneasy.

Release task is designed to keep serving the previous deployment until the release command finished. On the surface this is good enough, but consider what happens from the time migration is finished (and the changes are committed) and the switcheroo to the new deployment. It's only a few seconds gap, but during that time new db schema powers the old code. Now what if there is more than one migration? Suddenly the gap is wider. It's also not uncommon to run data migrations after schema migrations. Now we're just asking for a trouble. Disclaimer: I have no factual evidence to the above.

A safer option - just like in the pre release times - is to switch on the maintenance mode, update db and the switch the maintenance back off. Annoyingly manual. But easily scriptable. Which is what the rest of this post is about.

I'd like my deploy script to:

  • only do migration/maintenance dance if there are db changes (to minimize production downtime)
  • detect pending migrations and if there are any:
    • put app in maintenance mode
    • git push
    • run db migrations
    • run data migrations
    • if all is well, turn off maintenance mode
    • if any of the above goes wrong, rollback whatever succeeded
  • simply git push when if there are no migrations to run
  • have a dry-run option that outputs what type the next deployment is going to be
  • make sure we push the latest code

Implementation (could be simplified a lot if you don't use data migrations):

#!/usr/bin/env bash

# deploy rails app to heroku running migrations if needed
#
# deploy default app
# $ ./deploy
#
# deploy some other app
# $ HEROKU_APP=myapp ./deploy
#
# check what type the next migration is going to be
# $ ./deploy --query

HEROKU_APP=${HEROKU_APP:=YOUR_HEROKU_APP_NAME}

if [ "$(git rev-parse --abbrev-ref HEAD)" != master ]; then
  echo "Must be on the master branch"
  exit 1
fi

git fetch origin

if [ "$(git rev-parse HEAD)" != "$(git rev-parse @{u})" ]; then
  echo "Your local master is out of sync with the origin"
  exit 1
fi

heroku git:remote --app $HEROKU_APP --remote production
git fetch production

current_release_sha=$(heroku releases:info -a $HEROKU_APP | grep HEROKU_SLUG_COMMIT | awk '{print $2}')

if git diff --name-only master..$current_release_sha | grep -e db/migrate -e db/data_migrations; then
  echo "Deploy with migration"
  [ "$1" = '--query' ] && exit

  heroku maintenance:on -a $HEROKU_APP
  
  # might want to backup db for good measure
  # heroku pg:backups capture -a $HEROKU_APP

  git push production master

  if [ $? -eq 0 ]; then
    heroku run rails db:migrate -a $HEROKU_APP
    schema_migration_exit_code=$?

    if [ $schema_migration_exit_code -eq 0 ]; then
      heroku run rails data:migrate -a $HEROKU_APP
      data_migration_exit_code=$?

      if [ $data_migration_exit_code -ne 0 ]; then
        heroku run rails db:rollback
      fi
    fi

    if [ $schema_migration_exit_code -eq 0 ] && [ $data_migration_exit_code -eq 0 ]; then
      heroku restart -a $HEROKU_APP
    else
      heroku rollback -a $HEROKU_APP
    fi
  fi

  heroku maintenance:off -a $HEROKU_APP

  if [ $schema_migration_exit_code -ne 0 ] || [ $data_migration_exit_code -ne 0 ]; then
    echo "DEPLOY FAILED!!!"
    exit 1
  fi
else
  echo "Simple deploy"
  [ "$1" = '--query' ] && exit

  git push production master
fi

Bear in mind that as of this writing only the happy paths of that script have been battle tested, so give it a read before using.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment