recap 0.2.0 → 1.0.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.
Files changed (68) hide show
  1. data/.gitignore +2 -1
  2. data/README.md +55 -10
  3. data/Rakefile +19 -5
  4. data/bin/recap +2 -2
  5. data/features/managing-processes.feature +1 -1
  6. data/features/setting-environment-variables.feature +26 -1
  7. data/features/steps/capistrano_steps.rb +10 -6
  8. data/features/support/project.rb +24 -5
  9. data/features/templates/project/Capfile.erb +1 -1
  10. data/lib/recap/recipes/rails.rb +6 -0
  11. data/lib/recap/recipes/ruby.rb +11 -0
  12. data/lib/recap/recipes/static.rb +3 -0
  13. data/lib/recap/recipes.rb +18 -0
  14. data/lib/recap/support/capistrano_extensions.rb +85 -0
  15. data/lib/recap/support/cli.rb +57 -0
  16. data/lib/recap/{compatibility.rb → support/compatibility.rb} +2 -2
  17. data/lib/recap/support/environment.rb +61 -0
  18. data/lib/recap/support/namespace.rb +47 -0
  19. data/lib/recap/support/shell_command.rb +35 -0
  20. data/lib/recap/support/templates/Capfile.erb +6 -0
  21. data/lib/recap/tasks/bootstrap.rb +77 -0
  22. data/lib/recap/{bundler.rb → tasks/bundler.rb} +15 -6
  23. data/lib/recap/{deploy.rb → tasks/deploy.rb} +30 -17
  24. data/lib/recap/tasks/env.rb +111 -0
  25. data/lib/recap/{foreman.rb → tasks/foreman.rb} +20 -12
  26. data/lib/recap/{preflight.rb → tasks/preflight.rb} +13 -11
  27. data/lib/recap/tasks/rails.rb +42 -0
  28. data/lib/recap/tasks.rb +16 -0
  29. data/lib/recap/version.rb +1 -1
  30. data/lib/recap.rb +119 -10
  31. data/recap.gemspec +3 -2
  32. data/spec/models/capistrano_extensions_spec.rb +41 -0
  33. data/spec/models/cli_spec.rb +25 -0
  34. data/spec/models/environment_spec.rb +14 -14
  35. data/spec/models/shell_command_spec.rb +55 -0
  36. data/spec/spec_helper.rb +1 -1
  37. data/spec/tasks/bootstrap_spec.rb +9 -13
  38. data/spec/tasks/bundler_spec.rb +39 -7
  39. data/spec/tasks/deploy_spec.rb +42 -26
  40. data/spec/tasks/env_spec.rb +81 -5
  41. data/spec/tasks/foreman_spec.rb +10 -5
  42. data/spec/tasks/rails_spec.rb +80 -0
  43. metadata +65 -57
  44. data/doc/index.html +0 -235
  45. data/doc/lib/recap/bootstrap.html +0 -42
  46. data/doc/lib/recap/bundler.html +0 -168
  47. data/doc/lib/recap/capistrano_extensions.html +0 -208
  48. data/doc/lib/recap/cli.html +0 -42
  49. data/doc/lib/recap/compatibility.html +0 -73
  50. data/doc/lib/recap/deploy.html +0 -328
  51. data/doc/lib/recap/env.html +0 -108
  52. data/doc/lib/recap/foreman.html +0 -42
  53. data/doc/lib/recap/namespace.html +0 -42
  54. data/doc/lib/recap/preflight.html +0 -163
  55. data/doc/lib/recap/rails.html +0 -42
  56. data/doc/lib/recap/version.html +0 -42
  57. data/doc/lib/recap.html +0 -42
  58. data/index.rb +0 -62
  59. data/lib/recap/bootstrap.rb +0 -47
  60. data/lib/recap/capistrano_extensions.rb +0 -74
  61. data/lib/recap/cli.rb +0 -32
  62. data/lib/recap/deploy/templates/Capfile.erb +0 -6
  63. data/lib/recap/env.rb +0 -58
  64. data/lib/recap/environment.rb +0 -54
  65. data/lib/recap/namespace.rb +0 -37
  66. data/lib/recap/rails.rb +0 -24
  67. data/lib/recap/ruby.rb +0 -3
  68. data/lib/recap/static.rb +0 -1
