git-deploy 0.5.0 → 0.6.0

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,17 +1,20 @@
1
1
  Easy git deployment
2
2
  ===================
3
3
 
4
- Straightforward, [Heroku][]-style, push-based deployment. Your deploys will look like this:
4
+ Straightforward, [Heroku][]-style, push-based deployment. Your deploys can become as simple as this:
5
5
 
6
6
  $ git push production master
7
7
 
8
8
  To get started, install the "git-deploy" gem.
9
9
 
10
- $ gem install git-deploy
10
+ gem install git-deploy
11
11
 
12
+ Only the person who is setting up deployment for the first time needs to install
13
+ the gem. You don't have to add it to your project's Gemfile.
12
14
 
13
- What application frameworks/languages are supported?
14
- ----------------------------------------------------
15
+
16
+ Which app languages/frameworks are supported?
17
+ ---------------------------------------------
15
18
 
16
19
  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
20
 
@@ -20,50 +23,63 @@ Your deployment is customized with per-project callback scripts which can be wri
20
23
  The assumption is that you're deploying to a single host to which you connect over SSH using public/private key authentication.
21
24
 
22
25
 
23
- Setup steps
24
- -----------
26
+ Initial setup
27
+ -------------
25
28
 
26
29
  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).
27
-
28
- $ git remote add production user@example.com:/path/to/myapp
29
-
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.
30
+
31
+ ```sh
32
+ git remote add production "user@example.com:/apps/mynewapp"
33
+ ```
34
+
35
+ `/apps/mynewapp` is the directory where you want your code to reside on the
36
+ remote server. If the directory doesn't exist, the next step creates it.
31
37
 
32
38
  2. Run the setup task:
33
-
34
- $ git deploy setup -r production
35
-
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.
39
+
40
+ ```sh
41
+ git deploy setup -r "production"
42
+ ```
43
+
44
+ This will initialize the remote git repository in the deploy directory
45
+ (`/apps/mynewapp` in the above example) and install the remote git hook.
37
46
 
38
47
  3. Run the init task:
39
-
40
- $ git deploy init
41
-
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.
48
+
49
+ ```sh
50
+ git deploy init
51
+ ```
52
+
53
+ This generates default deploy callback scripts in the `deploy/` directory.
54
+ You should check them in git because they are going to be executed on the
55
+ server during each deploy.
43
56
 
44
57
  4. Push the code.
45
58
 
46
- $ git push production master
59
+ ```sh
60
+ git push production master
61
+ ```
47
62
 
48
63
  3. Login to your server and manually perform necessary one-time administrative operations. This might include:
49
64
  * set up the Apache/nginx virtual host for this application;
50
- * check your "config/database.yml" and create the production database.
65
+ * check your `config/database.yml` and create the production database.
51
66
 
52
67
 
53
- Deployment
54
- ----------
68
+ Everyday deployments
69
+ --------------------
55
70
 
56
- If you've set your app correctly, visiting "http://example.com" in your browser should show it up and running.
71
+ If you've set your app correctly, visiting <http://example.com> in your browser
72
+ should show it up and running.
57
73
 
58
- Now, subsequent deployments are done simply by pushing to the branch that is currently checked out on the remote:
74
+ Now, subsequent deployments are done simply **by pushing to the branch that is
75
+ currently checked out on the remote**:
59
76
 
60
- $ git push production master
77
+ git push production master
61
78
 
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.
79
+ Because the deployments are performed with git, nobody else on the team needs to
80
+ install the "git-deploy" gem.
63
81
 
64
- Deployments are logged to "log/deploy.log" in your application.
65
-
66
- On every deploy, the "deploy/after_push" script performs the following:
82
+ On every deploy, the default `deploy/after_push` script performs the following:
67
83
 
68
84
  1. updates git submodules (if there are any);
69
85
  2. runs `bundle install --deployment` if there is a Gemfile;
@@ -71,16 +87,48 @@ On every deploy, the "deploy/after_push" script performs the following:
71
87
  4. clears cached CSS/JS assets in "public/stylesheets" and "public/javascripts";
72
88
  5. restarts the web application.
73
89
 
74
- You can customize all of this by editing scripts in the "deploy/" directory of your app.
90
+ You can customize all this by editing generated scripts in the `deploy/`
91
+ directory of your app.
92
+
93
+ Deployments are logged to `log/deploy.log` in your application's directory.
94
+
75
95
 
76
96
  How it works
77
97
  ------------
