harrison 0.2.0 → 0.3.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.
@@ -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