data/lib/recap.rb CHANGED
@@ -1,12 +1,121 @@
1
+ # This is the documentation for [recap](https://github.com/freerange/recap), a simple, opinionated
2
+ # set of capistrano deployment recipes.
3
+ #
4
+ # Inspired in part by
5
+ # [this blog post](https://github.com/blog/470-deployment-script-spring-cleaning), these recipes use
6
+ # git's strengths to deploy applications in a faster, simpler manner than the standard capistrano
7
+ # deployment.
8
+ #
9
+ # ### Aims and features
10
+ #
11
+ # Releases are managed using git. All code is deployed to a single directory, and git tags are
12
+ # used to manage different released versions. No `releases`, `current` or `shared` directories are
13
+ # created, avoiding unnecessary sym-linking. For more information on how releases work, see
14
+ # [recap/tasks/deploy.rb](recap/tasks/deploy.html).
15
+ #
16
+ # Deployments do the minimum work possible, using git to determine whether tasks need to run. e.g.
17
+ # the `bundle:install` task only runs if the app contains a `Gemfile.lock` file and it has changed
18
+ # since the last deployment. You can see how this works in
19
+ # [recap/tasks/bundler.rb](recap/tasks/bundler.html).
20
+ #
21
+ # Applications have their own user and group, owning all of that application's associated
22
+ # files and processes. This gives them a dedicated environment, allowing environment variables to
23
+ # be used for application specific configuration. Tasks such as `env`, `env:set` and `env:edit` make
24
+ # setting and changing these variables easy. [recap/tasks/env.rb](recap/tasks/env.html) has more
25
+ # information about using these environment variables.
26
+ #
27
+ # Personal accounts are used to deploy to the server, distinct from the application user. The right
28
+ # to deploy an application is granted simply by adding a user to the application group. Most tasks
29
+ # are run as the application user using `sudo su...`. To avoid having to enter a password when
30
+ # running them, these lines can be added to `/etc/sudoers.d/application`
31
+ # (change `application` to the name of your app).
32
+ #
33
+ # <pre>%application ALL=NOPASSWD: /sbin/start application*
34
+ # %application ALL=NOPASSWD: /sbin/stop application*
35
+ # %application ALL=NOPASSWD: /sbin/restart application*
36
+ # %application ALL=NOPASSWD: /bin/su - application*
37
+ # %application ALL=NOPASSWD: /bin/su application*</pre>
38
+ #
39
+ # ### Limitations and Constraints
40
+ #
41
+ # Recap has been developed and tested using Ubuntu 11.04. It may work well with
42
+ # other flavours of unix, but proceed with caution.
43
+ #
44
+ # Recap also uses a different file layout than other capistrano-based deployments, so other
45
+ # recipes may not work well with it. You can improve compatibility with other recipes using
46
+ # [recap/support/compatibility.rb](recap/support/compatibility.html).
47
+ #
48
+ # ### Getting started
49
+ #
50
+ # To use recap you'll need a project stored in `git`. You'll also need a server with `git` installed
51
+ # and if deploying a rails or ruby app, `bundler` and `ruby` too. Finally you need an account on the
52
+ # server which you can SSH into and which is a sudoer.
53
+ #
54
+ # #### Preparing your project
55
+ #
56
+ # To get a project ready to deploy with recap, you'll need to install the gem, most likely by adding
57
+ # an entry like the following to the `Gemfile`, then running `bundle install`.
58
+ #
59
+ # <pre>gem 'recap', '~>1.0.0'</pre>
60
+ #
61
+ # Once the gem is installed, generate a `Capfile` by running `recap setup` within your project
62
+ # folder. You can see the supported options with `recap help setup`. The generated `Capfile`
63
+ # will look something like this:
64
+ #
65
+ # <pre>require 'recap/recipes/rails'
66
+ #
67
+ # set :application, 'example-app'
68
+ # set :repository, 'git@example.com:example/example-app.git'
69
+ #
70
+ # server 'server.example.com', :app</pre>
71
+ #
72
+ # Edit the `Capfile` to point at your deployment server and your project should be ready. `cap -T`
73
+ # will show all the tasks now available.
74
+ #
75
+ # #### Preparing the server
76
+ #
77
+ # The next step is setting up the server. Running `cap bootstrap` will check your personal account
78
+ # on the server is configured correctly, and add an account for your application.
79
+ #
80
+ # This application account is dedicated to your app, so you can edit its `.profile` as much as you
81
+ # need (to add a particular version of `ruby` to the path, for example).
82
+ # [recap/tasks/env.rb](recap/tasks/env.html) information on how to use the `env:set` and `env:edit`
83
+ # tasks to set configuration variables.
84
+ #
85
+ # #### Preparing the app
86
+ #
87
+ # Running `cap deploy:setup` clones your code and sets up everything ready for the first deployment.
88
+ # Once this has been run, you might want to set up a virtual host entry for nginx or Apache to
89
+ # point at your app.
90
+ #
91
+ # #### Deploying
92
+ #
93
+ # Finally running `cap deploy` will deploy your app for the first time. Each time you make a change
94
+ # you want deployed, commit and push your changes to your `git` repository, and run `cap deploy` to
95
+ # push those changes to the server.
96
+ #
97
+ # ### Further information
98
+ #
99
+ # Recap has recipes to deploy static, ruby-based and rails apps which you can find out about in
100
+ # [recap/recipes](recap/recipes.html).
101
+ #
102
+ # For more information about all the capistrano tasks recap provides, see
103
+ # [recap/tasks](recap/tasks.html).
104
+ #
105
+ # ### Versioning and License ###
106
+ #
107
+ # recap uses [semantic versioning](http://semver.org/).
108
+ # The code is available [on github](https://github.com/freerange/recap) and released under the
109
+ # [MIT License](https://github.com/freerange/recap/blob/master/LICENSE)
110
+
1
111
  module Recap
