git-deploy 0.4.1 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -0,0 +1,81 @@
1
+ require 'uri'
2
+ require 'cgi'
3
+ require 'forwardable'
4
+
5
+ class GitDeploy
6
+ module Configuration
7
+ private
8
+
9
+ extend Forwardable
10
+ def_delegator :remote_url, :host
11
+ def_delegator :remote_url, :port, :remote_port
12
+ def_delegator :remote_url, :path, :deploy_to
13
+
14
+ def remote_user
15
+ @user ||= begin
16
+ user = remote_url.user
17
+ user ? CGI.unescape(user) : `whoami`.chomp
18
+ end
19
+ end
20
+
21
+ def branch
22
+ 'master'
23
+ end
24
+
25
+ def git_config
26
+ @git_config ||= Hash.new do |cache, cmd|
27
+ git = ENV['GIT'] || 'git'
28
+ out = `#{git} #{cmd}`
29
+ if $?.success? then cache[cmd] = out.chomp
30
+ else cache[cmd] = nil
31
+ end
32
+ cache[cmd]
33
+ end
34
+ end
35
+
36
+ def remote_urls(remote)
37
+ git_config["config --get-all remote.#{remote}.url"].to_s.split("\n")
38
+ end
39
+
40
+ def remote_url(remote = options[:remote])
41
+ @remote_url ||= {}
42
+ @remote_url[remote] ||= begin
43
+ url = remote_urls(remote).first
44
+ if url.nil?
45
+ abort "Error: Remote url not found for remote #{remote.inspect}"
46
+ elsif url =~ /(^|@)github\.com\b/
47
+ abort "Error: Remote url for #{remote.inspect} points to GitHub. Can't deploy there!"
48
+ else
49
+ url = 'ssh://' + url.sub(%r{:/?}, '/') unless url =~ %r{^[\w-]+://}
50
+ begin
51
+ url = URI.parse url
52
+ rescue
53
+ abort "Error parsing remote url #{url}"
54
+ end
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, :port => remote_port)
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,39 @@
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
+ if use_bundler
13
+ bundler_args = ['--deployment']
14
+ BUNDLE_WITHOUT = ENV['BUNDLE_WITHOUT'] || 'development:test'
15
+ bundler_args << '--without' << BUNDLE_WITHOUT unless BUNDLE_WITHOUT.empty?
16
+
17
+ # update gem bundle
18
+ run "bundle install #{bundler_args.join(' ')}"
19
+ end
20
+
21
+ if File.file? 'Rakefile'
22
+ tasks = []
23
+
24
+ num_migrations = `git diff #{oldrev} #{newrev} --diff-filter=A --name-only -z db/migrate`.split("\0").size
25
+ # run migrations if new ones have been added
26
+ tasks << "db:migrate" if num_migrations > 0
27
+
28
+ # precompile assets
29
+ changed_assets = `git diff #{oldrev} #{newrev} --name-only -z app/assets`.split("\0")
30
+ tasks << "assets:precompile" if changed_assets.size > 0
31
+
32
+ run "#{rake_cmd} #{tasks.join(' ')} RAILS_ENV=#{RAILS_ENV}" if tasks.any?
33
+ end
34
+
35
+ # clear cached assets (unversioned/ignored files)
36
+ run "git clean -x -f -- public/stylesheets public/javascripts"
37
+
38
+ # clean unversioned files from vendor/plugins (e.g. old submodules)
39
+ run "git clean -d -f -- vendor/plugins"
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ touch tmp/restart.txt
3
+ echo "restarting Passenger app"
data/lib/git_deploy.rb CHANGED
@@ -1,121 +1,97 @@
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
+ unless 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
+ cmd
32
+ end
70
33
  end
71
34
 
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
35
+ run [] do |cmd|
36
+ cmd << "chmod g+ws #{deploy_to}" if options.shared?
37
+ cmd << "cd #{deploy_to}"
38
+ cmd << "git init #{options.shared? ? '--shared' : ''}"
39
+ cmd << "sed -i'' -e 's/master/#{branch}/' .git/HEAD" unless branch == 'master'
40
+ cmd << "git config --bool receive.denyNonFastForwards false" if options.shared?
41
+ cmd << "git config receive.denyCurrentBranch ignore"
88
42
  end