78
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
+ The `git deploy setup` command installed a `post-receive` git hook in the remote
100
+ repository. This is how your code on the server is kept up to date. This script
101
+ checks out the latest version of your project from the current branch and
102
+ runs the following callback scripts:
103
+
104
+ * `deploy/setup` - on first push.
105
+ * `deploy/after_push` - on subsequent pushes. It in turn executes:
106
+ * `deploy/before_restart`
107
+ * `deploy/restart`
108
+ * `deploy/after_restart`
109
+ * `deploy/rollback` - executed for `git deploy rollback`.
110
+
111
+ All of the callbacks are optional. These scripts are ordinary Unix executables.
112
+ The ones which get generated for you by `git deploy init` are written in shell
113
+ script and Ruby.
114
+
115
+
116
+ Extra commands
117
+ --------------
118
+
119
+ * `git deploy hooks` - Updates git hooks on the remote repository
120
+
121
+ * `git deploy log [N=20]` - Shows last 20 lines of deploy log on the server
122
+
123
+ * `git deploy rerun` - Re-runs the `deploy/after_push` callback as if a git push happened
124
+
125
+ * `git deploy restart` - Runs the `deploy/restart` callback
126
+
127
+ * `git deploy rollback` - Undo a deploy by checking out the previous revision,
128
+ runs `deploy/callback` if exists instead of `deploy/after_push`
80
129
 
81
- These scripts are ordinary unix executable files. The ones which are generated for you are written in shell script and Ruby.
130
+ * `git deploy upload <files>` - Copy local files to the remote app
82
131
 
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.
84
132
 
85
133
 
86
- [heroku]: http://heroku.com/
134
+ [heroku]: http://heroku.com/
@@ -1,30 +1,23 @@
1
+ require 'uri'
2
+ require 'cgi'
3
+ require 'forwardable'
4
+
1
5
  class GitDeploy
2
6
  module Configuration
3
7
  private
4
8
 
5
- def host
6
- extract_host_and_user unless defined? @host
7
- @host
8
- end
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
9
13
 
10
14
  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
15
+ @user ||= begin
16
+ user = remote_url.user
17
+ user ? CGI.unescape(user) : `whoami`.chomp
21
18
  end
22
19
  end
23
20
 
24
- def deploy_to
25
- @deploy_to ||= remote_url.split(':').last
26
- end
27
-
28
21
  def branch
29
22
  'master'
30
23
  end
@@ -41,7 +34,9 @@ class GitDeploy
41
34
  end
42
35
 
43
36
  def remote_urls(remote)