2
- autoload :Namespace, 'recap/namespace'
112
+ module Support
113
+ autoload :Compatibility, 'recap/support/compatibility'
114
+ autoload :Namespace, 'recap/support/namespace'
115
+ autoload :Environment, 'recap/support/environment'
116
+ autoload :ShellCommand, 'recap/support/shell_command'
117
+ autoload :CLI, 'recap/support/cli'
118
+ end
3
119
 
4
- autoload :Bootstrap, 'recap/bootstrap'
5
- autoload :Bundler, 'recap/bundler'
6
- autoload :Compatibility, 'recap/compatibility'
7
- autoload :Deploy, 'recap/deploy'
8
- autoload :Env, 'recap/env'
9
- autoload :Environment, 'recap/environment'
10
- autoload :Foreman, 'recap/foreman'
11
- autoload :Rails, 'recap/rails'
12
- end
120
+ autoload :Version, 'recap/version'
121
+ end
data/recap.gemspec CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
7
7
  s.version = Recap::VERSION
8
8
  s.authors = ["Tom Ward"]
9
9
  s.email = ["tom@popdog.net"]
10
- s.homepage = "http://code.gofreerange.com/recap"
10
+ s.homepage = "http://gofreerange.com/recap"
11
11
  s.summary = %q{GIT based deployment recipes for Capistrano}
12
12
  s.description = %q{GIT based deployment recipes for Capistrano}
13
13
 
@@ -18,8 +18,9 @@ Gem::Specification.new do |s|
18
18
 
19
19
  s.add_dependency('capistrano', '~>2.9.0')
20
20
  s.add_dependency('thor')
21
+ s.add_dependency('open4')
21
22
  s.add_development_dependency('rake', '~>0.9.2')
22
- s.add_development_dependency('rocco', '~>0.8.1')
23
+ s.add_development_dependency('fl-rocco', '~>1.0.0')
23
24
  s.add_development_dependency('rspec', '~>2.7.0')
24
25
  s.add_development_dependency('mocha', '~>0.10.0')