89
43
 
90
- task :install_hooks do
91
- dir = File.dirname(__FILE__) + '/hooks'
92
- remote_dir = "#{deploy_to}/.git/hooks"
44
+ invoke :hooks
45
+ end
93
46
 
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
47
+ desc "hooks", "Installs git hooks to the remote repository"
48
+ def hooks
49
+ hooks_dir = File.join(LOCAL_DIR, 'hooks')
50
+ remote_dir = "#{deploy_to}/.git/hooks"
98
51
 
99
- desc "Restarts your Passenger application."
100
- task :restart, :roles => :app do
101
- run "touch #{deploy_to}/tmp/restart.txt"
102
- end
52
+ scp_upload "#{hooks_dir}/post-receive.sh" => "#{remote_dir}/post-receive"
53
+ run "chmod +x #{remote_dir}/post-receive"
54
+ end
55
+
56
+ desc "restart", "Restarts the application on the server"
57
+ def restart
58
+ run "cd #{deploy_to} && deploy/restart | tee -a log/deploy.log"
59
+ end
103
60
 
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.
61
+ desc "rollback", "Rolls back the checkout to before the last push"
62
+ def rollback
63
+ run "cd #{deploy_to} && git reset --hard ORIG_HEAD"
64
+ invoke :restart
65
+ end
107
66
 
108
- Note that unversioned files on your server are likely to be \
109
- overwritten by the next push. Always persist your changes by committing.
67
+ desc "log", "Shows the last part of the deploy log on the server"
68
+ method_option :tail, :aliases => '-t', :type => :boolean, :default => false
69
+ method_option :lines, :aliases => '-l', :type => :numeric, :default => 20
70
+ def log(n = nil)
71
+ tail_args = options.tail? ? '-f' : "-n#{n || options.lines}"
72
+ run "tail #{tail_args} #{deploy_to}/log/deploy.log"
73
+ end
110
74
 
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?
75
+ desc "upload <files>", "Copy local files to the remote app"
76
+ def upload(*files)
77
+ files = files.map { |f| Dir[f.strip] }.flatten
78
+ abort "Error: Specify at least one file to upload" if files.empty?
117
79
 
118
- files.each { |file| top.upload(file, File.join(deploy_to, file)) }
119
- end
80
+ scp_upload files.inject({}) { |all, file|
81
+ all[file] = File.join(deploy_to, file)
82
+ all
83
+ }
120
84
  end
