harrison 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,51 @@
1
+ module Harrison
2
+ class Deploy::Phase
3
+ attr_accessor :name
4
+
5
+ def initialize(name, &phase_config)
6
+ self.name = name
7
+
8
+ @conditions = Array.new
9
+
10
+ yield self if block_given?
11
+ end
12
+
13
+ def add_condition(&block)
14
+ @conditions << block
15
+ end
16
+
17
+ # Ensure all conditions eval to true for this context.
18
+ def matches_context?(context)
19
+ @conditions.all? { |cblock| cblock.call(context) }
20
+ end
21
+
22
+ def on_run(&block)
23
+ @run_block = block
24
+ end
25
+
26
+ def on_fail(&block)
27
+ @fail_block = block
28
+ end
29
+
30
+ # These should only be invoked by the deploy action.
31
+ def _run(context)
32
+ return unless matches_context?(context)
33
+
34
+ if @run_block
35
+ puts "[#{context.host}] Executing \"#{self.name}\"..."
36
+ @run_block.call(context)
37
+ end
38
+ end
39
+
40
+ def _fail(context)
41
+ # Ensure all conditions eval to true for this context.
42
+ return unless matches_context?(context)
43
+
44
+ if @fail_block
45
+ puts "[#{context.host}] Reverting \"#{self.name}\"..."
46
+ @fail_block.call(context)
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -23,7 +23,11 @@ module Harrison
23
23
  def remote_exec(cmd)
24
24
  ensure_remote_dir("#{remote_project_dir}/package")
25
25
 
26
- super("cd #{remote_project_dir}/package && #{cmd}")
26
+ if @_remote_context
27
+ super("cd #{@_remote_context} && #{cmd}")
28
+ else
29
+ super("cd #{remote_project_dir}/package && #{cmd}")
30
+ end
27
31
  end
28
32
 
29
33
  def run(&block)
@@ -40,18 +44,22 @@ module Harrison
40
44
  # Fetch/clone git repo on remote host.
41
45
  remote_exec("if [ -d cached ] ; then cd cached && git fetch origin -p ; else git clone #{git_src} cached ; fi")
42
46
 
43
- # Check out target commit.
44
- remote_exec("cd cached && git reset --hard #{commit} && git clean -f -d")
45
-
46
47
  # Make a build folder of the target commit.
47
- remote_exec("rm -rf #{commit} && cp -a cached #{commit}")
48
+ remote_exec("rm -rf #{artifact_name(commit)} && cp -a cached #{artifact_name(commit)}")
48
49
 
49
- # Run user supplied build code.
50
- # TODO: alter remote_exec to set directory context to commit dir?
51
- super
50
+ # Check out target commit.
51
+ remote_exec("cd #{artifact_name(commit)} && git reset --hard #{commit} && git clean -f -d")
52
+
53
+ # Run user supplied build code in the context of the checked out code.
54
+ begin
55
+ @_remote_context = "#{remote_project_dir}/package/#{artifact_name(commit)}"
56
+ super
57
+ ensure
58
+ @_remote_context = nil
59
+ end
52
60
 
53
61
  # Package build folder into tgz.
54
- remote_exec("rm -f #{artifact_name(commit)}.tar.gz && cd #{commit} && tar #{excludes_for_tar} -czf ../#{artifact_name(commit)}.tar.gz .")
62
+ remote_exec("rm -f #{artifact_name(commit)}.tar.gz && cd #{artifact_name(commit)} && tar #{excludes_for_tar} -czf ../#{artifact_name(commit)}.tar.gz .")
55
63
 
56
64
  if match = remote_regex.match(destination)
57
65
  # Copy artifact to remote destination.
@@ -65,7 +73,7 @@ module Harrison
65
73
  end
66
74
 
67
75
  if purge
68
- remote_exec("cd .. && rm -rf package")
76
+ remote_exec("rm -rf #{artifact_name(commit)}")
69
77
  end
70
78
 
71
79
  puts "Sucessfully packaged #{commit} to #{destination}/#{artifact_name(commit)}.tar.gz"
data/lib/harrison/ssh.rb CHANGED
@@ -14,26 +14,32 @@ module Harrison
14
14
  end