25
26
  s.add_development_dependency('vagrant', '~>0.9.7')
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe Recap::Support::CapistranoExtensions do
4
+ let :config do
5
+ Capistrano::Configuration.new
6
+ end
7
+
8
+ describe "#edit_file" do
9
+ before do
10
+ Tempfile.any_instance.stubs(:path).returns('path/to/tempfile')
11
+ config.stubs(:as_app)
12
+ Recap::Support::ShellCommand.stubs(:execute_interactive)
13
+ config.stubs(:get)
14
+ config.stubs(:editor).returns("some-editor")
15
+ end
16
+
17
+ it 'downloads the file to a temporary file for editing' do
18
+ config.expects(:get).with('remote/path/to/file', 'path/to/tempfile')
19
+ File.stubs(:read).with('path/to/tempfile')
20
+ config.edit_file('remote/path/to/file')
21
+ end
22
+
23
+ it 'opens the editor using `execute_interactive` so that Vi works' do
24
+ config.stubs(:editor).returns('vi')
25
+ File.stubs(:read).with('path/to/tempfile')
26
+ Recap::Support::ShellCommand.expects(:execute_interactive).with('vi path/to/tempfile')
27
+ config.edit_file('remote/path/to/file')
28
+ end
29
+
30
+ it 'returns the locally edited file contents' do
31
+ File.expects(:read).with('path/to/tempfile').returns('edited contents')
32
+ config.edit_file('remote/path/to/file').should eql('edited contents')
33
+ end
34
+
35
+ it 'fails if no EDITOR is set' do
36
+ config.stubs(:editor).returns(nil)
37
+ config.expects(:abort).with(regexp_matches(/To edit a remote file, either the EDITOR or DEPLOY_EDITOR environment variables must be set/))
38
+ config.edit_file('remote/path/to/file')
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe Recap::Support::CLI do
4
+ subject { Recap::Support::CLI.new }
5
+
6
+ describe "#setup" do
7
+ it 'determines the git repository URL' do
8
+ Recap::Support::ShellCommand.stubs(:execute).with('git remote -v').returns(%{
9
+ origin git@github.com:freerange/recap.git (fetch)
10
+ origin git@github.com:freerange/recap.git (push)
11
+ })
12
+ subject.stubs(:template)
13
+ subject.setup
14
+ subject.repository.should eql('git@github.com:freerange/recap.git')
15
+ end
16
+
17
+ it 'handles exception when no git repository present and uses <unkown>' do
18
+ Recap::Support::ShellCommand.stubs(:execute).with('git remote -v').raises
19
+ subject.stubs(:template)
20
+ subject.expects(:warn)
21
+ lambda { subject.setup }.should_not raise_error
22
+ subject.repository.should eql('<unknown>')
23
+ end
24
+ end
25
+ end
@@ -1,29 +1,29 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Recap::Environment do
3
+ describe Recap::Support::Environment do
4
4
  describe '#empty?' do
5
5
  it 'returns true if no variables set' do
6
- Recap::Environment.new.empty?.should be_true
6
+ Recap::Support::Environment.new.empty?.should be_true
7
7
  end
8
8
 
9
9
  it 'returns false if no variables set' do
10
- Recap::Environment.new('FIRST' => 'One').empty?.should be_false
10
+ Recap::Support::Environment.new('FIRST' => 'One').empty?.should be_false
11
11
  end
12
12
  end
13
13
 
14
14
  describe '#include?(key)' do
15
15
  it 'returns true if variables set' do
16
- Recap::Environment.new('FIRST' => 'One').include?('FIRST').should be_true
16
+ Recap::Support::Environment.new('FIRST' => 'One').include?('FIRST').should be_true
17
17
  end
18
18
 
19
19
  it 'returns false if variable has not been set' do
20
- Recap::Environment.new('DIFFERENT' => 'One').include?('FIRST').should be_false
20
+ Recap::Support::Environment.new('DIFFERENT' => 'One').include?('FIRST').should be_false
21
21
  end
22
22
  end
23
23
 
24
24
  describe '#get(name)' do
25
25
  subject do
26
- Recap::Environment.new('FIRST' => 'One')
26
+ Recap::Support::Environment.new('FIRST' => 'One')
27
27
  end
28
28
 
29
29
  it 'returns value if variable set' do
@@ -37,7 +37,7 @@ describe Recap::Environment do
37
37
 
38
38
  describe '#set(name, value)' do
39
39
  subject do
40
- Recap::Environment.new('FIRST' => 'One')
40
+ Recap::Support::Environment.new('FIRST' => 'One')
41
41
  end
42
42
 
43
43
  it 'sets variable value' do
@@ -60,7 +60,7 @@ describe Recap::Environment do
60
60
 
61
61
  describe '#each' do
