git-deploy 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -1,109 +1,86 @@
1
- Capistrano strategy for smart git deployment
2
- ============================================
1
+ Easy git deployment
2
+ ===================
3
3
 
4
- Let's set up a straightforward, [Heroku][]-style, push-based deployment, shall we? The goal is that our deployment looks like this:
4
+ Straightforward, [Heroku][]-style, push-based deployment. Your deploys will look like this:
5
5
 
6
- $ git push origin production
6
+ $ git push production master
7
7
 
8
- Assumptions are that you are using git for your Rails app and Passenger on the server. For now, we're going to deploy on a single host.
8
+ To get started, install the "git-deploy" gem.
9
9
 
10
- To get started, install the "git-deploy" gem from Gemcutter.org.
10
+ $ gem install git-deploy
11
11
 
12
12
 
13
- Considerations
14
- --------------
13
+ What application frameworks/languages are supported?
14
+ ----------------------------------------------------
15
15
 
16
- This is not "yet another Capistrano strategy". Capistrano is only used for setup, after which it's all git hooks (see detailed description in "Deployment"). This actually replaces the default Capistrano recipe (which is not loaded) with only a basic set of tasks. If you have more advanced deployment (multiple hosts, many "after deploy" hooks) then this library cannot suit your needs at present.
16
+ 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.
17
+
18
+ Your deployment is customized with per-project callback scripts which can be written in any language.
19
+
20
+ The assumption is that you're deploying to a single host to which you connect over SSH using public/private key authentication.
17
21
 
18
22
 
19
23
  Setup steps
20
24
  -----------
21
25
 
22
- 1. Create a git remote for where you'll push the code on your server. The name of this remote in the examples is "origin", but it can be whatever you wish ("online", "website", or other).
26
+ 1. Create a git remote for where you'll push the code on your server. The name of this remote in the examples is "production", but it can be whatever you wish ("online", "website", or other).
23
27
 
24
- $ git remote add origin user@example.com:/path/to/myapp
28
+ $ git remote add production user@example.com:/path/to/myapp
25
29
 
26
30
  The "/path/to/myapp" is the directory where your code will reside. It doesn't have to exist; it will be created for you during this setup.
27
31
 
28
- 2. Create/overwrite the following files in your project:
29
-
30
- **config/deploy.rb** (entire file):
32
+ 2. Run the setup task:
31
33
 
32
- # set to the name of git remote you intend to deploy to
33
- set :remote, "origin"
34
- # specify the deployment branch
35
- set :branch, "master"
36
- # sudo will only be used to create the deployment directory
37
- set :use_sudo, true
38
- # the remote host is read automatically from your git remote specification
39
- server remote_host, :app, :web, :db, :primary => true
34
+ $ git deploy setup -r production
40
35
 
41
- **Capfile**:
42
-
43
- require 'git_deploy'
44
- load 'config/deploy'
45
-
46
- Test it by running `cap -T`. You should see several deploy tasks listed.
36
+ This will initialize the remote git repository in the target directory ("/path/to/myapp" in the above example) and install the remote git hooks.
47
37
 
48
- 3. Run the setup task:
38
+ 3. Run the init task:
49
39
 
50
- $ cap deploy:setup
40
+ $ git deploy init
51
41
 
52
- This will initialize a git repository in the target directory, install the push hook and push the branch you specified to the server.
42
+ 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.
43
+
44
+ 4. Push the code.
53
45
 
54
- 4. Login to your server to perform necessary one-time administrative operations. This might include:
46
+ $ git push production master
47
+
48
+ 3. Login to your server and manually perform necessary one-time administrative operations. This might include:
55
49
  * set up the Apache/nginx virtual host for this application;
56
- * check out the branch which you will push production code into (often this is "production");
57
- * check your config/database.yml and create or import the production database.
50
+ * check your "config/database.yml" and create the production database.
58
51
 
59
52
 
60
53
  Deployment
61
54
  ----------
62
55
 
63
- After you've set everything up, visiting "http://example.com" in your browser should show your app up and running. Subsequent deployments are done simply by pushing to the branch that is currently checked out on our server (see step 4.). The branch is by default "master", but it's suggested to have production code in another branch like "production" or other. This, of course, depends on your git workflow.
64
-
65
- We've reached our goal; our deployment now looks like:
66
-
67
- $ git push origin production
68
-
69
- In fact, running "cap deploy" does exactly this. So what does it do?
70
-
71
- The "deploy: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.
72
-
73
- Thus, on first push your server automatically:
56
+ If you've set your app correctly, visiting "http://example.com" in your browser should show it up and running.
74
57
 
