Permalink
Browse files

Updated deploy tasks

  • Loading branch information...
1 parent 6c450f0 commit 2b14ec174616c20384de8e59f03eae23b1c6327b @ivanvanderbyl committed Apr 16, 2012
Showing with 337 additions and 0 deletions.
  1. +165 −0 deploy.rb
  2. +152 −0 push.rb
  3. +13 −0 templates/repo.rb
  4. +7 −0 templates/task.rb
View
@@ -0,0 +1,165 @@
+dep 'ready for update.repo', :git_ref_data, :env do
+ env.default!(ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'production')
+ requires [
+ 'valid git_ref_data.repo'.with(git_ref_data),
+ 'clean.repo',
+ 'before deploy'.with(ref_info[:old_id], ref_info[:new_id], ref_info[:branch], env)
+ ]
+end
+
+dep 'up to date.repo', :git_ref_data, :env do
+ env.default!(ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'production')
+ requires [
+ 'on correct branch.repo'.with(ref_info[:branch]),
+ 'HEAD up to date.repo'.with(ref_info),
+ 'app bundled'.with(:root => '.', :env => env),
+
+ # This and 'after deploy' below are separated so the deps in 'current dir'
+ # they refer to load from the new code checked out by 'HEAD up to date.repo'.
+ # Normally it would be fine because dep loading is lazy, but the "if Dep('...')"
+ # checks trigger a source load when called.
+ 'on deploy'.with(ref_info[:old_id], ref_info[:new_id], ref_info[:branch], env),
+
+ 'app flagged for restart.task',
+ 'maintenance page down',
+ 'after deploy'.with(ref_info[:old_id], ref_info[:new_id], ref_info[:branch], env)
+ ]
+end
+
+dep 'before deploy', :old_id, :new_id, :branch, :env do
+ requires 'current dir:before deploy'.with(old_id, new_id, branch, env) if Dep('current dir:before deploy')
+end
+dep 'on deploy', :old_id, :new_id, :branch, :env do
+ requires 'current dir:on deploy'.with(old_id, new_id, branch, env) if Dep('current dir:on deploy')
+end
+dep 'after deploy', :old_id, :new_id, :branch, :env do
+ requires 'current dir:after deploy'.with(old_id, new_id, branch, env) if Dep('current dir:after deploy')
+end
+
+dep 'valid git_ref_data.repo', :git_ref_data do
+ met? {
+ git_ref_data[ref_data_regexp] || unmeetable!("Invalid git_ref_data '#{git_ref_data}'.")
+ }
+end
+
+dep 'clean.repo' do
+ requires 'no public directory'
+ setup {
+ # Clear git's internal cache, which sometimes says the repo is dirty when it isn't.
+ repo.repo_shell "git diff"
+ }
+ met? { repo.clean? || unmeetable!("The remote repo has local changes.") }
+ meet { repo.reset_hard! }
+end
+
+dep 'branch exists.repo', :branch do
+ met? {
+ repo.branches.include? branch
+ }
+ meet {
+ log_block "Creating #{branch}" do
+ repo.branch! branch
+ end
+ }
+end
+
+dep 'on correct branch.repo', :branch do
+ requires 'branch exists.repo'.with(branch)
+ met? {
+ repo.current_branch == branch
+ }
+ meet {
+ log_block "Checking out #{branch}" do
+ repo.checkout! branch
+ end
+ }
+end
+
+dep 'HEAD up to date.repo', :old_id, :new_id, :branch do
+ met? {
+ (repo.current_full_head == new_id && repo.clean?).tap {|result|
+ if result
+ log_ok "#{branch} is up to date at #{repo.current_head}."
+ else
+ log "#{branch} needs updating: #{old_id[0...7]}..#{new_id[0...7]}"
+ end
+ }
+ }
+ meet {
+ if old_id[/^0+$/]
+ log "Starting HEAD at #{new_id[0...7]} (a #{shell("git rev-list #{new_id} | wc -l").strip}-commit history) since the repo is blank."
+ else
+ log shell("git diff --stat #{old_id}..#{new_id}")
+ end
+ repo.reset_hard! new_id
+ }
+end
+
+dep 'app flagged for restart.task' do
+ run {
+ if File.exists? 'tmp/pids/unicorn.pid'
+ shell "kill -USR2 #{'tmp/pids/unicorn.pid'.p.read}"
+ else
+ shell "mkdir -p tmp && touch tmp/restart.txt"
+ end
+ }
+end
+
+dep 'maintenance page up' do
+ met? {
+ !'public/system/maintenance.html.off'.p.exists? or
+ 'public/system/maintenance.html'.p.exists?
+ }
+ meet { 'public/system/maintenance.html.off'.p.copy 'public/system/maintenance.html' }
+end
+
+dep 'maintenance page down' do
+ met? { !'public/system/maintenance.html'.p.exists? }
+ meet { 'public/system/maintenance.html'.p.rm }
+end
+
+dep 'when path changed', :path, :dep_spec, :old_id, :new_id, :env do
+ def effective_old_id
+ # If there is no initial commit (first push or branch change), git
+ # replace git's '0000000' with a parentless commit (usually there's
+ # just one, the initial repo commit).
+ old_id[/^0+$/] ? shell('git rev-list HEAD | tail -n1') : old_id
+ end
+ def pending
+ shell(
+ "git diff --numstat #{effective_old_id}..#{new_id}"
+ ).split("\n").grep(
+ /^[\d\s\-]+#{path}/
+ )
+ end
+ setup {
+ if pending.empty?
+ log "No changes within #{path.inspect} - not running '#{dep_spec}'."
+ else
+ log "#{pending.length} change#{'s' unless pending.length == 1} within #{path.inspect}:"
+ pending.each {|p| log p }
+
+ requires dep_spec.to_s.with(:env => env, :deploying => 'yes')
+ end
+ }
+end
+
+dep 'assets precompiled', :env, :deploying, :template => 'task' do
+ run {
+ shell "bundle exec rake assets:precompile:primary RAILS_GROUPS=assets RAILS_ENV=#{env}"
+ }
+end
+
+dep 'delayed job restarted', :template => 'task' do
+ run {
+ output = shell?('ps aux | grep "rake jobs:work" | grep -v grep')
+
+ if output.nil?
+ log "`rake jobs:work` isn't running."
+ true
+ else
+ shell "kill -s TERM #{output.scan(/^\w+\s+(\d+)\s+/).flatten.first}"
+ end
+ }
+end
+
View
@@ -0,0 +1,152 @@
+meta :push do
+ def repo
+ @repo ||= Babushka::GitRepo.new('.')
+ end
+ def self.remote_host_and_path remote
+ @remote_host_and_path ||= shell("git config remote.#{remote}.url").split(':', 2)
+ end
+ def self.remote_head remote
+ host, path = remote_host_and_path(remote)
+ @remote_head ||= shell!("ssh #{host} 'cd #{path} && git rev-parse --short HEAD 2>/dev/null || echo 0000000'")
+ end
+ def remote_host; self.class.remote_host_and_path(remote).first end
+ def remote_path; self.class.remote_host_and_path(remote).last end
+ def remote_head; self.class.remote_head(remote) end
+ def self.uncache_remote_head!
+ @remote_head = nil
+ end
+ def git_log from, to
+ if from[/^0+$/]
+ log "Starting #{remote} at #{to[0...7]} (a #{shell("git rev-list #{to} | wc -l").strip}-commit history) since the repo is blank."
+ else
+ log shell("git log --graph --pretty='format:%C(yellow)%h%Cblue%d%Creset %s %C(white) %an, %ar%Creset' #{from}..#{to}")
+ end
+ end
+end
+
+dep 'push!', :ref, :remote, :env do
+ ref.ask("What would you like to push?").default('HEAD')
+ env.default!(remote)
+
+ requires 'ready.push'
+ requires 'current dir:before push'.with(ref, remote, env) if Dep('current dir:before push')
+ requires 'pushed.push'.with(ref, remote)
+ requires 'schema up to date.push'.with(ref, remote, env)
+ requires 'marked on newrelic.task'.with(ref, env)
+ requires 'marked on airbrake.task'.with(ref, env)
+ requires 'current dir:after push'.with(ref, remote, env) if Dep('current dir:after push')
+end
+
+dep 'ready.push' do
+ met? {
+ state = [:dirty, :rebasing, :merging, :applying, :bisecting].detect {|s| repo.send("#{s}?") }
+ if !state.nil?
+ unmeetable! "The repo is #{state}."
+ else
+ log_ok "The repo is clean."
+ end
+ }
+end
+
+dep 'pushed.push', :ref, :remote do
+ ref.ask("What would you like to push?").default('HEAD')
+ remote.ask("Where would you like to push to?").choose(repo.repo_shell('git remote').split("\n"))
+ requires [
+ 'on origin.push'.with(ref),
+ 'ok to update.push'.with(ref, remote)
+ ]
+ met? {
+ (remote_head == shell("git rev-parse --short #{ref} 2>/dev/null")).tap {|result|
+ log "#{remote} is on #{remote_head}.", :as => (:ok if result)
+ }
+ }
+ meet {
+ git_log remote_head, ref
+ confirm "OK to push to #{remote} (#{repo.repo_shell("git config remote.#{remote}.url")})?" do
+ push_cmd = "git push #{remote} #{ref}:master -f"
+ log push_cmd.colorize("on grey") do
+ self.class.uncache_remote_head!
+ shell push_cmd, :log => true
+ end
+ end
+ }
+end
+
+dep 'schema up to date.push', :ref, :remote, :env do
+ def db_name
+ 'config/database.yml'.p.yaml[env.to_s]['database']
+ end
+ def dump_schema_cmd
+ pg_dump = "pg_dump #{db_name} --no-privileges --no-owner"
+ # Dump the schema, and then the schema_migrations table including its contents.
+ "#{pg_dump} --schema-only -T schema_migrations && #{pg_dump} -t schema_migrations"
+ end
+ def fetch_schema
+ shell "ssh #{remote_host} '#{dump_schema_cmd}' > db/schema.sql.tmp"
+ end
+ def move_schema_into_place
+ shell "mv db/schema.sql.tmp db/schema.sql"
+ end
+ setup {
+ # We fetch to a temporary file first and move it into place on ssh
+ # success, because a failed connection can result in an empty file.
+ fetch_schema and move_schema_into_place
+ }
+ met? {
+ Babushka::GitRepo.new('.').clean?
+ }
+ meet {
+ shell "git add db/schema.sql && git commit db/schema.sql -m 'Update DB schema after deploying #{shell("git rev-parse --short #{ref}")}.'"
+ }
+end
+
+dep 'marked on newrelic.task', :ref, :env do
+ requires 'app bundled'.with('.', 'development')
+ run {
+ if 'config/newrelic.yml'.p.exists?
+ shell "bundle exec newrelic deployments -e #{env} -r #{shell("git rev-parse --short #{ref}")}"
+ end
+ }
+end
+
+dep 'marked on airbrake.task', :ref, :env do
+ requires 'app bundled'.with('.', 'development')
+ run {
+ if 'config/initializers/airbrake.rb'.p.exists?
+ shell "bundle exec rake airbrake:deploy TO=#{env} REVISION=#{shell("git rev-parse --short #{ref}")} REPO=#{shell("git config remote.origin.url")} USER=#{shell('whoami')}"
+ end
+ }
+end
+
+dep 'ok to update.push', :ref, :remote do
+ met? {
+ if remote_head[/^0+$/]
+ log_ok "The remote repo is empty."
+ elsif !repo.repo_shell("git rev-parse #{remote_head}", &:ok?)
+ confirm "The current HEAD on #{remote}, #{remote_head}, isn't present locally. OK to push #{'(This is probably a bad idea)'.colorize('on red')}"
+ elsif shell("git merge-base #{ref} #{remote_head}", &:stdout)[0...7] != remote_head
+ confirm "Pushing #{ref} to #{remote} would not fast forward (#{remote} is on #{remote_head}). That OK?"
+ else
+ true
+ end
+ }
+end
+
+dep 'on origin.push', :ref do
+ requires 'remote exists.push'.with('origin')
+ met? {
+ shell("git branch -r --contains #{ref}").split("\n").map(&:strip).include? "origin/#{repo.current_branch}"
+ }
+ meet {
+ git_log "origin/#{repo.current_branch}", ref
+ confirm("#{ref} isn't pushed to origin/#{repo.current_branch} yet. Do that now?") do
+ shell("git push origin #{repo.current_branch}")
+ end
+ }
+end
+
+dep 'remote exists.push', :remote do
+ met? {
+ repo.repo_shell("git config remote.#{remote}.url") or log_error("The #{remote} remote isn't configured.")
+ }
+end
View
@@ -0,0 +1,13 @@
+meta :repo do
+ def repo
+ @repo ||= Babushka::GitRepo.new('.')
+ end
+ def ref_data_regexp
+ # Example: "83a90415670ec7ae4690d58563be628c73900716 e817f54d3e9a2d982b16328f8d7f0fbfcd7433f7 refs/heads/master"
+ /\A([\da-f]{40}) ([\da-f]{40}) refs\/[^\/]+\/(.+)\z/
+ end
+ def ref_info
+ old_id, new_id, branch = git_ref_data.to_s.scan(ref_data_regexp).flatten
+ {:old_id => old_id, :new_id => new_id, :branch => branch}
+ end
+end
View
@@ -0,0 +1,7 @@
+meta :task do
+ accepts_block_for :run
+ template {
+ met? { @run }
+ meet { invoke(:run).tap {|result| @run = result } }
+ }
+end

0 comments on commit 2b14ec1

Please sign in to comment.