121
- end
85
+ end
86
+
87
+ __END__
88
+ Multiple hosts:
89
+ # deploy:
90
+ invoke :code
91
+ command = ["cd #{deploy_to}"]
92
+ command << ".git/hooks/post-reset `cat .git/ORIG_HEAD` HEAD 2>&1 | tee -a log/deploy.log"
93
+
94
+ # code:
95
+ command = ["cd #{deploy_to}"]
96
+ command << source.scm('fetch', remote, "+refs/heads/#{branch}:refs/remotes/origin/#{branch}")
97
+ command << source.scm('reset', '--hard', "origin/#{branch}")
@@ -0,0 +1,52 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ if [ "$GIT_DIR" = "." ]; then
5
+ # The script has been called as a hook; chdir to the working copy
6
+ cd ..
7
+ unset GIT_DIR
8
+ fi
9
+
10
+ # try to obtain the usual system PATH
11
+ if [ -f /etc/profile ]; then
12
+ PATH=$(source /etc/profile; echo $PATH)
13
+ export PATH
14
+ fi
15
+
16
+ # get the current branch
17
+ head="$(git symbolic-ref HEAD)"
18
+
19
+ # read the STDIN to detect if this push changed the current branch
20
+ while read oldrev newrev refname
21
+ do
22
+ [ "$refname" = "$head" ] && break
23
+ done
24
+
25
+ # abort if there's no update, or in case the branch is deleted
26
+ if [ -z "${newrev//0}" ]; then
27
+ exit
28
+ fi
29
+
30
+ # check out the latest code into the working copy
31
+ umask 002
32
+ git reset --hard
33
+
34
+ logfile=log/deploy.log
35
+ restart=tmp/restart.txt
36
+
37
+ if [ -z "${oldrev//0}" ]; then
38
+ # this is the first push; this branch was just created
39
+ mkdir -p log tmp
40
+ chmod 0775 log tmp
41
+ touch $logfile $restart
42
+ chmod 0664 $logfile $restart
43
+
44
+ # init submodules
45
+ git submodule update --init | tee -a $logfile
46
+ else
47
+ # log timestamp
48
+ echo ==== $(date) ==== >> $logfile
49
+
50
+ # execute the deploy hook in background
51
+ [ -x deploy/after_push ] && nohup deploy/after_push $oldrev $newrev 1>>$logfile 2>>$logfile &
52
+ fi
@@ -0,0 +1,48 @@
1
+ require 'rspec'
2
+ require 'git_deploy/configuration'
3
+
4
+ describe GitDeploy::Configuration do
5
+
6
+ subject {
7
+ mod = described_class
8
+ obj = Object.new
9
+ opt = options
10
+ (class << obj; self; end).class_eval do
11
+ include mod
12
+ mod.private_instance_methods.each {|m| public m }
13
+ define_method(:options) { opt }
14
+ end
15
+ obj
16
+ }
17
+
18
+ let(:options) { {:remote => 'production'} }
19
+
20
+ def stub_git_config(cmd, value)
21
+ subject.git_config[cmd] = value
22
+ end
23
+
24
+ def stub_remote_url(url, remote = options[:remote])
25
+ stub_git_config("config --get-all remote.#{remote}.url", url)
26
+ end
27
+
28
+ describe "extracting user/host from remote url" do
29
+ context "ssh url" do
30
+ before { stub_remote_url 'ssh://jon%20doe@example.com:88/path/to/app' }
31
+
32
+ its(:host) { should eq('example.com') }
33
+ its(:remote_port) { should eq(88) }
34
+ its(:remote_user) { should eq('jon doe') }
35
+ its(:deploy_to) { should eq('/path/to/app') }
36
+ end
37
+
38
+ context "scp-style" do
39
+ before { stub_remote_url 'git@example.com:/path/to/app' }
40
+
41
+ its(:host) { should eq('example.com') }
42
+ its(:remote_port) { should be_nil }
43
+ its(:remote_user) { should eq('git') }
44
+ its(:deploy_to) { should eq('/path/to/app') }
45
+ end
46
+ end
47
+
48
+ end
metadata CHANGED
@@ -1,86 +1,92 @@
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.4
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: 2012-04-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: thor
16
+ requirement: &70233531184120 !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: *70233531184120
25
+ - !ruby/object:Gem::Dependency
26
+ name: net-ssh
27
+ requirement: &70233531200000 !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: *70233531200000
36
+ - !ruby/object:Gem::Dependency
37
+ name: net-scp
38
+ requirement: &70233531199500 !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: *70233531199500
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
64
+ - spec/configuration_spec.rb
49
65
  - README.markdown
50
66
  - LICENSE
51
- has_rdoc: false
52
- homepage: http://github.com/mislav/git-deploy
67
+ homepage: https://github.com/mislav/git-deploy
53
68
  licenses: []
54
-
55
69
  post_install_message:
56
70
  rdoc_options: []
57
-
58
- require_paths:
71
+ require_paths:
59
72
  - lib
60
- required_ruby_version: !ruby/object:Gem::Requirement
73
+ required_ruby_version: !ruby/object:Gem::Requirement
61
74
  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
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
80
  none: false
71
- requirements:
72
- - - ">="
73
- - !ruby/object:Gem::Version
74
- hash: 3
75
- segments:
76
- - 0
77
- version: "0"
81
+ requirements:
82
+ - - ! '>='
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
78
85
  requirements: []
79
-
80
86
  rubyforge_project:
81
- rubygems_version: 1.3.7
87
+ rubygems_version: 1.8.12
82
88
  signing_key:
83
89
  specification_version: 3
84
90
  summary: Simple git push-based application deployment
85
91
  test_files: []
86
-
92
+ has_rdoc:
@@ -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