75
- 1. creates the "log" and "tmp" directories;
76
- 2. copies "config/database.example.yml" or "config/database.yml.example" to "config/database.yml".
58
+ Now, subsequent deployments are done simply by pushing to the branch that is currently checked out on the remote:
77
59
 
78
- On every subsequent deploy, the "post-reset" script analyzes changes and:
60
+ $ git push production master
79
61
 
80
- 1. clears cached css and javascript assets if any versioned files under "public/stylesheets" and "public/javascripts" have changed, respectively;
81
- 2. runs "rake db:migrate" if new migrations have been added;
82
- 3. sync submodule urls if ".gitmodules" file has changed;
83
- 4. initialize and update submodules;
84
- 5. touches "tmp/restart.txt" if app restart is needed.
62
+ 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.
85
63
 
86
- Finally, these are the conditions that dictate an app restart:
64
+ Deployments are logged to "log/deploy.log" in your application.
87
65
 
88
- 1. css/javascript assets have been cleared;
89
- 2. the database has migrated;
90
- 3. one or more files/submodules under "app", "config", "lib", "public", or "vendor" changed.
66
+ On every deploy, the "deploy/after_push" script performs the following:
91
67
 
92
- The output of "post-reset" is logged to "log/deploy.log" in your application.
68
+ 1. updates git submodules (if there are any);
69
+ 2. runs `bundle install --deployment` if there is a Gemfile;
70
+ 3. runs `rake db:migrate` if new migrations have been added;
71
+ 4. clears cached CSS/JS assets in "public/stylesheets" and "public/javascripts";
72
+ 5. restarts the web application.
93
73
 
94
- It's worth remembering 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, depending on what you pushed.
74
+ You can customize all of this by editing scripts in the "deploy/" directory of your app.
95
75
 
76
+ How it works
77
+ ------------
96
78
 
97
- In the future
98
- -------------
79
+ 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.
99
80
 
100
- Next steps for this library are:
81
+ These scripts are ordinary unix executable files. The ones which are generated for you are written in shell script and Ruby.
101
82
 
102
- * Support for deployment on multiple hosts. This is a slightly different strategy based on git pull instead of push; something in-between regular "remote cache" strategy and the aforementioned
103
- * Better configurability
104
- * Steps forward to supporting more existing 3rd-party Capistrano tasks, like that of the EngineYard gem
105
- * Support for multiple environments on the same server (production, staging, continuous integration, etc.) sharing the same git repo, so you don't have to push same objects twice
106
- * Automatic submodule conflict resolving
83
+ 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.
107
84
 
108
85
 
109
- [heroku]: http://heroku.com/
86
+ [heroku]: http://heroku.com/
data/bin/git-deploy ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'git_deploy'
3
+ GitDeploy.start
data/lib/git_deploy.rb CHANGED
@@ -1,121 +1,93 @@
1
- require 'capistrano/recipes/deploy/scm/git'
1
+ require 'thor'
2
+ require 'net/ssh'
3
+ require 'net/scp'
2
4
 
3
- Capistrano::Configuration.instance(true).load do
4
- def _cset(name, *args, &block)
5
- unless exists?(name)
6
- set(name, *args, &block)
7
- end
8
- end
5
+ class GitDeploy < Thor
6
+ LOCAL_DIR = File.expand_path('..', __FILE__)
9
7
 
10
- _cset(:application) { abort "Please specify the name of your application, set :application, 'foo'" }
11
- _cset :remote, "origin"
12
- _cset :branch, "master"
13
-
14
- _cset(:multiple_hosts) { roles.values.map{ |v| v.servers}.flatten.uniq.size > 1 }
15
- _cset(:repository) { `#{ source.local.scm('config', "remote.#{remote}.url") }`.chomp }
16
- _cset(:remote_host) { repository.split(':', 2).first }
17
- _cset(:deploy_to) { repository.split(':', 2).last }
18
- _cset(:run_method) { fetch(:use_sudo, true) ? :sudo : :run }
19
- _cset :group_writeable, false
20
-
21
- _cset(:current_branch) { File.read('.git/HEAD').chomp.split(' refs/heads/').last }
22
- _cset(:revision) { branch }
23
- _cset(:source) { Capistrano::Deploy::SCM::Git.new(self) }
24
-
25
- # If :run_method is :sudo (or :use_sudo is true), this executes the given command
26
- # via +sudo+. Otherwise is uses +run+. If :as is given as a key, it will be
27
- # passed as the user to sudo as, if using sudo. If the :as key is not given,
28
- # it will default to whatever the value of the :admin_runner variable is,
29
- # which (by default) is unset.
30
- def try_sudo(*args)
31
- options = args.last.is_a?(Hash) ? args.pop : {}
32
- command = args.shift
33
- raise ArgumentError, "too many arguments" if args.any?
34
-
35
- as = options.fetch(:as, fetch(:admin_runner, nil))
36
-
37
- if command
38
- invoke_command(command, :via => run_method, :as => as)
39
- elsif :sudo == run_method
40
- sudo(:as => as)
41
- else
42
- ""
43
- end
44
- end
8
+ require 'git_deploy/configuration'
9
+ require 'git_deploy/ssh_methods'
10
+ include Configuration
11
+ include SSHMethods
45
12
 