62
62
  subject do
63
- Recap::Environment.new('FIRST' => 'One', 'SECOND' => 'Two', 'THIRD' => 'Three', 'FOURTH' => 'Four')
63
+ Recap::Support::Environment.new('FIRST' => 'One', 'SECOND' => 'Two', 'THIRD' => 'Three', 'FOURTH' => 'Four')
64
64
  end
65
65
 
66
66
  it 'yields each variable and value in turn (ordered alphabetically)' do
@@ -74,7 +74,7 @@ describe Recap::Environment do
74
74
 
75
75
  describe '#merge(variables)' do
76
76
  subject do
77
- Recap::Environment.new('FIRST' => 'One')
77
+ Recap::Support::Environment.new('FIRST' => 'One')
78
78
  end
79
79
 
80
80
  it 'sets each variable value' do
@@ -96,7 +96,7 @@ describe Recap::Environment do
96
96
 
97
97
  describe '#to_s' do
98
98
  subject do
99
- Recap::Environment.new('FIRST' => 'One', 'SECOND' => 'Two', 'THIRD' => nil, 'FOURTH' => 'Four').to_s
99
+ Recap::Support::Environment.new('FIRST' => 'One', 'SECOND' => 'Two', 'THIRD' => nil, 'FOURTH' => 'Four').to_s
100
100
  end
101
101
 
102
102
  it 'declares each variable on its own line' do
@@ -117,26 +117,26 @@ describe Recap::Environment do
117
117
 
118
118
  describe '.from_string(declarations)' do
119
119
  it 'builds instance using string representation' do
120
- instance = Recap::Environment.from_string("FIRST=One\nSECOND=Two\n")
120
+ instance = Recap::Support::Environment.from_string("FIRST=One\nSECOND=Two\n")
121
121
  instance.get('FIRST').should eql('One')
122
122
  instance.get('SECOND').should eql('Two')
123
123
  end
124
124
 
125
125
  it 'handles variables with numbers and underscores in their names' do
126
- instance = Recap::Environment.from_string("THIS_1=One\nThose_2=Two\n")
126
+ instance = Recap::Support::Environment.from_string("THIS_1=One\nThose_2=Two\n")
127
127
  instance.get('THIS_1').should eql('One')
128
128
  instance.get('Those_2').should eql('Two')
129
129
  end
130
130
 
131
131
  it 'gracefully ignores missing newline at end of string' do
132
- instance = Recap::Environment.from_string("FIRST=One\nSECOND=Two")
132
+ instance = Recap::Support::Environment.from_string("FIRST=One\nSECOND=Two")
133
133
  instance.get('FIRST').should eql('One')
134
134
  instance.get('SECOND').should eql('Two')
135
135
  end
136
136
 
137
137
  it 'acts as the inverse of #to_s' do
138
138
  string = "FIRST=One\nSECOND=Two\nTHIRD=three\n"
139
- excercised = Recap::Environment.from_string(Recap::Environment.from_string(string).to_s).to_s
139
+ excercised = Recap::Support::Environment.from_string(Recap::Support::Environment.from_string(string).to_s).to_s
140
140
  excercised.should eql(string)
141
141
  end
142
142
  end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe Recap::Support::ShellCommand do
4
+ subject { Recap::Support::ShellCommand }
5
+
6
+ it 'returns stdout output if execution succeeds' do
7
+ subject.execute("echo 'foo'").should eql("foo\n")
8
+ end
9
+
10
+ it 'returns stdout output from last command if execution of multiple commands succeeds' do
11
+ subject.execute("echo 'foo'", "echo 'bar'").should eql("bar\n")
12
+ end
13
+
14
+ it 'does not raise error if execution succeeds' do
15
+ lambda {
16
+ subject.execute("true")
17
+ }.should_not raise_error
18
+ end
19
+
20
+ it 'does not raise error if execution of multiple commands succeeds' do
21
+ lambda {
22
+ subject.execute("true", "true")
23
+ }.should_not raise_error
24
+ end
25
+
26
+ it 'raises error if execution fails' do
27
+ lambda {
28
+ subject.execute("false")
29
+ }.should raise_error
30
+ end
31
+
32
+ it 'raises error if execution of any command fails' do
33
+ lambda {
34
+ subject.execute("true", "false", "true")
35
+ }.should raise_error
36
+ end
37
+
38
+ it 'includes exist status in error message if execution fails' do
39
+ lambda {
40
+ subject.execute("false")
41
+ }.should raise_error(RuntimeError, %r{Command:\sfalse$})
42
+ end
43
+
44
+ it 'includes exist status in error message if execution fails' do
45
+ lambda {
46
+ subject.execute("false")
47
+ }.should raise_error(RuntimeError, %r{Status:\s+1$})
48
+ end
49
+
50
+ it 'includes stderr output in error message if execution fails' do
51
+ lambda {
52
+ subject.execute("echo 'error' 1>&2 && false")
53
+ }.should raise_error(RuntimeError, %r{Message:\s+error$})
54
+ end
55
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'recap'
2
2
 
