Skip to content

Instantly share code, notes, and snippets.

@gerhard
Created February 17, 2012 11:37
Show Gist options
  • Save gerhard/1852864 to your computer and use it in GitHub Desktop.
Save gerhard/1852864 to your computer and use it in GitHub Desktop.
Deliver vs git-hooks

Why not just use git hooks?

  1. They run in the background, thus there is no visibility as to whether they succeed or fail.

  2. Deploying an app from start to finish involves multiple users, and if done right requires sudo privileges. The system user under which the app runs must not have sudo privileges, but the app itself would use Foreman ideally. If you export to Sys V or Upstart, you will need sudo privileges.

@gerhard
Copy link
Author

gerhard commented Feb 17, 2012

  1. Not true! Here's a screenshot of git hooks running a deployment which reported status synchronously:
    git push production
  2. Agreed, but you don't need sudo to fire events via upstart, nor to signal your own processes, and there is always simple file monitoring like touch tmp/restart.txt which most rack servers respect.

@sj26
Copy link

sj26 commented Feb 17, 2012

The intrepid developer could also embrace the ideas in the upstart cookbook to Run a Job When a File or Directory is Created/Deleted and merge this with some sort of evented file monitoring (inotify on linux these days?).

@sj26
Copy link

sj26 commented Feb 17, 2012

That screen shot was meant to include the command that triggered it, sorry:

git push production

Production is git remote, twatter_production@<somewhere>.com:current. The server is an ubuntu machine with a user, twatter_production, whose shell is set to /usr/bin/git-shell and whose home directory houses the git repository (of the application). There are a couple of interesting things about the git repository:

  • git config --add receive.denycurrentbranch ignore
  • .git/hooks/pre-receive:
#!/usr/bin/env ruby

refspecs = STDIN.read.chomp.split("\n")

if refspecs.length != 1
  puts %Q{
remote says:
  Hi there, you just pushed #{refspecs.length} branches, but I only accept one branch
  at a time, because I use it to update the working copy.

  The best way to push just a single branch is like this:
    $ git push <remote-name> <branch-name>

}
  exit 1
end
  • .git/hooks/post-receive:
#!/usr/local/bin/rvm-auto-ruby

def fail message=nil
  puts message unless message.nil?
  exit 1
end

def shell cmd
  output = `#{cmd}`
  output.chomp if $? == 0
end

def log_shell message, cmd
  print "#{message}... "
  output = `#{cmd}`
  if $? == 0
    puts "done."
  else
    fail "failed!\n\n#{output.chomp}"
  end
end

STDOUT.sync = true

refspecs = STDIN.read.chomp.split("\n")
old_id, new_id, ref_name = refspecs.first.split(/\s+/)
new_branch = ref_name.scan(/^refs\/heads\/(.*)$/).flatten.first

# Otherwise operations in sub-gits fail
ENV.delete "GIT_DIR"

if new_branch.nil?
  fail "Couldn't figure out what branch '#{ref_name}' refers to, not updating."
else
  env_git = "env -i #{`which git`.chomp}"
  Dir.chdir('..') do # change dir to .git/..
    branches = shell("#{env_git} branch").split("\n")
    star_branches = branches.grep(/^\*/)
    old_branch = star_branches.empty? ? nil : star_branches.first.split(/\s+/, 2)[-1]
    branches.map! { |branch| branch.split(/\s+/, 2).last }

    if !branches.include?(new_branch)
      log_shell "Creating the '#{new_branch}' branch", "#{env_git} checkout -b '#{new_branch}'"
    end

    if old_branch != new_branch
      log_shell "Switching to the '#{new_branch}' branch", "#{env_git} checkout '#{new_branch}'"
    end

    print "Writing database.yml... "
    File.open 'config/database.yml', 'w' do |file|
      file.write "production:\n  adapter: postgresql\n  database: twatter_production\n  username: twatter_production\n  password: d0178bd6923df8eb251cca416c56eb7ac9325820\n"
    end
    puts "done."

    log_shell "Enabling maintenance mode", "mkdir -p tmp && touch tmp/maintenance.txt"
    log_shell "Updating to #{new_id[0...7]}", "#{env_git} reset --hard '#{new_id}'"
    log_shell "Updating submodules", "#{env_git} submodule update --init"
    log_shell "Bundling", 'bundle --deployment'
    log_shell "Pre-compiling assets", 'bundle exec rake RAILS_ENV=production assets:clean assets:precompile'
    log_shell "Migrating", 'bundle exec rake RAILS_ENV=production db:migrate'
    log_shell "Flagging app for restart", 'mkdir -p tmp && touch tmp/restart.txt'
    log_shell "Disabling maintenance mode", "rm tmp/maintenance.txt"
  end
end

Being that this is small scale, this application is currently deployed with passenger, but the principles are the same. The caveat in this particular git script is that it doesn't rollback on error, although adding that behaviour would be fairly easy. I have other apps deployed via nginx/foreman/thin/resque setups this way with upstart signals.

(Also, credit to @benhoskings' early git deployment scripts from babushka for the above.)

@gerhard
Copy link
Author

gerhard commented Feb 17, 2012

1. Nice, I didn't know git hooks would run synchronously. Thanks!

With git hooks, don't you have to keep modifying the remote git hooks whenever there is an extra deployment step?
With deliver, I wanted to keep the strategy under version control, together with the project, similar to Capfile. With this approach, I can test deployment changes on a local VM before committing them.

Also, git-hooks alone can't work with multi-server environments, you need something extra to orchestrate the deploy. What about cloud environments which are in constant flux? A deliver strategy which uses eg. an API to fetch the instances where it should push code to feels a lot more flexible. That feature will be coming soon, we're using it at GoSquared where instances auto-scale based on load. One minute I might be pushing the code to 2 instances, half an hour later it might be 10.

2. Can you run restart app-name on Ubuntu without sudo privileges? Also, an app is no longer just a rack server. It might have schedulers, bg workers, RabbitMQ consumers & publishers etc. restart app-name is the most efficient way I know of restarting all those services which are part of it. How would you approach this?

@gerhard
Copy link
Author

gerhard commented Feb 17, 2012

Run a Job When a File or Directory is Created/Deleted

That's a brilliant tip! I shall explore the idea further ; ).

@gerhard
Copy link
Author

gerhard commented Feb 17, 2012

Care to paste your /usr/local/bin/rvm-auto-ruby ? Would really like to see how it works!

@sj26
Copy link

sj26 commented Feb 17, 2012

You could have your git hook call something inside your application as a deployment step. rake deploy might be interesting, then you can do whatever is necessary with all the power of rake dependencies.

Yeah, the multi-server stuff would be a kicker, but I don't see Capistrano/Chef falling short here. What will deliver offer which is different?

Keep in mind that restart on ubuntu simply emits a stop and start event for an upstart job. You can make your application job emit a deploy job from the git deploy hook, then fire any additional events you need—reload configuration, bounce your message queue, scale up/down background workers, etc., even make it re-export the forman upstart scripts.

@sj26
Copy link

sj26 commented Feb 17, 2012

rvm-auto-ruby comes with rvm. :-)

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