46
- namespace :deploy do
47
- desc "Deploys your project."
48
- task :default do
49
- unless multiple_hosts
50
- push
51
- else
52
- code
53
- command = ["cd #{deploy_to}"]
54
- command << ".git/hooks/post-reset `cat .git/ORIG_HEAD` HEAD 2>&1 | tee -a log/deploy.log"
55
-
56
- run command.join(' && ')
57
- end
58
- end
13
+ class_option :remote, :aliases => '-r', :type => :string, :default => 'origin'
14
+ class_option :noop, :aliases => '-n', :type => :boolean, :default => false
59
15
 
60
- task :push do
61
- system source.local.scm('push', remote, "#{revision}:#{branch}")
62
- end
16
+ desc "init", "Generates deployment customization scripts for your app"
17
+ def init
18
+ require 'git_deploy/generator'
19
+ Generator::start([])
20
+ end
21
+
22
+ desc "setup", "Create the remote git repository and install push hooks for it"
23
+ method_option :shared, :aliases => '-g', :type => :boolean, :default => true
24
+ method_option :sudo, :aliases => '-s', :type => :boolean, :default => true
25
+ def setup
26
+ sudo = options.sudo? ? "#{sudo_cmd} " : ''
63
27
 
64
- task :code do
65
- command = ["cd #{deploy_to}"]
66
- command << source.scm('fetch', remote, "+refs/heads/#{branch}:refs/remotes/origin/#{branch}")
67
- command << source.scm('reset', '--hard', "origin/#{branch}")
68
-
69
- run command.join(' && ')
28
+ if run_test("test -x #{deploy_to}")
29
+ run ["#{sudo}mkdir -p #{deploy_to}"] do |cmd|
30
+ cmd << "#{sudo}chown $USER #{deploy_to}" if options.sudo?
31
+ end
70
32
  end
71
33
 
72
- desc "Prepares servers for deployment."
73
- task :setup do
74
- shared = fetch(:group_writeable)
75
-
76
- command = ["#{try_sudo} mkdir -p #{deploy_to}"]
77
- command << "#{try_sudo} chown $USER #{deploy_to}" if :sudo == run_method
78
- command << "chmod g+ws #{deploy_to}" if shared
79
- command << "cd #{deploy_to}"
80
- command << "git init #{shared ? '--shared' : ''}"
81
- command << "sed -i'' -e 's/master/#{branch}/' .git/HEAD" unless branch == 'master'
82
- command << "git config --bool receive.denyNonFastForwards false" if shared
83
- command << "git config receive.denyCurrentBranch ignore"
84
- run command.join(' && ')
85
-
86
- install_hooks
87
- push
34
+ run [] do |cmd|
35
+ cmd << "chmod g+ws #{deploy_to}" if options.shared?
36
+ cmd << "cd #{deploy_to}"
37
+ cmd << "git init #{options.shared? ? '--shared' : ''}"
38
+ cmd << "sed -i'' -e 's/master/#{branch}/' .git/HEAD" unless branch == 'master'
39
+ cmd << "git config --bool receive.denyNonFastForwards false" if options.shared?
40
+ cmd << "git config receive.denyCurrentBranch ignore"
88
41
  end
89
42
 
90
- task :install_hooks do
91
- dir = File.dirname(__FILE__) + '/hooks'
92
- remote_dir = "#{deploy_to}/.git/hooks"
43
+ invoke :hooks
44
+ end
93
45
 
94
- top.upload "#{dir}/post-receive.rb", "#{remote_dir}/post-receive"
95
- top.upload "#{dir}/post-reset.rb", "#{remote_dir}/post-reset"
96
- run "chmod +x #{remote_dir}/post-receive #{remote_dir}/post-reset"
97
- end
46
+ desc "hooks", "Installs git hooks to the remote repository"
47
+ def hooks
48
+ hooks_dir = File.join(LOCAL_DIR, 'hooks')
49
+ remote_dir = "#{deploy_to}/.git/hooks"
98
50
 
