recap 0.1.0 → 0.2.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 (61) hide show
  1. data/.gitignore +3 -0
  2. data/.travis.yml +4 -0
  3. data/README.md +12 -3
  4. data/Rakefile +8 -0
  5. data/Vagrantfile +61 -0
  6. data/doc/index.html +45 -26
  7. data/doc/lib/recap/bootstrap.html +42 -0
  8. data/doc/lib/recap/bundler.html +36 -19
  9. data/doc/lib/recap/capistrano_extensions.html +28 -23
  10. data/doc/lib/recap/cli.html +3 -0
  11. data/doc/lib/recap/compatibility.html +6 -3
  12. data/doc/lib/recap/deploy.html +41 -86
  13. data/doc/lib/recap/env.html +6 -1
  14. data/doc/lib/recap/foreman.html +3 -0
  15. data/doc/lib/recap/namespace.html +42 -0
  16. data/doc/lib/recap/preflight.html +12 -7
  17. data/doc/lib/recap/rails.html +3 -0
  18. data/doc/lib/recap/version.html +3 -0
  19. data/doc/lib/recap.html +42 -0
  20. data/features/bundling-gems.feature +18 -0
  21. data/features/deploying-projects.feature +21 -0
  22. data/features/managing-processes.feature +17 -0
  23. data/features/setting-environment-variables.feature +21 -0
  24. data/features/steps/capistrano_steps.rb +98 -0
  25. data/features/support/project.rb +211 -0
  26. data/features/support/server.rb +53 -0
  27. data/features/templates/gem/binary.erb +43 -0
  28. data/features/templates/gem/gemspec.erb +11 -0
  29. data/features/templates/project/Capfile +21 -0
  30. data/features/templates/project/Capfile.erb +21 -0
  31. data/features/templates/project/Gemfile.erb +7 -0
  32. data/features/templates/project/Procfile.erb +1 -0
  33. data/index.rb +26 -17
  34. data/lib/recap/bootstrap.rb +47 -0
  35. data/lib/recap/bundler.rb +31 -21
  36. data/lib/recap/capistrano_extensions.rb +11 -9
  37. data/lib/recap/cli.rb +1 -1
  38. data/lib/recap/compatibility.rb +3 -3
  39. data/lib/recap/deploy.rb +45 -57
  40. data/lib/recap/env.rb +30 -26
  41. data/lib/recap/environment.rb +54 -0
  42. data/lib/recap/foreman.rb +28 -9
  43. data/lib/recap/namespace.rb +37 -0
  44. data/lib/recap/preflight.rb +10 -8
  45. data/lib/recap/rails.rb +6 -4
  46. data/lib/recap/ruby.rb +3 -0
  47. data/lib/recap/static.rb +1 -0
  48. data/lib/recap/version.rb +1 -1
  49. data/lib/recap.rb +12 -0
  50. data/recap.gemspec +8 -4
  51. data/spec/models/environment_spec.rb +143 -0
  52. data/spec/spec_helper.rb +7 -0
  53. data/spec/tasks/bootstrap_spec.rb +34 -0
  54. data/spec/tasks/bundler_spec.rb +126 -0
  55. data/spec/tasks/deploy_spec.rb +209 -0
  56. data/spec/tasks/env_spec.rb +38 -0
  57. data/spec/tasks/foreman_spec.rb +154 -0
  58. data/test-vm/manifests/base.pp +17 -0
  59. data/test-vm/share/.gitkeep +0 -0
  60. metadata +138 -19
  61. /data/bin/{tomafro-deploy → recap} +0 -0