3
- Recap::Namespace.default_config = nil
3
+ Recap::Support::Namespace.default_config = nil
4
4
 
5
5
  RSpec.configure do |config|
6
6
  config.mock_with :mocha
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
- require 'recap/bootstrap'
2
+ require 'recap/tasks/bootstrap'
3
3
 
4
- describe Recap::Bootstrap do
4
+ describe Recap::Tasks::Bootstrap do
5
5
  let :config do
6
6
  Capistrano::Configuration.new
7
7
  end
@@ -10,25 +10,21 @@ describe Recap::Bootstrap do
10
10
  config.bootstrap
11
11
  end
12
12
 
13
+ let :commands do
14
+ sequence('commands')
15
+ end
16
+
13
17
  before do
14
- Recap::Bootstrap.load_into(config)
18
+ Recap::Tasks::Bootstrap.load_into(config)
15
19
  end
16
20
 
17
21
  describe 'Tasks' do
18
22
  describe 'bootstrap' do
19
23
  it 'runs bootsrap:application and bootstrap:user tasks' do
20
- namespace.expects(:application).in_sequence
21
- namespace.expects(:user).in_sequence
24
+ namespace.expects(:application).in_sequence(commands)
25
+ namespace.expects(:user).in_sequence(commands)
22
26
  config.find_and_execute_task('bootstrap')
23
27
  end
24
28
  end
25
-
26
- describe 'bootstrap:user' do
27
- pending 'Tests not written'
28
- end
29
-
30
- describe 'bootstrap:application' do
31
- pending 'Tests not written'
32
- end
33
29
  end
34
30
  end
@@ -1,6 +1,7 @@
1
1
  require 'spec_helper'
2
+ require 'recap/tasks/bundler'
2
3
 
3
- describe Recap::Bundler do
4
+ describe Recap::Tasks::Bundler do
4
5
  let :config do
5
6
  Capistrano::Configuration.new
6
7
  end
@@ -15,7 +16,7 @@ describe Recap::Bundler do
15
16
 
16
17
  before do
17
18
  config.set :deploy_to, deploy_to
18
- Recap::Bundler.load_into(config)
19
+ Recap::Tasks::Bundler.load_into(config)
19
20
  end
20
21
 
21
22
  describe 'Settings' do
@@ -39,8 +40,8 @@ describe Recap::Bundler do
39
40
  end
40
41
 
41
42
  describe '#bundle_without' do
42
- it 'defaults to development, test and assets groups' do
43
- config.bundle_without.should eql("development test assets")
43
+ it 'defaults to development and test groups' do
44
+ config.bundle_without.should eql("development test")
44
45
  end
45
46
  end
46
47
 
@@ -94,9 +95,9 @@ describe Recap::Bundler do
94
95
  it 'aborts with warning if Gemfile exists but Gemfile.lock doesn\'t' do
95
96
  namespace.stubs(:deployed_file_exists?).with(config.bundle_gemfile).returns(true)
96
97
  namespace.stubs(:deployed_file_exists?).with(config.bundle_gemfile_lock).returns(false)
97
- lambda do
98
- namespace.find_and_execute_task('bundle:install')
99
- end.should raise_error(SystemExit, 'Gemfile found without Gemfile.lock. The Gemfile.lock should be committed to the project repository')
98
+ expected_message = 'Gemfile found without Gemfile.lock. The Gemfile.lock should be committed to the project repository'
99
+ namespace.install.expects(:abort).with(expected_message)
100
+ namespace.find_and_execute_task('bundle:install')
100
101
  end