99
- desc "Restarts your Passenger application."
100
- task :restart, :roles => :app do
101
- run "touch #{deploy_to}/tmp/restart.txt"
102
- end
51
+ scp_upload "#{hooks_dir}/post-receive.sh" => "#{remote_dir}/post-receive"
52
+ run "chmod +x #{remote_dir}/post-receive"
53
+ end
54
+
55
+ desc "restart", "Restarts the application on the server"
56
+ def restart
57
+ run "cd #{deploy_to} && deploy/restart | tee -a log/deploy.log"
58
+ end
103
59
 
104
- desc <<-DESC
105
- Copy files to the currently deployed version. Use a comma-separated \
106
- list in FILES to specify which files to upload.
60
+ desc "rollback", "Rolls back the checkout to before the last push"
61
+ def rollback
62
+ run "cd #{deploy_to} && git reset --hard ORIG_HEAD"
63
+ invoke :restart
64
+ end
107
65
 
108
- Note that unversioned files on your server are likely to be \
109
- overwritten by the next push. Always persist your changes by committing.
66
+ desc "log [n=20]", "Shows the last part of the deploy log on the server"
67
+ def log(n = 20)
68
+ run "tail -n#{n} #{deploy_to}/log/deploy.log"
69
+ end
110
70
 
111
- $ cap deploy:upload FILES=templates,controller.rb
112
- $ cap deploy:upload FILES='config/apache/*.conf'
113
- DESC
114
- task :upload do
115
- files = (ENV["FILES"] || "").split(",").map { |f| Dir[f.strip] }.flatten
116
- abort "Please specify at least one file or directory to update (via the FILES environment variable)" if files.empty?
71
+ desc "upload <files>", "Copy local files to the remote app"
72
+ def upload(*files)
73
+ files = files.map { |f| Dir[f.strip] }.flatten
74
+ abort "Error: Specify at least one file to upload" if files.empty?
117
75
 
118
- files.each { |file| top.upload(file, File.join(deploy_to, file)) }
119
- end
76
+ scp_upload files.inject({}) { |all, file|
77
+ all[file] = File.join(deploy_to, file)
78
+ all
79
+ }
120
80
  end