44
- git_config["config --get-all remote.#{remote}.url"].to_s.split("\n")
37
+ git_config["remote -v"].to_s.split("\n").
38
+ select {|l| l =~ /^#{remote}\t/ }.
39
+ map {|l| l.split("\t")[1].sub(/\(.+?\)$/, '') }
45
40
  end
46
41
 
47
42
  def remote_url(remote = options[:remote])
@@ -50,8 +45,15 @@ class GitDeploy
50
45
  url = remote_urls(remote).first
51
46
  if url.nil?
52
47
  abort "Error: Remote url not found for remote #{remote.inspect}"
53
- elsif url =~ /\bgithub\.com\b/
48
+ elsif url =~ /(^|@)github\.com\b/
54
49
  abort "Error: Remote url for #{remote.inspect} points to GitHub. Can't deploy there!"
50
+ else
51
+ url = 'ssh://' + url.sub(%r{:/?}, '/') unless url =~ %r{^[\w-]+://}
52
+ begin
53
+ url = URI.parse url
54
+ rescue
55
+ abort "Error parsing remote url #{url}"
56
+ end
55
57
  end
56
58
  url
57
59
  end
@@ -11,10 +11,13 @@ class GitDeploy
11
11
  super unless options.noop?
12
12
  end
13
13
 
14
- def run(cmd = nil)
14
+ def run(cmd = nil, opt = {})
15
15
  cmd = yield(cmd) if block_given?
16
16
  cmd = cmd.join(' && ') if Array === cmd
17
- puts "[#{options[:remote]}] $ " + cmd.gsub(' && ', " && \\\n ")
17
+
18
+ if opt.fetch(:echo, true)
19
+ puts "[#{options[:remote]}] $ " + cmd.gsub(' && ', " && \\\n ")
20
+ end
18
21
 
19
22
  unless options.noop?
20
23
  status, output = ssh_exec cmd do |ch, stream, data|
@@ -90,7 +93,7 @@ class GitDeploy
90
93
 
91
94
  def ssh_connection
92
95
  @ssh ||= begin
93
- ssh = Net::SSH.start(host, remote_user)
96
+ ssh = Net::SSH.start(host, remote_user, :port => remote_port)
94
97
  at_exit { ssh.close }
95
98
  ssh
96
99
  end
@@ -11,7 +11,7 @@ echo files changed: $(git diff $oldrev $newrev --diff-filter=ACDMR --name-only |
11
11
 
12
12
  umask 002
13
13
 
14
- git submodule init && git submodule sync && git submodule update
14
+ git submodule sync && git submodule update --init --recursive
15
15
 
16
16
  run deploy/before_restart
17
17
  run deploy/restart && run deploy/after_restart
@@ -9,13 +9,27 @@ RAILS_ENV = ENV['RAILS_ENV'] || 'production'
9
9
  use_bundler = File.file? 'Gemfile'
10
10
  rake_cmd = use_bundler ? 'bundle exec rake' : 'rake'
11
11
 
12
- # update gem bundle
13
- run "bundle install --deployment" if use_bundler
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
14
20
 
15
21
  if File.file? 'Rakefile'
16
- num_migrations = `git diff #{oldrev} #{newrev} --diff-filter=A --name-only`.split("\n").size
22
+ tasks = []
23
+
24
+ num_migrations = `git diff #{oldrev} #{newrev} --diff-filter=A --name-only -z db/migrate`.split("\0").size
17
25
  # run migrations if new ones have been added
18
- run "#{rake_cmd} db:migrate RAILS_ENV=#{RAILS_ENV}" if num_migrations > 0
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?
19
33
  end
20
34
 
21
35
  # clear cached assets (unversioned/ignored files)
data/lib/git_deploy.rb CHANGED
@@ -20,14 +20,15 @@ class GitDeploy < Thor
20
20
  end
21
21
 
22
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
23
+ method_option :shared, :aliases => '-g', :type => :boolean, :default => false
24
+ method_option :sudo, :aliases => '-s', :type => :boolean, :default => false
25
25
  def setup
26
26
  sudo = options.sudo? ? "#{sudo_cmd} " : ''
27
27
 
28
- if run_test("test -x #{deploy_to}")
28
+ unless run_test("test -x #{deploy_to}")
29
29
  run ["#{sudo}mkdir -p #{deploy_to}"] do |cmd|
30
30
  cmd << "#{sudo}chown $USER #{deploy_to}" if options.sudo?
31
+ cmd
31
32
  end
32
33
  end
33
34
 
@@ -54,18 +55,40 @@ class GitDeploy < Thor
54
55
 
55
56
  desc "restart", "Restarts the application on the server"
56
57
  def restart
57
- run "cd #{deploy_to} && deploy/restart | tee -a log/deploy.log"
58
+ run "cd #{deploy_to} && deploy/restart 2>&1 | tee -a log/deploy.log"
59
+ end
60
+
61
+ desc "rerun", "Runs the `deploy/after_push' callback as if a new revision was pushed via git"
62
+ def rerun
63
+ run <<-BASH, :echo => false
64
+ bash -e -c '
65
+ cd '#{deploy_to}'
66
+ declare -a revs=( $(git rev-parse HEAD@{1} HEAD) )
67
+ deploy/after_push ${revs[@]} 2>&1 | tee -a log/deploy.log
68
+ '
69
+ BASH
58
70
  end
59
71
 
60
72
  desc "rollback", "Rolls back the checkout to before the last push"
61
73
  def rollback
62
- run "cd #{deploy_to} && git reset --hard ORIG_HEAD"
63
- invoke :restart
74
+ run <<-BASH, :echo => false
75
+ bash -e -c '
76
+ cd '#{deploy_to}'
77
+ declare -a revs=( $(git rev-parse HEAD HEAD@{1}) )
78
+ git reset --hard ${revs[1]}
79
+ callback=after_push
80
+ [ -x deploy/rollback ] && callback=rollback
81
+ deploy/$callback ${revs[@]} 2>&1 | tee -a log/deploy.log
82
+ '
83
+ BASH
64
84
  end
65
85
 
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"
86
+ desc "log", "Shows the last part of the deploy log on the server"
87
+ method_option :tail, :aliases => '-t', :type => :boolean, :default => false
88
+ method_option :lines, :aliases => '-l', :type => :numeric, :default => 20
89
+ def log(n = nil)
90
+ tail_args = options.tail? ? '-f' : "-n#{n || options.lines}"
91
+ run "tail #{tail_args} #{deploy_to}/log/deploy.log"
69
92
  end
70
93
 
71
94
  desc "upload <files>", "Copy local files to the remote app"
@@ -79,15 +102,3 @@ class GitDeploy < Thor
79
102
  }
80
103
  end
81
104
  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}")
@@ -1,9 +1,10 @@
1
- #!/usr/bin/env bash
1
+ #!/bin/bash
2
+ set -e
3
+
2
4
  if [ "$GIT_DIR" = "." ]; then
3
5
  # The script has been called as a hook; chdir to the working copy
4
6
  cd ..
5
- GIT_DIR=.git
6
- export GIT_DIR
7
+ unset GIT_DIR
7
8
  fi
8
9
 
9
10
  # try to obtain the usual system PATH
@@ -14,16 +15,17 @@ fi
14
15
 
15
16
  # get the current branch
16
17
  head="$(git symbolic-ref HEAD)"
17
- # abort if we're on a detached head
18
- [ "$?" != "0" ] && exit 1
19
18
 
20
19
  # read the STDIN to detect if this push changed the current branch
21
20
  while read oldrev newrev refname
22
21
  do
23
22
  [ "$refname" = "$head" ] && break
24
23
  done
24
+
25
25
  # abort if there's no update, or in case the branch is deleted
26
- [ -z "${newrev//0}" ] && exit
26
+ if [ -z "${newrev//0}" ]; then
27
+ exit
28
+ fi
27
29
 
28
30
  # check out the latest code into the working copy
29
31
  umask 002
@@ -40,11 +42,14 @@ if [ -z "${oldrev//0}" ]; then
40
42
  chmod 0664 $logfile $restart
41
43
 
42
44
  # init submodules
43
- git submodule update --init | tee -a $logfile
45
+ git submodule update --recursive --init 2>&1 | tee -a $logfile
46
+
47
+ # execute the one-time setup hook
48
+ [ -x deploy/setup ] && deploy/setup $oldrev $newrev 2>&1 | tee -a $logfile
44
49
  else
45
50
  # log timestamp
46
51
  echo ==== $(date) ==== >> $logfile
47
52
 
48
- # execute the deploy hook in background
49
- [ -x deploy/after_push ] && nohup deploy/after_push $oldrev $newrev 1>>$logfile 2>>$logfile &
53
+ # execute the main deploy hook
54
+ [ -x deploy/after_push ] && deploy/after_push $oldrev $newrev 2>&1 | tee -a $logfile
50
55
  fi
@@ -0,0 +1,48 @@
1
+ require 'rspec/autorun'
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("remote -v", "#{remote}\t#{url} (fetch)")
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,49 +1,64 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: git-deploy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
5
4
  prerelease:
5
+ version: 0.6.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Mislav Marohnić
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-10-04 00:00:00.000000000 Z
12
+ date: 2013-07-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
+ prerelease: false
16
+ type: :runtime
17
+ version_requirements: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - '='
21
+ - !ruby/object:Gem::Version
22
+ version: 0.14.6
15
23
  name: thor
16
- requirement: &70292395458180 !ruby/object:Gem::Requirement
24
+ requirement: !ruby/object:Gem::Requirement
17
25
  none: false
18
26
  requirements:
19
- - - ! '>='
27
+ - - '='
20
28
  - !ruby/object:Gem::Version
21
- version: '0'
22
- type: :runtime
23
- prerelease: false
24
- version_requirements: *70292395458180
29
+ version: 0.14.6
25
30
  - !ruby/object:Gem::Dependency
31
+ prerelease: false
32
+ type: :runtime
33
+ version_requirements: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: 2.6.6
26
39
  name: net-ssh
27
- requirement: &70292395455260 !ruby/object:Gem::Requirement
40
+ requirement: !ruby/object:Gem::Requirement
28
41
  none: false
29
42
  requirements:
30
- - - ! '>='
43
+ - - ~>
31
44
  - !ruby/object:Gem::Version
32
- version: '0'
33
- type: :runtime
34
- prerelease: false
35
- version_requirements: *70292395455260
45
+ version: 2.6.6
36
46
  - !ruby/object:Gem::Dependency
47
+ prerelease: false
48
+ type: :runtime
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 1.1.0
37
55
  name: net-scp
38
- requirement: &70292395453880 !ruby/object:Gem::Requirement
56
+ requirement: !ruby/object:Gem::Requirement
39
57
  none: false
40
58
  requirements:
41
- - - ! '>='
59
+ - - ~>
42
60
  - !ruby/object:Gem::Version
43
- version: '0'
44
- type: :runtime
45
- prerelease: false
46
- version_requirements: *70292395453880
61
+ version: 1.1.0
47
62
  description: A tool to install useful git hooks on your remote repository to enable
48
63
  push-based, Heroku-like deployment on your host.
49
64
  email: mislav.marohnic@gmail.com
@@ -61,10 +76,12 @@ files:
61
76
  - lib/git_deploy/templates/restart.sh
62
77
  - lib/git_deploy.rb
63
78
  - lib/hooks/post-receive.sh
79
+ - spec/configuration_spec.rb
64
80
  - README.markdown
65
81
  - LICENSE
66
82
  homepage: https://github.com/mislav/git-deploy
67
- licenses: []
83
+ licenses:
84
+ - MIT
68
85
  post_install_message:
69
86
  rdoc_options: []
70
87
  require_paths:
@@ -83,7 +100,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
100
  version: '0'
84
101
  requirements: []
85
102
  rubyforge_project:
86
- rubygems_version: 1.8.10
103
+ rubygems_version: 1.8.23
87
104
  signing_key:
88
105
  specification_version: 3
89
106
  summary: Simple git push-based application deployment