@@ -0,0 +1,54 @@
1
+ class Recap::Environment
2
+ def initialize(variables = {})
3
+ @variables = variables
4
+ end
5
+
6
+ def get(name)
7
+ @variables[name]
8
+ end
9
+
10
+ def set(name, value)
11
+ if value.nil? || value.empty?
12
+ @variables.delete(name)
13
+ else
14
+ @variables[name] = value
15
+ end
16
+ end
17
+
18
+ def set_string(string)
19
+ if string =~ /\A([A-Za-z0-9_]+)=(.*)\z/
20
+ set $1, $2
21
+ end
22
+ end
23
+
24
+ def empty?
25
+ @variables.empty?
26
+ end
27
+
28
+ def merge(hash)
29
+ hash.each {|k, v| set(k, v)}
30
+ end
31
+
32
+ def each(&block)
33
+ @variables.sort.each(&block)
34
+ end
35
+
36
+ def include?(key)
37
+ @variables.include?(key)
38
+ end
39
+
40
+ def to_s
41
+ @variables.keys.sort.map do |key|
42
+ key + "=" + @variables[key] + "\n" if @variables[key]
43
+ end.compact.join
44
+ end
45
+
46
+ class << self
47
+ def from_string(string)
48
+ string.split("\n").inject(new) do |env, line|
49
+ env.set_string(line)
50
+ env
51
+ end
52
+ end
53
+ end
54
+ end
data/lib/recap/foreman.rb CHANGED
@@ -1,39 +1,58 @@
1
- Capistrano::Configuration.instance(:must_exist).load do
1
+ module Recap::Foreman
2
+ extend Recap::Namespace
3
+
2
4
  namespace :foreman do
3
- set(:procfile) {"#{deploy_to}/Procfile"}
5
+ # Processes are delcared in a `Procfile`, by default in the root of the application directory
6
+ set(:procfile) { "#{deploy_to}/Procfile" }
7
+
8
+ # Foreman startup scripts are exported in `upstart` format by default
4
9
  set(:foreman_export_format, "upstart")
10
+
11
+ # Scripts are exported (as the the application user) to a temporary location first
12
+ set(:foreman_tmp_location) { "#{deploy_to}/tmp/foreman" }
13
+
14
+ # After exports, the scripts are moved to their final location, usually `/etc/init`
5
15
  set(:foreman_export_location, "/etc/init")
6
16
 
17
+ # The standard foreman export
18
+ set(:foreman_export_command) { "./bin/foreman export #{foreman_export_format} #{foreman_tmp_location} --procfile #{procfile} --app #{application} --user #{application_user} --log #{deploy_to}/log" }
19
+
7
20
  namespace :export do
21
+ # After each deployment, the startup scripts are exported if the `Procfile` has changed
8
22
  task :if_changed do
9
23
  if deployed_file_changed?(procfile)
10
24
  top.foreman.export.default
11
25
  end
12
26
  end
13
27
 
14
- task :default, :roles => :app do
28
+ # To export the scripts, they are first generated in a temporary location, then copied to their final
29
+ # destination. This is done because the foreman export command needs to be run as the application user,
30
+ # while sudo is required to write to `/etc/init`.
31
+ task :default do
15
32
  if deployed_file_exists?(procfile)
16
- tmp = "#{deploy_to}/tmp/foreman"
17
- as_app "./bin/foreman export #{foreman_export_format} #{tmp} --procfile #{procfile} --app #{application} --user #{application_user} --log #{deploy_to}/log"
33
+ as_app foreman_export_command
18
34
  sudo "rm -f #{foreman_export_location}/#{application}*"
19
- sudo "cp #{tmp}/* #{foreman_export_location}"
35
+ sudo "cp #{foreman_tmp_location}/* #{foreman_export_location}"
20
36
  end
21
37
  end
22
38
  end
23
39
 
24
- task :start, :roles => :app do
40
+ # Starts all processes that form the application
41
+ task :start do
25
42
  if deployed_file_exists?(procfile)
26
43
  sudo "start #{application}"
27
44
  end
28
45
  end
29
46
 
30
- task :restart, :roles => :app do
47
+ # Restarts all processes that form the application
48
+ task :restart do
31
49
  if deployed_file_exists?(procfile)
32
50
  sudo "restart #{application} || sudo start #{application}"
33
51
  end
34
52
  end
35
53
 
36
- task :stop, :roles => :app do
54
+ # Stops all processes that form the application
55
+ task :stop do
37
56
  if deployed_file_exists?(procfile)
38
57
  sudo "stop #{application}"
39
58
  end