121
- end
81
+ end
82
+
83
+ __END__
84
+ Multiple hosts:
85
+ # deploy:
86
+ invoke :code
87
+ command = ["cd #{deploy_to}"]
88
+ command << ".git/hooks/post-reset `cat .git/ORIG_HEAD` HEAD 2>&1 | tee -a log/deploy.log"
89
+
90
+ # code:
91
+ command = ["cd #{deploy_to}"]
92
+ command << source.scm('fetch', remote, "+refs/heads/#{branch}:refs/remotes/origin/#{branch}")
93
+ command << source.scm('reset', '--hard', "origin/#{branch}")
@@ -0,0 +1,81 @@
1
+ class GitDeploy
2
+ module Configuration
3
+ private
4
+
5
+ def host
6
+ extract_host_and_user unless defined? @host
7
+ @host
8
+ end
9
+
10
+ def remote_user
11
+ extract_host_and_user unless defined? @user
12
+ @user
13
+ end
14
+
15
+ def extract_host_and_user
16
+ info = remote_url.split(':').first.split('@')
17
+ if info.size < 2
18
+ @user, @host = `whoami`.chomp, info.first
19
+ else
20
+ @user, @host = *info
21
+ end
22
+ end
23
+
24
+ def deploy_to
25
+ @deploy_to ||= remote_url.split(':').last
26
+ end
27
+
28
+ def branch
29
+ 'master'
30
+ end
31
+
32
+ def git_config
33
+ @git_config ||= Hash.new do |cache, cmd|
34
+ git = ENV['GIT'] || 'git'
35
+ out = `#{git} #{cmd}`
36
+ if $?.success? then cache[cmd] = out.chomp
37
+ else cache[cmd] = nil
38
+ end
39
+ cache[cmd]
40
+ end
41
+ end
42
+
43
+ def remote_urls(remote)
44
+ git_config["config --get-all remote.#{remote}.url"].to_s.split("\n")
45
+ end
46
+
47
+ def remote_url(remote = options[:remote])
48
+ @remote_url ||= {}
49
+ @remote_url[remote] ||= begin
50
+ url = remote_urls(remote).first
51
+ if url.nil?
52
+ abort "Error: Remote url not found for remote #{remote.inspect}"
53
+ elsif url =~ /\bgithub\.com\b/
54
+ abort "Error: Remote url for #{remote.inspect} points to GitHub. Can't deploy there!"
55
+ end
56
+ url
57
+ end
58
+ end
59
+
60
+ def current_branch
61
+ git_config['symbolic-ref -q HEAD']
62
+ end
63
+
64
+ def tracked_branch
65
+ branch = current_branch && tracked_for(current_branch)
66
+ normalize_branch(branch) if branch
67
+ end
68
+
69
+ def normalize_branch(branch)
70
+ branch.sub('refs/heads/', '')
71
+ end
72
+
73
+ def remote_for(branch)
74
+ git_config['config branch.%s.remote' % normalize_branch(branch)]
75
+ end
76
+
77
+ def tracked_for(branch)
78
+ git_config['config branch.%s.merge' % normalize_branch(branch)]
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,28 @@
1
+ require 'thor/group'
2
+
3
+ class GitDeploy::Generator < Thor::Group
4
+ include Thor::Actions
5
+
6
+ def self.source_root
7
+ File.expand_path('../templates', __FILE__)
8
+ end
9
+
10
+ def copy_main_hook
11
+ copy_hook 'after_push.sh', 'deploy/after_push'
12
+ end
13
+
14
+ def copy_restart_hook
15
+ copy_hook 'restart.sh', 'deploy/restart'
16
+ end
17
+
18
+ def copy_restart_callbacks
19
+ copy_hook 'before_restart.rb', 'deploy/before_restart'
20
+ end
21
+
22
+ private
23
+
24
+ def copy_hook(template, destination)
25
+ copy_file template, destination
26
+ chmod destination, 0744 unless File.executable? destination
27
+ end
28
+ end
@@ -0,0 +1,99 @@
1
+ class GitDeploy
2
+ module SSHMethods
3
+ private
4
+
5
+ def sudo_cmd
6
+ "sudo -p 'sudo password: '"
7
+ end
8
+
9
+ def system(*args)
10
+ puts "[local] $ " + args.join(' ').gsub(' && ', " && \\\n ")
11
+ super unless options.noop?
12
+ end
13
+
14
+ def run(cmd = nil)
15
+ cmd = yield(cmd) if block_given?
16
+ cmd = cmd.join(' && ') if Array === cmd
17
+ puts "[#{options[:remote]}] $ " + cmd.gsub(' && ', " && \\\n ")
18
+
19
+ unless options.noop?
20
+ status, output = ssh_exec cmd do |ch, stream, data|
21
+ case stream
22
+ when :stdout then $stdout.print data
23
+ when :stderr then $stderr.print data
24
+ end
25
+ ch.send_data(askpass) if data =~ /^sudo password: /
26
+ end
27
+ output
28
+ end
29
+ end
30
+
31
+ def run_test(cmd)
32
+ status, output = ssh_exec(cmd) { }
33
+ status == 0
34
+ end
35
+
36
+ def ssh_exec(cmd, &block)
37
+ status = nil
38
+ output = ''
39
+
40
+ channel = ssh_connection.open_channel do |chan|
41
+ chan.exec(cmd) do |ch, success|
42
+ raise "command failed: #{cmd.inspect}" unless success
43
+ # ch.request_pty
44
+
45
+ ch.on_data do |c, data|
46
+ output << data
47
+ yield(c, :stdout, data)
48
+ end
49
+
50
+ ch.on_extended_data do |c, type, data|
51
+ output << data
52
+ yield(c, :stderr, data)
53
+ end
54
+
55
+ ch.on_request "exit-status" do |ch, data|
56
+ status = data.read_long
57
+ end
58
+ end
59
+ end
60
+
61
+ channel.wait
62
+ [status, output]
63
+ end
64
+
65
+ # TODO: use Highline for cross-platform support
66
+ def askpass
67
+ tty_state = `stty -g`
68
+ system 'stty raw -echo -icanon isig' if $?.success?
69
+ pass = ''
70
+ while char = $stdin.getbyte and not (char == 13 or char == 10)
71
+ if char == 127 or char == 8
72
+ pass[-1,1] = '' unless pass.empty?
73
+ else
74
+ pass << char.chr
75
+ end
76
+ end
77
+ pass
78
+ ensure
79
+ system "stty #{tty_state}" unless tty_state.empty?
80
+ end
81
+
82
+ def scp_upload(files)
83
+ channels = []
84
+ files.each do |local, remote|
85
+ puts "FILE: [local] #{local.sub(LOCAL_DIR + '/', '')} -> [#{options[:remote]}] #{remote}"
86
+ channels << ssh_connection.scp.upload(local, remote) unless options.noop?
87
+ end
88
+ channels.each { |c| c.wait }
89
+ end
90
+
91
+ def ssh_connection
92
+ @ssh ||= begin
93
+ ssh = Net::SSH.start(host, remote_user)
94
+ at_exit { ssh.close }
95
+ ssh
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+ oldrev=$1
4
+ newrev=$2
5
+
6
+ run() {
7
+ [ -x $1 ] && $1 $oldrev $newrev
8
+ }
9
+
10
+ echo files changed: $(git diff $oldrev $newrev --diff-filter=ACDMR --name-only | wc -l)
11
+
12
+ umask 002
13
+
14
+ git submodule init && git submodule sync && git submodule update
15
+
16
+ run deploy/before_restart
17
+ run deploy/restart && run deploy/after_restart
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ oldrev, newrev = ARGV
3
+
4
+ def run(cmd)
5
+ exit($?.exitstatus) unless system "umask 002 && #{cmd}"
6
+ end
7
+
8
+ RAILS_ENV = ENV['RAILS_ENV'] || 'production'
9
+ use_bundler = File.file? 'Gemfile'
10
+ rake_cmd = use_bundler ? 'bundle exec rake' : 'rake'
11
+
12
+ # update gem bundle
13
+ run "bundle install --deployment" if use_bundler
14
+
15
+ if File.file? 'Rakefile'
16
+ num_migrations = `git diff #{oldrev} #{newrev} --diff-filter=A --name-only`.split("\n").size
17
+ # run migrations if new ones have been added
18
+ run "#{rake_cmd} db:migrate RAILS_ENV=#{RAILS_ENV}" if num_migrations > 0
19
+ end
20
+
21
+ # clear cached assets (unversioned/ignored files)
22
+ run "git clean -x -f -- public/stylesheets public/javascripts"
23
+
24
+ # clean unversioned files from vendor/plugins (e.g. old submodules)
25
+ run "git clean -d -f -- vendor/plugins"
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ touch tmp/restart.txt
3
+ echo "restarting Passenger app"
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ if [ "$GIT_DIR" = "." ]; then
3
+ # The script has been called as a hook; chdir to the working copy
4
+ cd ..
5
+ GIT_DIR=.git
6
+ export GIT_DIR
7
+ fi
8
+
9
+ # try to obtain the usual system PATH
10
+ if [ -f /etc/profile ]; then
11
+ PATH=$(source /etc/profile; echo $PATH)
12
+ export PATH
13
+ fi
14
+
15
+ # get the current branch
16
+ head="$(git symbolic-ref HEAD)"
17
+ # abort if we're on a detached head
18
+ [ "$?" != "0" ] && exit 1
19
+
20
+ # read the STDIN to detect if this push changed the current branch
21
+ while read oldrev newrev refname
22
+ do
23
+ [ "$refname" = "$head" ] && break
24
+ done
25
+ # abort if there's no update, or in case the branch is deleted
26
+ [ -z "${newrev//0}" ] && exit
27
+
28
+ # check out the latest code into the working copy
29
+ umask 002
30
+ git reset --hard
31
+
32
+ logfile=log/deploy.log
33
+ restart=tmp/restart.txt
34
+
35
+ if [ -z "${oldrev//0}" ]; then
36
+ # this is the first push; this branch was just created
37
+ mkdir -p log tmp
38
+ chmod 0775 log tmp
39
+ touch $logfile $restart
40
+ chmod 0664 $logfile $restart
41
+
42
+ # init submodules
43
+ git submodule update --init | tee -a $logfile
44
+ else
45
+ # log timestamp
46
+ echo ==== $(date) ==== >> $logfile
47
+
48
+ # execute the deploy hook in background
49
+ [ -x deploy/after_push ] && nohup deploy/after_push $oldrev $newrev 1>>$logfile 2>>$logfile &
50
+ fi
metadata CHANGED
@@ -1,86 +1,90 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: git-deploy
3
- version: !ruby/object:Gem::Version
4
- hash: 13
5
- prerelease: false
6
- segments:
7
- - 0
8
- - 4
9
- - 1
10
- version: 0.4.1
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ prerelease:
11
6
  platform: ruby
