recap 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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