@@ -0,0 +1,37 @@
1
+ require 'capistrano'
2
+ require 'recap/capistrano_extensions'
3
+
4
+ module Recap::Namespace
5
+ def self.default_config
6
+ @default_config
7
+ end
8
+
9
+ def self.default_config=(config)
10
+ @default_config = config
11
+ end
12
+
13
+ if Capistrano::Configuration.instance
14
+ self.default_config = Capistrano::Configuration.instance(:must_exist)
15
+ end
16
+
17
+ def capistrano_definitions
18
+ @capistrano_definitions ||= []
19
+ end
20
+
21
+ def namespace(name, &block)
22
+ capistrano_definitions << Proc.new do
23
+ namespace name do
24
+ instance_eval(&block)
25
+ end
26
+ end
27
+
28
+ load_into(Recap::Namespace.default_config) if Recap::Namespace.default_config
29
+ end
30
+
31
+ def load_into(configuration)
32
+ configuration.extend(self)
33
+ capistrano_definitions.each do |definition|
34
+ configuration.load(&definition)
35
+ end
36
+ end
37
+ end
@@ -17,17 +17,19 @@
17
17
  # This preflight recipe checks each of these things in turn, and attempts to give helpful advice
18
18
  # should a check fail.
19
19
 
20
- Capistrano::Configuration.instance(:must_exist).load do
21
- # The preflight check is pretty quick, so run it before every `deploy:setup` and `deploy`
22
- before 'deploy:setup', 'preflight:check'
23
- before 'deploy', 'preflight:check'
24
-
25
- set(:remote_username) { capture('whoami').strip }
20
+ module Recap::Preflight
21
+ extend Recap::Namespace
26
22
 
27
23
  namespace :preflight do
24
+ # The preflight check is pretty quick, so run it before every `deploy:setup` and `deploy`
25
+ before 'deploy:setup', 'preflight:check'
26
+ before 'deploy', 'preflight:check'
27
+
28
+ set(:remote_username) { capture('whoami').strip }
29
+
28
30
  task :check do
29
31
  # First check the `application_user` exists