12
- authors:
13
- - "Mislav Marohni\xC4\x87"
7
+ authors:
8
+ - Mislav Marohnić
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2010-10-08 00:00:00 +02:00
19
- default_executable:
20
- dependencies:
21
- - !ruby/object:Gem::Dependency
22
- name: capistrano
12
+ date: 2011-10-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: thor
16
+ requirement: &70292395458180 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
23
  prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
24
+ version_requirements: *70292395458180
25
+ - !ruby/object:Gem::Dependency
26
+ name: net-ssh
27
+ requirement: &70292395455260 !ruby/object:Gem::Requirement
25
28
  none: false
26
- requirements:
27
- - - ~>
28
- - !ruby/object:Gem::Version
29
- hash: 9
30
- segments:
31
- - 2
32
- - 5
33
- - 9
34
- version: 2.5.9
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
35
33
  type: :runtime
36
- version_requirements: *id001
37
- description: A tool to install useful git hooks on your remote repository to enable push-based, Heroku-like deployment on your host.
34
+ prerelease: false
35
+ version_requirements: *70292395455260
36
+ - !ruby/object:Gem::Dependency
37
+ name: net-scp
38
+ requirement: &70292395453880 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70292395453880
47
+ description: A tool to install useful git hooks on your remote repository to enable
48
+ push-based, Heroku-like deployment on your host.
38
49
  email: mislav.marohnic@gmail.com