15
15
 
16
16
  def exec(command)
17
- puts "INFO (ssh-exec #{desc}): #{command}" if Harrison::DEBUG
17
+ puts "[#{desc}] INFO: ssh-exec #{command}" if Harrison::DEBUG
18
18
 
19
19
  result = invoke(@conn, command)
20
20
 
21
21
  if Harrison::DEBUG || result[:status] != 0
22
- warn "STDERR (ssh-exec #{desc}): #{result[:stderr]}" unless result[:stderr].empty?
23
- warn "STDOUT (ssh-exec #{desc}): #{result[:stdout]}" unless result[:stdout].empty?
22
+ warn "[#{desc}] STDERR: #{result[:stderr]}" unless result[:stderr].empty?
23
+ warn "[#{desc}] STDOUT: #{result[:stdout]}" unless result[:stdout].empty?
24
24
  end
25
25
 
26
26
  (result[:status] == 0) ? result[:stdout] : nil
27
27
  end
28
28
 
29
29
  def download(remote_path, local_path)
30
- puts "INFO (scp-down #{desc}): #{local_path} <<< #{remote_path}" if Harrison::DEBUG
30
+ puts "[#{desc}] INFO: scp-down #{local_path} <<< #{remote_path}" if Harrison::DEBUG
31
+
31
32
  @conn.scp.download!(remote_path, local_path)
33
+
34
+ return true
32
35
  end
33
36
 
34
37
  def upload(local_path, remote_path)
35
- puts "INFO (scp-up #{desc}): #{local_path} >>> #{remote_path}" if Harrison::DEBUG
38
+ puts "[#{desc}] INFO: scp-up #{local_path} >>> #{remote_path}" if Harrison::DEBUG
39
+
36
40
  @conn.scp.upload!(local_path, remote_path)
41
+
42
+ return true
37
43
  end
38
44
 
39
45
  def close
@@ -68,7 +74,7 @@ module Harrison
68
74
 
69
75
  conn.open_channel do |channel|
70
76
  channel.exec(cmd) do |ch, success|
71
- warn "Couldn't execute command (ssh.channel.exec) on remote host: #{cmd}" unless success
77
+ warn "[#{conn.host}] Couldn't execute command (ssh.channel.exec): #{cmd}" unless success
72
78
 
73
79
  channel.on_data do |ch,data|
74
80
  stdout_data += data
@@ -1,3 +1,3 @@
1
1
  module Harrison
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  require 'bundler/setup'
2
2
  Bundler.setup
3
3
 
4
+ require 'simplecov'
5
+ SimpleCov.start
6
+
4
7
  require 'harrison'
5
8
 
6
9
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
@@ -43,12 +43,8 @@ describe Harrison::Base do
43
43
  expect(instance.exec('echo "foo"')).to eq('foo')
44
44
  end
45
45
 
46
- it 'should complain if command returns non-zero' do
47
- output = capture(:stderr) do
48
- expect(lambda { instance.exec('cat noexist 2>/dev/null') }).to exit_with_code(1)
49
- end
50
-
51
- expect(output).to include('unable', 'execute', 'local', 'command')
46
+ it 'should throw :failure if command returns non-zero' do
47
+ expect(lambda { instance.exec('cat noexist 2>/dev/null') }).to throw_symbol(:failure)
52
48
  end
53
49
  end
54
50
 
@@ -64,14 +60,9 @@ describe Harrison::Base do
64
60
  expect(instance.remote_exec('remote exec')).to eq('remote_exec_return')
65
61
  end
66
62
 
67
- it 'should complain if command returns nil' do
63
+ it 'should throw :failure if command returns nil' do
68
64
  expect(@mock_ssh).to receive(:exec).and_return(nil)
69
-
70
- output = capture(:stderr) do
71
- expect(lambda { instance.remote_exec('remote exec fail') }).to exit_with_code(1)
72
- end
73
-
74
- expect(output).to include('unable', 'execute', 'remote', 'command')
65
+ expect(lambda { instance.remote_exec('remote exec fail') }).to throw_symbol(:failure)
75
66
  end