101
102
  end
102
103
 
@@ -122,5 +123,36 @@ describe Recap::Bundler do
122
123
  config.find_and_execute_task('bundle:install:if_changed')
123
124
  end
124
125
  end
126
+
127
+ describe 'bundle:check_installed' do
128
+ before do
129
+ config.set(:application_user, 'fred')
130
+ end
131
+
132
+ it 'checks to see whether bundler is installed' do
133
+ namespace.expects(:exit_code_as_app).with('bundle --version', '.').returns("0")
134
+ config.find_and_execute_task('bundle:check_installed')
135
+ end
136
+
137
+ it 'aborts with explanation if bundler command fails' do
138
+ namespace.stubs(:exit_code_as_app).returns("1")
139
+ namespace.expects(:abort).with("The application user 'fred' cannot execute `bundle`. Please check you have bundler installed.")
140
+ config.find_and_execute_task('bundle:check_installed')
141
+ end
142
+ end
143
+ end
144
+
145
+ describe 'Callbacks' do
146
+ describe 'bundle:check_installed' do
147
+ before do
148
+ Recap::Tasks::Preflight.load_into(config)
149
+ end
150
+
151
+ it 'runs after `preflight:check`' do
152
+ config.stubs(:find_and_execute_task)
153
+ config.expects(:find_and_execute_task).with('bundle:check_installed')
154
+ config.trigger :after, config.find_task('preflight:check')
155
+ end
156
+ end
125
157
  end
126
158
  end
@@ -1,6 +1,7 @@
1
1
  require 'spec_helper'
2
+ require 'recap/tasks/deploy'
2
3
 
3
- describe Recap::Deploy do
4
+ describe Recap::Tasks::Deploy do
4
5
  let :config do
5
6
  Capistrano::Configuration.new
6
7
  end
@@ -9,8 +10,12 @@ describe Recap::Deploy do
9
10
  config.deploy
10
11
  end
11
12
 
13
+ let :commands do
14
+ sequence('commands')
15
+ end
16
+
12
17
  before do
13
- Recap::Deploy.load_into(config)
18
+ Recap::Tasks::Deploy.load_into(config)
14
19
  end
15
20
 
16
21
  it 'configures capistrano to use ssh key forwarding' do
@@ -24,17 +29,15 @@ describe Recap::Deploy do
24
29
  describe 'Settings' do
25
30
  describe '#application' do
26
31
  it 'exits if accessed before being set' do
27
- lambda do
28
- config.application
29
- end.should raise_error(SystemExit)
32
+ namespace.expects(:abort).with(regexp_matches(/You must set the name of your application in your Capfile/))
33
+ config.application
30
34
  end
31
35
  end
32
36
 
33
37
  describe '#repository' do
34
38
  it 'exits if accessed before being set' do
35
- lambda do
36
- config.repository
37
- end.should raise_error(SystemExit)
39
+ namespace.expects(:abort).with(regexp_matches(/You must set the git respository location in your Capfile/))
40
+ config.repository
38
41
  end
39
42
  end
40
43
 
@@ -59,10 +62,10 @@ describe Recap::Deploy do
59
62
  end
60
63
 
61
64
  describe '#deploy_to' do
62
- it 'defaults to a folder within the application user home directory' do
65
+ it 'defaults to an `app` folder within the application user home directory' do
63
66
  config.set :application, 'hare'
64
67
  config.set :application_user, 'rabbitfoot'
65
- config.deploy_to.should eql('/home/rabbitfoot/apps/hare')
68
+ config.deploy_to.should eql('/home/rabbitfoot/app')
66
69
  end
67
70
  end
68
71
 
@@ -103,7 +106,10 @@ describe Recap::Deploy do
103
106
  end
104
107
 
105
108
  describe 'deploy:setup' do
106
- it 'runs deploy:clone_code task' do
109
+ it 'runs env:set and deploy:clone_code tasks' do
110
+ env = stub('env')
111
+ config.stubs(:env).returns(env)
112
+ env.expects('set')
107
113
  namespace.expects(:clone_code)
108
114
  config.find_and_execute_task('deploy:setup')
109
115
  end