39
- executables: []
40
-
50
+ executables:
51
+ - git-deploy
41
52
  extensions: []
42
-
43
53
  extra_rdoc_files: []
44
-
45
- files:
54
+ files:
55
+ - bin/git-deploy
56
+ - lib/git_deploy/configuration.rb
57
+ - lib/git_deploy/generator.rb
58
+ - lib/git_deploy/ssh_methods.rb
59
+ - lib/git_deploy/templates/after_push.sh
60
+ - lib/git_deploy/templates/before_restart.rb
61
+ - lib/git_deploy/templates/restart.sh
46
62
  - lib/git_deploy.rb
47
- - lib/hooks/post-receive.rb
48
- - lib/hooks/post-reset.rb
63
+ - lib/hooks/post-receive.sh
49
64
  - README.markdown
50
65
  - LICENSE
51
- has_rdoc: false
52
- homepage: http://github.com/mislav/git-deploy
66
+ homepage: https://github.com/mislav/git-deploy
53
67
  licenses: []
54
-
55
68
  post_install_message:
56
69
  rdoc_options: []
57
-
58
- require_paths:
70
+ require_paths:
59
71
  - lib
60
- required_ruby_version: !ruby/object:Gem::Requirement
72
+ required_ruby_version: !ruby/object:Gem::Requirement
61
73
  none: false
62
- requirements:
63
- - - ">="
64
- - !ruby/object:Gem::Version
65
- hash: 3
66
- segments:
67
- - 0
68
- version: "0"
69
- required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
79
  none: false
71
- requirements:
72
- - - ">="
73
- - !ruby/object:Gem::Version
74
- hash: 3
75
- segments:
76
- - 0
77
- version: "0"
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
78
84
  requirements: []
79
-
80
85
  rubyforge_project:
81
- rubygems_version: 1.3.7
86
+ rubygems_version: 1.8.10
82
87
  signing_key:
83
88
  specification_version: 3
84
89
  summary: Simple git push-based application deployment
85
90
  test_files: []