76
67
  end
77
68
 
@@ -0,0 +1,152 @@
1
+ require 'spec_helper'
2
+
3
+ describe Harrison::Deploy::Phase do
4
+ let(:instance) do
5
+ Harrison::Deploy::Phase.new(:test_phase)
6
+ end
7
+
8
+ describe 'initialize' do
9
+ it 'should set name' do
10
+ expect(instance.name).to be :test_phase
11
+ end
12
+
13
+ it 'should yield itself to passed block' do
14
+ expect { |b| Harrison::Deploy::Phase.new(:block_test, &b) }.to yield_with_args(Harrison::Deploy::Phase)
15
+ end
16
+ end
17
+
18
+ describe 'instance methods' do
19
+ describe '#add_condition' do
20
+ it 'should accept and store a block' do
21
+ instance.add_condition { |c| true }
22
+
23
+ expect(instance.instance_variable_get(:@conditions).size).to be 1
24
+ end
25
+ end
26
+
27
+ describe '#matches_context?' do
28
+ it 'should pass the given context to each condition block' do
29
+ mock_proc = double(:proc)
30
+ instance.instance_variable_set(:@conditions, [ mock_proc ])
31
+
32
+ mock_context = double(:context)
33
+
34
+ expect(mock_proc).to receive(:call).with(mock_context)
35
+
36
+ instance.matches_context?(mock_context)
37
+ end
38
+
39
+ it 'should be true if all conditions evaluate to true' do
40
+ instance.instance_variable_set(:@conditions, [
41
+ Proc.new { |c| true },
42
+ Proc.new { |c| true },
43
+ Proc.new { |c| true },
44
+ ])
45
+
46
+ expect(instance.matches_context?(double(:context))).to be true
47
+ end
48
+
49
+ it 'should be false if at least one condition evaluates to false' do
50
+ instance.instance_variable_set(:@conditions, [
51
+ Proc.new { |c| true },
52
+ Proc.new { |c| false },
53
+ Proc.new { |c| true },
54
+ ])
55
+
56
+ expect(instance.matches_context?(double(:context))).to be false
57
+ end
58
+ end
59
+
60
+ describe '#on_run' do
61
+ it 'should store the given block' do
62
+ instance.on_run { |c| true }
63
+
64
+ expect(instance.instance_variable_get(:@run_block)).to_not be_nil
65
+ end
66
+ end
67
+
68
+ describe '#on_fail' do
69
+ it 'should store the given block' do
70
+ instance.on_fail { |c| true }
71
+
72
+ expect(instance.instance_variable_get(:@fail_block)).to_not be_nil
73
+ end
74
+ end
75
+
76
+ describe '#_run' do
77
+ context 'context matches conditions' do
78
+ before(:each) do
79
+ allow(instance).to receive(:matches_context?).and_return(true)
80
+ end
81
+
82
+ it 'should invoke the previously stored block with given context' do
83
+ mock_context = double(:context, host: 'test_host')
84
+
85
+ mock_proc = double(:proc)
86
+ instance.instance_variable_set(:@run_block, mock_proc)
87
+
88
+ expect(mock_proc).to receive(:call).with(mock_context)
89
+
90
+ capture(:stdout) do
91
+ instance._run(mock_context)
92
+ end
93
+ end
94
+ end
95
+
96
+ context 'context does not match conditions' do
97
+ before(:each) do
98
+ allow(instance).to receive(:matches_context?).and_return(false)
99
+ end
100
+
101
+ it 'should not invoke the previously stored block' do
102
+ mock_context = double(:context)
103
+
104
+ mock_proc = double(:proc)
105
+ instance.instance_variable_set(:@run_block, mock_proc)
106
+
107
+ expect(mock_proc).to_not receive(:call)
108
+
109
+ instance._run(mock_context)
110
+ end
111
+ end
112
+ end
113
+
114
+ describe '#_fail' do
115
+ context 'context matches conditions' do
116
+ before(:each) do
117
+ allow(instance).to receive(:matches_context?).and_return(true)
118
+ end
119
+
120
+ it 'should invoke the previously stored block with given context' do
121
+ mock_context = double(:context, host: 'test_host')
122
+
123
+ mock_proc = double(:proc)
124
+ instance.instance_variable_set(:@fail_block, mock_proc)
125
+
126
+ expect(mock_proc).to receive(:call).with(mock_context)
127
+
128
+ capture(:stdout) do
129
+ instance._fail(mock_context)
130
+ end
131
+ end
132
+ end
133
+
134
+ context 'context does not match conditions' do
135
+ before(:each) do
136
+ allow(instance).to receive(:matches_context?).and_return(false)
137
+ end
138
+
139
+ it 'should not invoke the previously stored block' do
140
+ mock_context = double(:context)
141
+
142
+ mock_proc = double(:proc)
143
+ instance.instance_variable_set(:@fail_block, mock_proc)
144
+
145
+ expect(mock_proc).to_not receive(:call)
146
+
147
+ instance._fail(mock_context)
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -22,6 +22,10 @@ describe Harrison::Deploy do
22
22
 
