Permalink
Browse files

big rewrite: scripts are now in per-project "deploy/" dir

  • Loading branch information...
1 parent cb7dc14 commit 013b39a7f54abed697ca393e196294eaa93cda4f @mislav committed Oct 3, 2011
View
@@ -5,10 +5,20 @@ Straightforward, [Heroku][]-style, push-based deployment. Your deploys will look
$ git push production master
-Assumptions are that you are deploying to a single host. Also, that you have Phusion Passenger on the server running your application.
-
To get started, install the "git-deploy" gem.
+ $ gem install git-deploy
+
+
+What application frameworks/languages are supported?
+----------------------------------------------------
+
+Regardless of the fact that this tool is mostly written in Ruby, git-deploy can be useful for any kind of code that needs deploying on a remote server. The default scripts are suited for Ruby web apps, but can be edited to accommodate other frameworks.
+
+Your deployment is customized with per-project callback scripts which can be written in any language.
+
+The assumption is that you're deploying to a single host to which you connect over SSH using public/private key authentication.
+
Setup steps
-----------
@@ -23,7 +33,17 @@ Setup steps
$ git deploy setup -r production
- This will initialize the remote git repository in the target directory ("/path/to/myapp" in the above example), install the remote git hooks and push the master branch to the server.
+ This will initialize the remote git repository in the target directory ("/path/to/myapp" in the above example) and install the remote git hooks.
+
+3. Run the init task:
+
+ $ git deploy init
+
+ This generates default deploy callback scripts in the "deploy/" directory. You must check them in version control. They are going to be executed on the server on each deploy.
+
+4. Push the code.
+
+ $ git push production master
3. Login to your server and manually perform necessary one-time administrative operations. This might include:
* set up the Apache/nginx virtual host for this application;
@@ -33,41 +53,34 @@ Setup steps
Deployment
----------
-After you've set everything up, visiting "http://example.com" in your browser should show your app up and running.
+If you've set your app correctly, visiting "http://example.com" in your browser should show it up and running.
Now, subsequent deployments are done simply by pushing to the branch that is currently checked out on the remote:
$ git push production master
-Deployments are logged to "log/deploy.log" in your application.
-
-On first push your server automatically:
+Because the deployments are done with git, not everyone on the team had to install git-deploy. Just the person who was doing the setup.
-1. creates the "log" and "tmp" directories;
-2. copies "config/database.example.yml" to "config/database.yml".
+Deployments are logged to "log/deploy.log" in your application.
-On every subsequent deploy, the "post-reset" script analyzes changes and:
+On every deploy, the "deploy/after_push" script performs the following:
-4. sync submodule urls if ".gitmodules" file has changed;
-5. initialize and update submodules;
-1. clears cached CSS/JS assets if any versioned files under "public/stylesheets" and "public/javascripts" have changed;
-2. runs `bundle install --deployment` if Gemfile or Gemfile.lock have been changed
+1. updates git submodules (if there are any);
+2. runs `bundle install --deployment` if there is a Gemfile;
3. runs `rake db:migrate` if new migrations have been added;
-6. `touch tmp/restart.txt` if app restart is needed.
-
-Finally, these are the conditions that trigger the app restart:
-
-1. some CSS/JS assets have been cleared;
-2. the database schema has been migrated;
-3. one or more files/submodules under "app", "config", "lib", "public", or "vendor" have changed.
+4. clears cached CSS/JS assets in "public/stylesheets" and "public/javascripts";
+5. restarts the web application.
+You can customize all of this by editing scripts in the "deploy/" directory of your app.
How it works
------------
-The "setup" task installed a couple of hooks in the remote git repository: "post-receive" and "post-reset". The former is a git hook which is invoked after every push to your server, while the latter is a *custom* hook that's called asynchronously by "post-receive" when we updated the deployment branch. This is how your working copy on the server is kept up-to-date.
+The "setup" task installed a "post-receive" hook in the remote git repository. This is how your working copy on the server is kept up to date. This hook, after checking out latest code, asynchronously dispatches to "deploy/after_push" script in your application. This script executes on the server and also calls "deploy/before_restart", "restart", and "after_restart" callbacks if they are present.
+
+These scripts are ordinary unix executable files. The ones which are generated for you are written in shell script and Ruby.
-It's worth knowing that "post-reset" is done **asynchronously from your push operation**. This is because migrating the database and updating submodules might take a long time and we don't want to wait for all that while we're doing a git push. But, this means that when the push is done, the server has *not yet restarted*. You might need to wait a few seconds or a minute.
+It's worth remembering that "after_push" is done **asynchronously from your git push**. This is because migrating the database and updating submodules might take a long time and you don't want to wait for all that during a git push. But, this means that when the push is done, the server has *not yet restarted*. You might need to wait a few seconds or a minute.
[heroku]: http://heroku.com/
View
@@ -5,43 +5,62 @@
class GitDeploy < Thor
LOCAL_DIR = File.expand_path('..', __FILE__)
+ require 'git_deploy/configuration'
+ require 'git_deploy/ssh_methods'
+ include Configuration
+ include SSHMethods
+
class_option :remote, :aliases => '-r', :type => :string, :default => 'origin'
class_option :noop, :aliases => '-n', :type => :boolean, :default => false
- desc "setup", "Create the remote git repository, install git hooks, push the code"
+ desc "init", "Generates deployment customization scripts for your app"
+ def init
+ require 'git_deploy/generator'
+ Generator::start([])
+ end
+
+ desc "setup", "Create the remote git repository and install push hooks for it"
method_option :shared, :aliases => '-g', :type => :boolean, :default => true
method_option :sudo, :aliases => '-s', :type => :boolean, :default => true
def setup
- sudo_cmd = options.sudo? ? 'sudo' : ''
-
- run ["#{sudo_cmd} mkdir -p #{deploy_to}"] do |cmd|
- cmd << "#{sudo_cmd} chown $USER #{deploy_to}" if options.sudo?
+ sudo = options.sudo? ? "#{sudo_cmd} " : ''
+
+ if run_test("test -x #{deploy_to}")
+ run ["#{sudo}mkdir -p #{deploy_to}"] do |cmd|
+ cmd << "#{sudo}chown $USER #{deploy_to}" if options.sudo?
+ end
+ end
+
+ run [] do |cmd|
cmd << "chmod g+ws #{deploy_to}" if options.shared?
cmd << "cd #{deploy_to}"
cmd << "git init #{options.shared? ? '--shared' : ''}"
cmd << "sed -i'' -e 's/master/#{branch}/' .git/HEAD" unless branch == 'master'
cmd << "git config --bool receive.denyNonFastForwards false" if options.shared?
cmd << "git config receive.denyCurrentBranch ignore"
end
-
+
invoke :hooks
- system 'git', 'push', options[:remote], branch
end
desc "hooks", "Installs git hooks to the remote repository"
def hooks
hooks_dir = File.join(LOCAL_DIR, 'hooks')
remote_dir = "#{deploy_to}/.git/hooks"
- scp_upload "#{hooks_dir}/post-receive.rb" => "#{remote_dir}/post-receive",
- "#{hooks_dir}/post-reset.rb" => "#{remote_dir}/post-reset"
-
- run "chmod +x #{remote_dir}/post-receive #{remote_dir}/post-reset"
+ scp_upload "#{hooks_dir}/post-receive.sh" => "#{remote_dir}/post-receive"
+ run "chmod +x #{remote_dir}/post-receive"
end
- desc "restart", "Restarts the application"
+ desc "restart", "Restarts the application on the server"
def restart
- run "touch #{deploy_to}/tmp/restart.txt"
+ run "cd #{deploy_to} && deploy/restart | tee -a log/deploy.log"
+ end
+
+ desc "rollback", "Rolls back the checkout to before the last push"
+ def rollback
+ run "cd #{deploy_to} && git reset --hard ORIG_HEAD"
+ invoke :restart
end
desc "log [n=20]", "Shows the last part of the deploy log on the server"
@@ -59,125 +78,6 @@ def upload(*files)
all
}
end
-
- private
-
- def host
- extract_host_and_user unless defined? @host
- @host
- end
-
- def remote_user
- extract_host_and_user unless defined? @user
- @user
- end
-
- def extract_host_and_user
- info = remote_url.split(':').first.split('@')
- if info.size < 2
- @user, @host = `whoami`.chomp, info.first
- else
- @user, @host = *info
- end
- end
-
- def deploy_to
- @deploy_to ||= remote_url.split(':').last
- end
-
- def branch
- 'master'
- end
-
- def run(cmd = nil)
- cmd = yield(cmd) if block_given?
- cmd = cmd.join(' && ') if Array === cmd
- ssh_exec cmd
- end
-
- def system(*args)
- puts "[local] $ " + args.join(' ').gsub(' && ', " && \\\n ")
- super unless options.noop?
- end
-
- def ssh_exec(cmd)
- puts "[#{options[:remote]}] $ " + cmd.gsub(' && ', " && \\\n ")
-
- ssh_connection.exec!(cmd) do |channel, stream, data|
- case stream
- when :stdout then $stdout.puts data
- when :stderr then $stderr.puts data
- else
- raise "unknown stream: #{stream.inspect}"
- end
- end unless options.noop?
- end
-
- def scp_upload(files)
- channels = []
- files.each do |local, remote|
- puts "FILE: [local] #{local.sub(LOCAL_DIR + '/', '')} -> [#{options[:remote]}] #{remote}"
- channels << ssh_connection.scp.upload(local, remote) unless options.noop?
- end
- channels.each { |c| c.wait }
- end
-
- def ssh_connection
- @ssh ||= begin
- ssh = Net::SSH.start(host, remote_user)
- at_exit { ssh.close }
- ssh
- end
- end
-
- def git_config
- @git_config ||= Hash.new do |cache, cmd|
- git = ENV['GIT'] || 'git'
- out = `#{git} #{cmd}`
- if $?.success? then cache[cmd] = out.chomp
- else cache[cmd] = nil
- end
- cache[cmd]
- end
- end
-
- def remote_urls(remote)
- git_config["config --get-all remote.#{remote}.url"].to_s.split("\n")
- end
-
- def remote_url(remote = options[:remote])
- @remote_url ||= {}
- @remote_url[remote] ||= begin
- url = remote_urls(remote).first
- if url.nil?
- abort "Error: Remote url not found for remote #{remote.inspect}"
- elsif url =~ /\bgithub\.com\b/
- abort "Error: Remote url for #{remote.inspect} points to GitHub. Can't deploy there!"
- end
- url
- end
- end
-
- def current_branch
- git_config['symbolic-ref -q HEAD']
- end
-
- def tracked_branch
- branch = current_branch && tracked_for(current_branch)
- normalize_branch(branch) if branch
- end
-
- def normalize_branch(branch)
- branch.sub('refs/heads/', '')
- end
-
- def remote_for(branch)
- git_config['config branch.%s.remote' % normalize_branch(branch)]
- end
-
- def tracked_for(branch)
- git_config['config branch.%s.merge' % normalize_branch(branch)]
- end
end
__END__
@@ -0,0 +1,81 @@
+class GitDeploy
+ module Configuration
+ private
+
+ def host
+ extract_host_and_user unless defined? @host
+ @host
+ end
+
+ def remote_user
+ extract_host_and_user unless defined? @user
+ @user
+ end
+
+ def extract_host_and_user
+ info = remote_url.split(':').first.split('@')
+ if info.size < 2
+ @user, @host = `whoami`.chomp, info.first
+ else
+ @user, @host = *info
+ end
+ end
+
+ def deploy_to
+ @deploy_to ||= remote_url.split(':').last
+ end
+
+ def branch
+ 'master'
+ end
+
+ def git_config
+ @git_config ||= Hash.new do |cache, cmd|
+ git = ENV['GIT'] || 'git'
+ out = `#{git} #{cmd}`
+ if $?.success? then cache[cmd] = out.chomp
+ else cache[cmd] = nil
+ end
+ cache[cmd]
+ end
+ end
+
+ def remote_urls(remote)
+ git_config["config --get-all remote.#{remote}.url"].to_s.split("\n")
+ end
+
+ def remote_url(remote = options[:remote])
+ @remote_url ||= {}
+ @remote_url[remote] ||= begin
+ url = remote_urls(remote).first
+ if url.nil?
+ abort "Error: Remote url not found for remote #{remote.inspect}"
+ elsif url =~ /\bgithub\.com\b/
+ abort "Error: Remote url for #{remote.inspect} points to GitHub. Can't deploy there!"
+ end
+ url
+ end
+ end
+
+ def current_branch
+ git_config['symbolic-ref -q HEAD']
+ end
+
+ def tracked_branch
+ branch = current_branch && tracked_for(current_branch)
+ normalize_branch(branch) if branch
+ end
+
+ def normalize_branch(branch)
+ branch.sub('refs/heads/', '')
+ end
+
+ def remote_for(branch)
+ git_config['config branch.%s.remote' % normalize_branch(branch)]
+ end
+
+ def tracked_for(branch)
+ git_config['config branch.%s.merge' % normalize_branch(branch)]
+ end
+ end
+end
Oops, something went wrong.

0 comments on commit 013b39a

Please sign in to comment.