86
-
@@ -1,57 +0,0 @@
1
- #!/usr/bin/env ruby
2
- if ENV['GIT_DIR'] == '.'
3
- # this means the script has been called as a hook, not manually.
4
- # get the proper GIT_DIR so we can descend into the working copy dir;
5
- # if we don't then `git reset --hard` doesn't affect the working tree.
6
- Dir.chdir('..')
7
- ENV['GIT_DIR'] = '.git'
8
- end
9
-
10
- cmd = %(bash -c "[ -f /etc/profile ] && source /etc/profile; echo $PATH")
11
- envpath = IO.popen(cmd, 'r') { |io| io.read.chomp }
12
- ENV['PATH'] = envpath
13
-
14
- # find out the current branch
15
- head = `git symbolic-ref HEAD`.chomp
16
- # abort if we're on a detached head
17
- exit unless $?.success?
18
-
19
- oldrev = newrev = nil
20
- null_ref = '0' * 40
21
-
22
- # read the STDIN to detect if this push changed the current branch
23
- while newrev.nil? and gets
24
- # each line of input is in form of "<oldrev> <newrev> <refname>"
25
- revs = $_.split
26
- oldrev, newrev = revs if head == revs.pop
27
- end
28
-
29
- # abort if there's no update, or in case the branch is deleted
30
- exit if newrev.nil? or newrev == null_ref
31
-
32
- # update the working copy
33
- `umask 002 && git reset --hard`
34
-
35
- config = 'config/database.yml'
36
- logfile = 'log/deploy.log'
37
- restart = 'tmp/restart.txt'
38
-
39
- if oldrev == null_ref
40
- # this is the first push; this branch was just created
41
- require 'fileutils'
42
- FileUtils.mkdir_p %w(log tmp)
43
- FileUtils.chmod 0775, %w(log tmp)
44
- FileUtils.touch [logfile, restart]
45
- FileUtils.chmod 0664, [logfile, restart]
46
-
47
- unless File.exists?(config)
48
- # install the database config from the example file
49
- example = ['config/database.example.yml', config + '.example'].find { |f| File.exists? f }
50
- FileUtils.cp example, config if example
51
- end
52
- else
53
- # log timestamp
54
- File.open(logfile, 'a') { |log| log.puts "==== #{Time.now} ====" }
55
- # start the post-reset hook in background
56
- system %(nohup .git/hooks/post-reset #{oldrev} #{newrev} 1>>#{logfile} 2>>#{logfile} &)
57
- end
@@ -1,116 +0,0 @@
1
- #!/usr/bin/env ruby
2
- RAILS_ENV = 'production'
3
- oldrev, newrev = ARGV
4
- $stdout.sync = true
5
-
6
- def parse_configuration(file)
7
- config = {}
8
- current = nil
9
-
10
- File.open(file).each_line do |line|
11
- case line
12
- when /^\[(\w+)(?: "(.+)")\]/
13
- key, subkey = $1, $2
14
- current = (config[key] ||= {})
15
- current = (current[subkey] ||= {}) if subkey
16
- else
17
- key, value = line.strip.split(' = ')
18
- current[key] = value
19
- end
20
- end
21
-
22
- config
23
- end
24
-
25
- class Array
26
- # scans the list of files to see if any of them are under the given path
27
- def any_in_dir?(dir)
28
- if Array === dir
29
- exp = %r{^(?:#{dir.join('|')})/}
30
- any? { |file| file =~ exp }
31
- else
32
- dir += '/'
33
- any? { |file| file.index(dir) == 0 }
34
- end
35
- end
36
- end
37
-
38
- # get a list of files that changed
39
- changes = `git diff #{oldrev} #{newrev} --diff-filter=ACDMR --name-status`.split("\n")
40
-
41
- # make a hash of files that changed and how they changed
42
- changes_hash = changes.inject(Hash.new { |h, k| h[k] = [] }) do |hash, line|
43
- modifier, filename = line.split("\t", 2)
44
- hash[modifier] << filename
45
- hash
46
- end
47
-
48
- # create an array of files added, copied, modified or renamed
49
- modified_files = %w(A C M R).inject([]) { |files, bit| files.concat changes_hash[bit] }
50
- added_files = changes_hash['A'] # added
51
- deleted_files = changes_hash['D'] # deleted
52
- changed_files = modified_files + deleted_files # all
53
- puts "files changed: #{changed_files.size}"
54
-
55
- cached_assets_cleared = false
56
-
57
- # detect modified asset dirs
58
- asset_dirs = %w(public/stylesheets public/javascripts).select do |dir|
59
- # did any on the assets under this dir change?
60
- changed_files.any_in_dir?(dir)
61
- end
62
-
63
- unless asset_dirs.empty?
64
- # clear cached assets (unversioned/ignored files)
65
- system %(git clean -x -f -- #{asset_dirs.join(' ')})
66
- cached_assets_cleared = true
67
- end
68
-
69
- if changed_files.include?('Gemfile') || changed_files.include?('Gemfile.lock')
70
- # update bundled gems if manifest file has changed
71
- system %(umask 002 && bundle install --deployment)
72
- end
73
-
74
- # run migrations when new ones added
75
- if new_migrations = added_files.any_in_dir?('db/migrate')
76
- system %(umask 002 && rake db:migrate RAILS_ENV=#{RAILS_ENV})
77
- end
78
-
79
- if modified_files.include?('.gitmodules')
80
- # initialize new submodules
81
- system %(umask 002 && git submodule init)
82
- # sync submodule remote urls in case of changes
83
- config = parse_configuration('.gitmodules')
84
-
85
- if config['submodule']
86
- config['submodule'].values.each do |submodule|
87
- path = submodule['path']
88
- subconf = "#{path}/.git/config"
89
-
90
- if File.exists? subconf
91
- old_url = `git config -f "#{subconf}" remote.origin.url`.chomp
92
- new_url = submodule['url']
93
- unless old_url == new_url
94
- puts "changing #{path.inspect} URL:\n #{old_url.inspect} → #{new_url.inspect}"
95
- `git config -f "#{subconf}" remote.origin.url "#{new_url}"`
96
- end
97
- else
98
- $stderr.puts "a submodule in #{path.inspect} doesn't exist"
99
- end
100
- end
101
- end
102
- end
103
- # update existing submodules
104
- system %(umask 002 && git submodule update)
105
-
106
- # clean unversioned files from vendor (e.g. old submodules)
107
- system %(git clean -d -f vendor)
108
-
109
- # determine if app restart is needed
110
- if cached_assets_cleared or new_migrations or !File.exists?('config/environment.rb') or
111
- changed_files.any_in_dir?(%w(app config lib public vendor))
112
- require 'fileutils'
113
- # tell Passenger to restart this app
114
- FileUtils.touch 'tmp/restart.txt'
115
- puts "restarting Passenger app"
116
- end