30
- if capture("id #{application_user} > /dev/null 2>&1; echo $?").strip != "0"
32
+ if exit_code("id #{application_user}").strip != "0"
31
33
  abort %{
32
34
  The application user '#{application_user}' doesn't exist. You can create this user by logging into the server and running:
33
35
 
@@ -36,7 +38,7 @@ The application user '#{application_user}' doesn't exist. You can create this u
36
38
  end
37
39
 
38
40
  # Then the `application_group`
39
- if capture("id -g #{application_group} > /dev/null 2>&1; echo $?").strip != "0"
41
+ if exit_code("id -g #{application_group}") != "0"
40
42
  abort %{
41
43
  The application group '#{application_group}' doesn't exist. You can create this group by logging into the server and running:
42
44
 
data/lib/recap/rails.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'recap/deploy'
2
2
 
3
- Capistrano::Configuration.instance(:must_exist).load do
3
+ module Recap::Rails
4
+ extend Recap::Namespace
5
+
4
6
  namespace :rails do
5
7
  namespace :db do
6
8
  task :load_schema do
@@ -15,8 +17,8 @@ Capistrano::Configuration.instance(:must_exist).load do
15
17
  end
16
18
  end
17
19
  end
18
- end
19
20
 
20
- after "deploy:clone_code", "rails:db:load_schema"
21
- after "deploy:update_code", "rails:db:migrate"
21
+ after "deploy:clone_code", "rails:db:load_schema"
22
+ after "deploy:update_code", "rails:db:migrate"
23
+ end
22
24
  end
data/lib/recap/ruby.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'recap/deploy'
2
+ require 'recap/bundler'
3
+ require 'recap/foreman'
@@ -0,0 +1 @@
1
+ require 'recap/deploy'
data/lib/recap/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Recap
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/recap.rb ADDED
@@ -0,0 +1,12 @@
1
+ module Recap
2
+ autoload :Namespace, 'recap/namespace'
3
+
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
data/recap.gemspec CHANGED
@@ -7,19 +7,23 @@ 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 = "https://github.com/freerange/recap"
10
+ s.homepage = "http://code.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
 
14
- s.rubyforge_project = "recap"
15
-
16
14
  s.files = `git ls-files`.split("\n")
17
15
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
16
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
17
  s.require_paths = ["lib"]
20
18
 
21
- s.add_dependency('capistrano', '~>2.6.0')
19
+ s.add_dependency('capistrano', '~>2.9.0')
22
20
  s.add_dependency('thor')
23
21
  s.add_development_dependency('rake', '~>0.9.2')
24
22
  s.add_development_dependency('rocco', '~>0.8.1')
23
+ s.add_development_dependency('rspec', '~>2.7.0')
24
+ s.add_development_dependency('mocha', '~>0.10.0')
25
+ s.add_development_dependency('vagrant', '~>0.9.7')
26
+ s.add_development_dependency('sahara', '~>0.0.10')
27
+ s.add_development_dependency('cucumber', '~>1.1.4')
28
+ s.add_development_dependency('faker', '~>1.0.1')
25
29
  end
@@ -0,0 +1,143 @@
1
+ require 'spec_helper'
2
+
3
+ describe Recap::Environment do
4
+ describe '#empty?' do
5
+ it 'returns true if no variables set' do
6
+ Recap::Environment.new.empty?.should be_true
7
+ end
8
+
9
+ it 'returns false if no variables set' do
10
+ Recap::Environment.new('FIRST' => 'One').empty?.should be_false
11
+ end
12
+ end
13
+
14
+ describe '#include?(key)' do
15
+ it 'returns true if variables set' do
16
+ Recap::Environment.new('FIRST' => 'One').include?('FIRST').should be_true
17
+ end
18
+
19
+ it 'returns false if variable has not been set' do
20
+ Recap::Environment.new('DIFFERENT' => 'One').include?('FIRST').should be_false
21
+ end
22
+ end
23
+
24
+ describe '#get(name)' do
25
+ subject do
26
+ Recap::Environment.new('FIRST' => 'One')
27
+ end
28
+
29
+ it 'returns value if variable set' do
30
+ subject.get('FIRST').should eql('One')
31
+ end
32
+
33
+ it 'returns nil if variable not set' do
34
+ subject.get('MISSING').should be_nil
35
+ end
36
+ end
37
+
38
+ describe '#set(name, value)' do
39
+ subject do
40
+ Recap::Environment.new('FIRST' => 'One')
41
+ end
42
+
43
+ it 'sets variable value' do
44
+ subject.set('SECOND', 'Two')
45
+ subject.get('SECOND').should eql('Two')
46
+ end
47
+
48
+ it 'unsets variable if value is nil' do
49
+ subject.set('FIRST', nil)
50
+ subject.get('FIRST').should be_nil
51
+ subject.include?('FIRST').should be_false
52
+ end
53
+
54
+ it 'unsets variable if value is empty' do
55
+ subject.set('FIRST', '')
56
+ subject.get('FIRST').should be_nil
57
+ subject.include?('FIRST').should be_false
58
+ end
59
+ end
60
+
61
+ describe '#each' do
62
+ subject do
63
+ Recap::Environment.new('FIRST' => 'One', 'SECOND' => 'Two', 'THIRD' => 'Three', 'FOURTH' => 'Four')
64
+ end
65
+
66
+ it 'yields each variable and value in turn (ordered alphabetically)' do
67
+ result = []
68
+ subject.each do |k, v|
69
+ result << [k, v]
70
+ end
71
+ result.should eql([['FIRST', 'One'], ['FOURTH', 'Four'], ['SECOND', 'Two'], ['THIRD', 'Three']])
72
+ end
73
+ end
74
+
75
+ describe '#merge(variables)' do
76
+ subject do
77
+ Recap::Environment.new('FIRST' => 'One')
78
+ end
79
+
80
+ it 'sets each variable value' do
81
+ subject.merge('SECOND' => 'Two', 'THIRD' => 'Three')
82
+ subject.get('SECOND').should eql('Two')
83
+ subject.get('THIRD').should eql('Three')
84
+ end
85
+
86
+ it 'preserves existing values if not provided' do
87
+ subject.merge('ANYTHING' => 'Goes')
88
+ subject.get('FIRST').should eql('One')
89
+ end
90
+
91
+ it 'overides existing values if provided' do
92
+ subject.merge('FIRST' => 'Un')
93
+ subject.get('FIRST').should eql('Un')
94
+ end
95
+ end
96
+
97
+ describe '#to_s' do
98
+ subject do
99
+ Recap::Environment.new('FIRST' => 'One', 'SECOND' => 'Two', 'THIRD' => nil, 'FOURTH' => 'Four').to_s
100
+ end
101
+
102
+ it 'declares each variable on its own line' do
103
+ subject.match(/^FIRST=One\n/).should_not be_nil
104
+ subject.match(/^SECOND=Two\n/).should_not be_nil
105
+ subject.match(/^FOURTH=Four\n/).should_not be_nil
106
+ end
107
+
108
+ it 'ignores nil variable values' do
109
+ subject.match(/THIRD/).should be_nil
110
+ end
111
+
112
+ it 'orders variables alphabetically' do
113
+ indexes = ['FIRST', 'FOURTH', 'SECOND'].map {|k| subject.index(k)}
114
+ indexes.sort.should eql(indexes)
115
+ end
116
+ end
117
+
118
+ describe '.from_string(declarations)' do
119
+ it 'builds instance using string representation' do
120
+ instance = Recap::Environment.from_string("FIRST=One\nSECOND=Two\n")
121
+ instance.get('FIRST').should eql('One')
122
+ instance.get('SECOND').should eql('Two')
123
+ end
124
+
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")
127
+ instance.get('THIS_1').should eql('One')
128
+ instance.get('Those_2').should eql('Two')
129
+ end
130
+
131
+ it 'gracefully ignores missing newline at end of string' do
132
+ instance = Recap::Environment.from_string("FIRST=One\nSECOND=Two")
133
+ instance.get('FIRST').should eql('One')
134
+ instance.get('SECOND').should eql('Two')
135
+ end
136
+
137
+ it 'acts as the inverse of #to_s' do
138
+ string = "FIRST=One\nSECOND=Two\nTHIRD=three\n"
139
+ excercised = Recap::Environment.from_string(Recap::Environment.from_string(string).to_s).to_s
140
+ excercised.should eql(string)
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,7 @@
1
+ require 'recap'
2
+
3
+ Recap::Namespace.default_config = nil
4
+
5
+ RSpec.configure do |config|
6
+ config.mock_with :mocha
7
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+ require 'recap/bootstrap'
3
+
4
+ describe Recap::Bootstrap do
5
+ let :config do
6
+ Capistrano::Configuration.new
7
+ end
8
+
9
+ let :namespace do
10
+ config.bootstrap
11
+ end
12
+
13
+ before do
14
+ Recap::Bootstrap.load_into(config)
15
+ end
16
+
17
+ describe 'Tasks' do
18
+ describe 'bootstrap' do
19
+ it 'runs bootsrap:application and bootstrap:user tasks' do
20
+ namespace.expects(:application).in_sequence
21
+ namespace.expects(:user).in_sequence
22
+ config.find_and_execute_task('bootstrap')
23
+ end
24
+ 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
+ end
34
+ end
@@ -0,0 +1,126 @@
1
+ require 'spec_helper'
2
+
3
+ describe Recap::Bundler do
4
+ let :config do
5
+ Capistrano::Configuration.new
6
+ end
7
+
8
+ let :namespace do
9
+ config.bundle
10
+ end
11
+
12
+ let :deploy_to do
13
+ 'path/to/deploy/to'
14
+ end
15
+
16
+ before do
17
+ config.set :deploy_to, deploy_to
18
+ Recap::Bundler.load_into(config)
19
+ end
20
+
21
+ describe 'Settings' do
22
+ describe '#bundle_gemfile' do
23
+ it 'defaults to deploy_to + /Gemfile' do
24
+ config.bundle_gemfile.should eql(deploy_to + '/Gemfile')
25
+ end
26
+ end
27
+
28
+ describe '#bundle_gemfile_lock' do
29
+ it 'defaults to bundle_gemfile + .lock' do
30
+ config.set :bundle_gemfile, 'custom/Gemfile'
31
+ config.bundle_gemfile_lock.should eql('custom/Gemfile.lock')
32
+ end
33
+ end
34
+
35
+ describe '#bundle_path' do
36
+ it 'defaults to deploy_to + /vendor/gems' do
37
+ config.bundle_path.should eql(deploy_to + '/vendor/gems')
38
+ end
39
+ end
40
+
41
+ describe '#bundle_without' do
42
+ it 'defaults to development, test and assets groups' do
43
+ config.bundle_without.should eql("development test assets")
44
+ end
45
+ end
46
+
47
+ describe '#bundle_install_command' do
48
+ it 'takes --gemfile from the bundle_gemfile setting' do
49
+ config.set :bundle_gemfile, 'path/to/bundle/Gemfile'
50
+ config.bundle_install_command.include?(" --gemfile path/to/bundle/Gemfile ").should be_true
51
+ end
52
+
53
+ it 'takes --path from the bundle_path setting' do
54
+ config.set :bundle_path, 'path/to/install/gems'
55
+ config.bundle_install_command.include?(" --path path/to/install/gems ").should be_true
56
+ end
57
+
58
+ it 'takes --without from the bundle_without setting' do
59
+ config.set :bundle_without, 'groups to skip'
60
+ config.bundle_install_command.include?(" --without groups to skip").should be_true
61
+ end
62
+
63
+ it 'includes --deployment flag to ensure Gemfile.lock exists' do
64
+ config.bundle_install_command.include?(" --deployment ").should be_true
65
+ end
66
+
67
+ it 'includes --binstubs flag to generate binary stubs used by other tasks' do
68
+ config.bundle_install_command.include?(" --binstubs ").should be_true
69
+ end
70
+
71
+ it 'includes --quiet flag to reduce uneccessary noise' do
72
+ config.bundle_install_command.include?(" --quiet ").should be_true
73
+ end
74
+ end
75
+ end
76
+
77
+ describe 'Tasks' do
78
+ describe 'bundle:install' do
79
+ it 'run bundle_install_command as the app if the Gemfile and Gemfile.lock exist' do
80
+ namespace.stubs(:deployed_file_exists?).with(config.bundle_gemfile).returns(true)
81
+ namespace.stubs(:deployed_file_exists?).with(config.bundle_gemfile_lock).returns(true)
82
+ namespace.expects(:as_app).with(config.bundle_install_command)
83
+
84
+ config.find_and_execute_task('bundle:install')
85
+ end
86
+
87
+ it 'skips bundle_install if the Gemfile missing' do
88
+ namespace.stubs(:deployed_file_exists?).with(config.bundle_gemfile).returns(false)
89
+ namespace.expects(:as_app).never
90
+
91
+ config.find_and_execute_task('bundle:install')
92
+ end
93
+
94
+ it 'aborts with warning if Gemfile exists but Gemfile.lock doesn\'t' do
95
+ namespace.stubs(:deployed_file_exists?).with(config.bundle_gemfile).returns(true)
96
+ 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')
100
+ end
101
+ end
102
+
103
+ describe 'bundle:install:if_changed' do
104
+ it 'calls bundle:install:default if the Gemfile.lock has changed' do
105
+ namespace.stubs(:deployed_file_changed?).with(config.bundle_gemfile).returns(false)
106
+ namespace.stubs(:deployed_file_changed?).with(config.bundle_gemfile_lock).returns(true)
107
+ namespace.install.expects(:default)
108
+ config.find_and_execute_task('bundle:install:if_changed')
109
+ end
110
+
111
+ it 'calls bundle:install:default if the Gemfile has changed' do
112
+ namespace.stubs(:deployed_file_changed?).with(config.bundle_gemfile).returns(true)
113
+ namespace.stubs(:deployed_file_changed?).with(config.bundle_gemfile_lock).returns(false)
114
+ namespace.install.expects(:default)
115
+ config.find_and_execute_task('bundle:install:if_changed')
116
+ end
117
+
118
+ it 'skips bundle_install if neither Gemfile nor Gemfile.lock have changed' do
119
+ namespace.stubs(:deployed_file_changed?).with(config.bundle_gemfile).returns(false)
120
+ namespace.stubs(:deployed_file_changed?).with(config.bundle_gemfile_lock).returns(false)
121
+ namespace.install.expects(:default).never
122
+ config.find_and_execute_task('bundle:install:if_changed')
123
+ end
124
+ end
125
+ end
126
+ end