23
23
  expect(instance.instance_variable_get('@options')).to include(testopt: 'foo')
24
24
  end
25
+
26
+ it 'should set up default phases' do
27
+ expect(instance.instance_variable_get(:@_phases)).to_not be_empty
28
+ end
25
29
  end
26
30
 
27
31
  describe 'instance methods' do
@@ -41,121 +45,249 @@ describe Harrison::Deploy do
41
45
  end
42
46
  end
43
47
 
48
+ describe '#add_phase' do
49
+ before(:each) do
50
+ allow(Harrison::Deploy::Phase).to receive(:new).with(anything)
51
+ end
52
+
53
+ it 'should instantiate a new Phase object with the given name' do
54
+ expect(Harrison::Deploy::Phase).to receive(:new).with(:test)
55
+
56
+ instance.add_phase(:test)
57
+ end
58
+
59
+ it 'should pass a given block to the Phase object constructor' do
60
+ @mock_phase = double(:phase)
61
+
62
+ expect(Harrison::Deploy::Phase).to receive(:new).with(:test).and_yield(@mock_phase)
63
+ expect(@mock_phase).to receive(:in_block)
64
+
65
+ instance.add_phase :test do |p|
66
+ p.in_block
67
+ end
68
+ end
69
+ end
70
+
44
71
  describe '#remote_exec' do
45
72
  before(:each) do
73
+ instance.base_dir = '/opt'
74
+ instance.project = 'test_project'
75
+
46
76
  @mock_ssh = double(:ssh)
47
77
  expect(instance).to receive(:ssh).and_return(@mock_ssh)
48
78
  end
49
79
 
50
80
  it 'should prepend project dir onto passed command' do
51
- instance.base_dir = '/opt'
52
- instance.project = 'test_project'
53
-
54
81
  expect(@mock_ssh).to receive(:exec).with("cd /opt/test_project && test_command").and_return('')
55
82
 
56
83
  instance.remote_exec("test_command")
57
84
  end
58
85
  end
59
86
 
