recap 0.1.0 → 0.2.0

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