@@ -113,22 +119,33 @@ describe Recap::Deploy do
113
119
  namespace.expects(:clone_code).never
114
120
  config.find_and_execute_task('deploy:setup')
115
121
  end
122
+
123
+ it 'removes the deploy_to dir if a rollback is triggered' do
124
+ config.stubs(:env).returns(stub_everything('env'))
125
+ namespace.stubs(:as_app)
126
+ namespace.expects(:as_app).with('rm -fr ' + deploy_to)
127
+ namespace.stubs(:git).raises(RuntimeError)
128
+ config.find_and_execute_task('deploy:setup') rescue RuntimeError
129
+ end
116
130
  end
117
131
 
118
132
  describe 'deploy:clone_code' do
119
133
  it 'creates deploy_to dir, ensures it\'s group writable, then clones the repository into it' do
120
- namespace.expects(:as_app).with('mkdir -p ' + deploy_to, '~').in_sequence
121
- namespace.expects(:as_app).with('chmod g+rw ' + deploy_to).in_sequence
122
- namespace.expects(:git).with('clone ' + repository + ' .').in_sequence
134
+ namespace.expects(:as_app).with('mkdir -p ' + deploy_to, '~').in_sequence(commands)
135
+ namespace.expects(:as_app).with('chmod g+rw ' + deploy_to).in_sequence(commands)
136
+ namespace.expects(:git).with('clone ' + repository + ' .').in_sequence(commands)
123
137
  config.find_and_execute_task('deploy:clone_code')
124
138
  end
125
139
  end
126
140
 
127
141
  describe 'deploy' do
128
- it 'runs deploy:update_code, deploy:tag and then deploy:restart tasks' do
129
- namespace.expects(:update_code).in_sequence
130
- namespace.expects(:tag).in_sequence
131
- namespace.expects(:restart).in_sequence
142
+ it 'runs env:set, deploy:update_code, deploy:tag and then deploy:restart tasks' do
143
+ env = stub('env')
144
+ config.stubs(:env).returns(env)
145
+ env.expects('set')
146
+ namespace.expects(:update_code).in_sequence(commands)
147
+ namespace.expects(:tag).in_sequence(commands)
148
+ namespace.expects(:restart).in_sequence(commands)
132
149
  config.find_and_execute_task('deploy')
133
150
  end
134
151
 
@@ -154,8 +171,8 @@ describe Recap::Deploy do
154
171
  describe 'deploy:update_code' do
155
172
  it 'fetches latest changes, then resets to repository branch' do
156
173
  config.set :branch, 'release-branch'
157
- namespace.expects(:git).with('fetch').in_sequence
158
- namespace.expects(:git).with('reset --hard origin/release-branch').in_sequence
174
+ namespace.expects(:git).with('fetch').in_sequence(commands)
175
+ namespace.expects(:git).with('reset --hard origin/release-branch').in_sequence(commands)
159
176
  namespace.find_and_execute_task('deploy:update_code')
160
177
  end
161
178
  end
@@ -176,17 +193,16 @@ describe Recap::Deploy do
176
193
  it 'deletes latest tag, resets to previous tag and restarts' do
177
194
  config.stubs(:latest_tag).returns('release-2')
178
195
  config.stubs(:latest_tag_from_repository).returns('release-1')
179
- namespace.expects(:git).with('tag -d release-2').in_sequence
180
- namespace.expects(:git).with('reset --hard release-1').in_sequence
181
- namespace.expects(:restart).in_sequence
196
+ namespace.expects(:git).with('tag -d release-2').in_sequence(commands)
197
+ namespace.expects(:git).with('reset --hard release-1').in_sequence(commands)
198
+ namespace.expects(:restart).in_sequence(commands)
182
199
  namespace.find_and_execute_task('deploy:rollback')
183
200
  end
184
201
 
185
202
  it 'aborts if no tag has been deployed' do
186
203
  config.stubs(:latest_tag).returns(nil)
187
- lambda do
188
- namespace.find_and_execute_task('deploy:rollback')
189
- end.should raise_error(SystemExit, 'This app is not currently deployed')
204
+ namespace.rollback.expects(:abort).with('This app is not currently deployed')
205
+ namespace.find_and_execute_task('deploy:rollback')
190
206
  end
191
207
  end
192
208