60
- describe '#run' do
87
+ describe '#current_symlink' do
88
+ it 'starts at base_dir' do
89
+ expect(instance.current_symlink).to match(/^#{instance.base_dir}/)
90
+ end
91
+
92
+ it 'is not just base_dir' do
93
+ expect(instance.current_symlink).to_not equal(instance.base_dir)
94
+ end
95
+ end
96
+
97
+ describe '#update_current_symlink' do
61
98
  before(:each) do
62
- instance.artifact = 'test_artifact.tar.gz'
63
- instance.project = 'test_project'
99
+ instance.deploy_link = 'new_deploy'
100
+
101
+ allow(instance).to receive(:remote_exec).with(/^ln/)
64
102
  end
65
103
 
66
- context 'when passed a block' do
67
- it 'should store the block' do
68
- test_block = Proc.new { |test| "block_output" }
69
- instance.run(&test_block)
104
+ context 'current_symlink already exists' do
105
+ before(:each) do
106
+ expect(instance).to receive(:remote_exec).with(/readlink/).and_return('old_link_target')
107
+ end
108
+
109
+ it 'should store old symlink target' do
110
+ instance.update_current_symlink
70
111
 
71
- expect(instance.instance_variable_get("@run_block")).to be test_block
112
+ expect(instance.instance_variable_get(:@_old_current)).to_not be_nil
113
+ end
114
+
115
+ it 'should replace existing symlink' do
116
+ expect(instance).to receive(:remote_exec).with(/^ln .* #{instance.deploy_link}/)
117
+
118
+ instance.update_current_symlink
72
119
  end
73
120
  end
74
121
 
75
- context 'when not passed a block' do
122
+ context 'current_symlink does not exist yet' do
76
123
  before(:each) do
77
- @mock_ssh = double(:ssh, host: 'test_host1', exec: '', upload: true, download: true)
78
- allow(instance).to receive(:ssh).and_return(@mock_ssh)
124
+ expect(instance).to receive(:remote_exec).with(/readlink/).and_return('')
125
+ end
126
+
127
+ it 'should not store old symlink target' do
128
+ instance.update_current_symlink
79
129
 
80
- instance.instance_variable_set(:@run_block, Proc.new { |h| "block for #{h.host}" })
130
+ expect(instance.instance_variable_get(:@_old_current)).to be_nil
81
131
  end
82
132
 
83
- it 'should use hosts from --hosts if passed' do
84
- instance.instance_variable_set(:@_argv_hosts, [ 'argv_host1', 'argv_host2' ])
133
+ it 'should create symlink' do
134
+ expect(instance).to receive(:remote_exec).with(/^ln .* #{instance.deploy_link}/)
85
135
 
86
- output = capture(:stdout) do
87
- instance.run
88
- end
136
+ instance.update_current_symlink
137
+ end
138
+ end
139
+ end
140
+
141
+ describe '#revert_current_symlink' do
142
+ it 'should set link back to old target if old target is set' do
143
+ instance.instance_variable_set(:@_old_current, 'old_link_target')
144
+
145
+ expect(instance).to receive(:remote_exec).with(/^ln .* old_link_target/)
146
+
147
+ instance.revert_current_symlink
148
+ end
149
+
150
+ it 'should be a no-op if old target is not set' do
151
+ expect(instance).to_not receive(:remote_exec)
152
+
153
+ instance.revert_current_symlink
154
+ end
155
+ end
89
156
 
90
- expect(instance.hosts).to eq([ 'argv_host1', 'argv_host2' ])
91
- expect(output).to include('argv_host1', 'argv_host2')
92
- expect(output).to_not include('hf_host')
157
+ describe '#run' do
158
+ before(:each) do
159
+ instance.artifact = 'test_artifact.tar.gz'
160
+ instance.project = 'test_project'
161
+
162
+ @mock_ssh = double(:ssh, host: 'test_host1', exec: '', upload: true, download: true)
163
+ allow(instance).to receive(:ssh).and_return(@mock_ssh)
164
+ end
165
+
166
+ it 'should use hosts from --hosts if passed' do
167
+ instance.instance_variable_set(:@_argv_hosts, [ 'argv_host1', 'argv_host2' ])
168
+
169
+ output = capture(:stdout) do
170
+ instance.run
93
171
  end
94
172
 
95
- it 'should use hosts from Harrisonfile if --hosts not passed' do
96
- output = capture(:stdout) do
97
- instance.run
98
- end
173
+ expect(instance.hosts).to eq([ 'argv_host1', 'argv_host2' ])
174
+ expect(output).to include('argv_host1', 'argv_host2')
175
+ expect(output).to_not include('hf_host')
176
+ end
99
177
 
100
- expect(instance.hosts).to eq([ 'hf_host' ])
101
- expect(output).to include('hf_host')
178
+ it 'should use hosts from Harrisonfile if --hosts not passed' do
179
+ output = capture(:stdout) do
180
+ instance.run
102
181
  end
103
182
 
104
- it 'should require hosts to be set somehow' do
105
- instance.hosts = nil
183
+ expect(instance.hosts).to eq([ 'hf_host' ])
184
+ expect(output).to include('hf_host')
185
+ end
106
186
 
107
- output = capture(:stderr) do
108
- expect(lambda { instance.run }).to exit_with_code(1)
109
- end
187
+ it 'should require hosts to be set somehow' do
188
+ instance.hosts = nil
110
189
 
111
- expect(output).to include('must', 'specify', 'hosts')
190
+ output = capture(:stderr) do
191
+ expect(lambda { instance.run }).to exit_with_code(1)
112
192
  end
113
193
 
114
- it 'should invoke the previously stored block once for each host' do
115
- instance.hosts = [ 'host1', 'host2', 'host3' ]
194
+ expect(output).to include('must', 'specify', 'hosts')
195
+ end
116
196
 
117
- output = capture(:stdout) do
118
- expect { |b| instance.run(&b); instance.run }.to yield_control.exactly(3).times
119
- end
197
+ it 'should run the specified phases once for each host' do
198
+ instance.hosts = [ 'host1', 'host2', 'host3' ]
199
+
200
+ mock_phase = double(:phase, matches_context?: true)
201
+ expect(Harrison::Deploy::Phase).to receive(:new).with(:test).and_return(mock_phase)
202
+ instance.add_phase :test
203
+ instance.phases = [ :test ]
204
+
205
+ expect(mock_phase).to receive(:_run).exactly(3).times
120
206
 
121
- expect(output).to include('host1', 'host2', 'host3')
207
+ output = capture(:stdout) do
208
+ instance.run
122
209
  end
123
210
 
124
- it 'should clean up old releases if passed a --keep option' do
125
- instance.keep = 3
211
+ expect(output).to include('host1', 'host2', 'host3')
212
+ end
126
213
 
127
- expect(instance).to receive(:cleanup_deploys).with(3)
128
- expect(instance).to receive(:cleanup_releases)
214
+ context 'when a deployment phase fails on a host' do
215
+ before(:each) do
216
+ @upload = double(:phase, name: :upload, matches_context?: true, _run: true, _fail: true)
217
+ @extract = double(:phase, name: :extract, matches_context?: true, _run: true, _fail: true)
218
+ @link = double(:phase, name: :link, matches_context?: true)
129
219
 
130
- output = capture(:stdout) do
131
- instance.run
220
+ allow(@link).to receive(:_run).and_throw(:failure, true)
221
+
222
+ instance.instance_variable_set(:@_phases, {
223
+ upload: @upload,
224
+ extract: @extract,
225
+ link: @link,
226
+ })
227
+
228
+ instance.phases = [ :upload, :extract, :link ]
229
+
230
+ instance.hosts = [ 'host1', 'host2', 'host3' ]
231
+ end
232
+
233
+ it "should invoke on_fail block for completed phases on each host" do
234
+ expect(@extract).to receive(:_fail).exactly(3).times.ordered
235
+ expect(@upload).to receive(:_fail).exactly(3).times.ordered
236
+
237
+ capture([ :stdout, :stderr ]) do
238
+ expect(lambda { instance.run }).to exit_with_code(1)
132
239
  end
133
240
  end
134
241
 
135
- context 'when deploying from a remote artifact source' do
136
- before(:each) do
137
- instance.artifact = 'test_user@test_host1:/tmp/test_artifact.tar.gz'
242
+ it "should invoke on_fail block on each host in reverse order" do
243
+ expect(@extract).to receive(:_fail) { |context| expect(context.host).to eq('host3') }.ordered
244
+ expect(@extract).to receive(:_fail) { |context| expect(context.host).to eq('host2') }.ordered
245
+ expect(@extract).to receive(:_fail) { |context| expect(context.host).to eq('host1') }.ordered
246
+
247
+ capture([ :stdout, :stderr ]) do
248
+ expect(lambda { instance.run }).to exit_with_code(1)
138
249
  end
250
+ end
251
+ end
139
252
 
140
- it 'should invoke scp on the remote host' do
141
- allow(instance).to receive(:remote_exec).and_return('')
142
- expect(instance).to receive(:remote_exec).with(/scp test_user@test_host1:\/tmp\/test_artifact.tar.gz/).and_return('')
253
+ context 'when invoked via rollback' do
254
+ before(:each) do
255
+ instance.rollback = true
143
256
 
144
- output = capture(:stdout) do
145
- instance.run
146
- end
257
+ instance.project = 'test_project'
147
258
 
148
- expect(output).to include('deployed', 'test_user', 'test_host1', '/tmp/test_artifact.tar.gz')
259
+ @mock_ssh = double(:ssh, host: 'test_host1', exec: '', upload: true, download: true)
260
+ allow(instance).to receive(:ssh).and_return(@mock_ssh)
261
+ end
262
+
263
+ it 'should find the release of the previous deploy' do
264
+ expect(instance).to receive(:deploys).and_return([ 'deploy_1', 'deploy_2', 'deploy_3', 'deploy_4', 'deploy_5' ])
265
+ expect(@mock_ssh).to receive(:exec).with(/readlink .* deploy_4/)
266
+
267
+ capture(:stdout) do
268
+ instance.run
149
269
  end
270
+ end
150
271
 
151
- it 'should not invoke Harrison::SSH.upload' do
152
- expect(@mock_ssh).not_to receive(:upload)
272
+ it 'should not run :upload, :extract, or :cleanup phases' do
273
+ expect(instance).to receive(:deploys).and_return([ 'deploy_1', 'deploy_2', 'deploy_3', 'deploy_4', 'deploy_5' ])
274
+ expect(@mock_ssh).to receive(:exec).with(/readlink .* deploy_4/).and_return('old_release')
153
275
 
154
- output = capture(:stdout) do
155
- instance.run
156
- end
276
+ disabled_phase = double(:phase)
277
+ expect(disabled_phase).to_not receive(:_run)
278
+
279
+ enabled_phase = double(:phase, matches_context?: true)
280
+ expect(enabled_phase).to receive(:_run)
157
281
 
158
- expect(output).to include('deployed', 'test_user', 'test_host1', '/tmp/test_artifact.tar.gz')
282
+ instance.instance_variable_set(:@_phases, {
283
+ upload: disabled_phase,
284
+ extract: disabled_phase,
285
+ link: enabled_phase,
286
+ cleanup: disabled_phase,
287
+ })
288
+
289
+ capture(:stdout) do
290
+ instance.run
159
291
  end
160
292
  end
161
293
  end
@@ -220,6 +352,20 @@ describe Harrison::Deploy do
220
352
  end
221
353
 
222
354
  describe 'protected methods' do
355
+ describe '#add_default_phases' do
356
+ it 'should add each default phase' do
357
+ # Trigger constructor invocations so we can only count new invocation below.
358
+ instance
359
+
360
+ expect(Harrison::Deploy::Phase).to receive(:new).with(:upload)
361
+ expect(Harrison::Deploy::Phase).to receive(:new).with(:extract)
362
+ expect(Harrison::Deploy::Phase).to receive(:new).with(:link)
363
+ expect(Harrison::Deploy::Phase).to receive(:new).with(:cleanup)
364
+
365
+ instance.send(:add_default_phases)
366
+ end
367
+ end
368
+
223
369
  describe '#ssh' do
224
370
  it 'should open a new SSH connection to self.host' do
225
371
  mock_ssh = double(:ssh)
@@ -316,4 +462,147 @@ describe Harrison::Deploy do
316
462
  end
317
463
  end
318
464
  end
465
+
466
+ describe 'default phases' do
467
+ before(:each) do
468
+ instance.artifact = '/tmp/test_artifact.tar.gz'
469
+
470
+ @mock_ssh = double(:ssh, host: 'test_host1', exec: '', upload: true, download: true)
471
+ allow(instance).to receive(:ssh).and_return(@mock_ssh)
472
+ end
473
+
474
+ describe 'upload' do
475
+ before(:each) do
476
+ @phase = instance.instance_variable_get(:@_phases)[:upload]
477
+ end
478
+
479
+ describe 'on_run' do
480
+ context 'when deploying from a local artifact' do
481
+ it 'should invoke Harrison::SSH.upload' do
482
+ expect(@mock_ssh).to receive(:upload).with(/test_artifact\.tar\.gz/, anything)
483
+
484
+ capture(:stdout) do
485
+ @phase._run(instance)
486
+ end
487
+ end
488
+ end
489
+
490
+ context 'when deploying from a remote artifact' do
491
+ before(:each) do
492
+ instance.artifact = 'test_user@test_host1:/tmp/test_artifact.tar.gz'
493
+ end
494
+
495
+ it 'should invoke scp on the remote host' do
496
+ allow(instance).to receive(:remote_exec).and_return('')
497
+ expect(instance).to receive(:remote_exec).with(/scp test_user@test_host1:\/tmp\/test_artifact.tar.gz/).and_return('')
498
+
499
+ capture(:stdout) do
500
+ @phase._run(instance)
501
+ end
502
+ end
503
+
504
+ it 'should not invoke Harrison::SSH.upload' do
505
+ expect(@mock_ssh).not_to receive(:upload)
506
+
507
+ capture(:stdout) do
508
+ @phase._run(instance)
509
+ end
510
+ end
511
+ end
512
+ end
513
+
514
+ describe 'on_fail' do
515
+ it 'should remove uploaded artifact' do
516
+ expect(instance).to receive(:remote_exec).with(/rm.*test_artifact\.tar\.gz/)
517
+
518
+ capture(:stdout) do
519
+ @phase._fail(instance)
520
+ end
521
+ end
522
+ end
523
+ end
524
+
525
+ describe 'extract' do
526
+ before(:each) do
527
+ @phase = instance.instance_variable_get(:@_phases)[:extract]
528
+ end
529
+
530
+ describe 'on_run' do
531
+ it 'should untar the artifact and then remove it' do
532
+ allow(instance).to receive(:remote_exec)
533
+ expect(instance).to receive(:remote_exec).with(/tar.*test_artifact\.tar\.gz/).ordered
534
+ expect(instance).to receive(:remote_exec).with(/rm.*test_artifact\.tar\.gz/).ordered
535
+
536
+ capture(:stdout) do
537
+ @phase._run(instance)
538
+ end
539
+ end
540
+ end
541
+
542
+ describe 'on_fail' do
543
+ it 'should remove the extracted release' do
544
+ expect(instance).to receive(:remote_exec).with(/rm.*#{instance.release_dir}/)
545
+
546
+ capture(:stdout) do
547
+ @phase._fail(instance)
548
+ end
549
+ end
550
+ end
551
+ end
552
+
553
+ describe 'link' do
554
+ before(:each) do
555
+ @phase = instance.instance_variable_get(:@_phases)[:link]
556
+ end
557
+
558
+ describe 'on_run' do
559
+ it 'should create a new deploy link' do
560
+ expect(instance).to receive(:remote_exec).with(/ln.*#{instance.release_dir}.*#{instance.deploy_link}/)
561
+
562
+ capture(:stdout) do
563
+ @phase._run(instance)
564
+ end
565
+ end
566
+ end
567
+
568
+ describe 'on_fail' do
569
+ it 'should remove deploy link' do
570
+ expect(instance).to receive(:remote_exec).with(/rm.*#{instance.deploy_link}/)
571
+
572
+ capture(:stdout) do
573
+ @phase._fail(instance)
574
+ end
575
+ end
576
+ end
577
+ end
578
+
579
+ describe 'cleanup' do
580
+ before(:each) do
581
+ @phase = instance.instance_variable_get(:@_phases)[:cleanup]
582
+ end
583
+
584
+ describe 'on_run' do
585
+ it 'should clean up old releases if passed a --keep option' do
586
+ instance.keep = 3
587
+
588
+ expect(instance).to receive(:cleanup_deploys).with(3)
589
+ expect(instance).to receive(:cleanup_releases)
590
+
591
+ capture(:stdout) do
592
+ @phase._run(instance)
593
+ end
594
+ end
595
+ end
596
+
597
+ describe 'on_fail' do
598
+ it 'should not do anything' do
599
+ expect(instance).to_not receive(:remote_exec)
600
+
601
+ capture(:stdout) do
602
+ @phase._fail(instance)
603
+ end
604
+ end
605
+ end
606
+ end
607
+ end
319
608
  end