hr_deploy 0.0.2 → 0.0.6

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.
@@ -1,387 +1,340 @@
1
+ require 'hr_deploy/config_handler'
2
+ require 'hr_deploy/color.rb'
3
+ require 'hr_deploy/tasks/check_network_task'
4
+ require 'hr_deploy/tasks/check_heroku_task'
5
+ require 'hr_deploy/tasks/enable_pinger_task'
6
+ require 'hr_deploy/tasks/disable_pinger_task'
7
+ require 'hr_deploy/tasks/enable_maintenance_task'
8
+ require 'hr_deploy/tasks/disable_maintenance_task'
9
+ require 'hr_deploy/tasks/backup_db_task'
10
+ require 'hr_deploy/tasks/clean_old_assets_task.rb'
11
+ require 'hr_deploy/tasks/clean_new_assets_task.rb'
12
+ require 'hr_deploy/tasks/precompile_assets_task.rb'
13
+ require 'hr_deploy/tasks/push_code_task.rb'
14
+ require 'hr_deploy/tasks/migrate_db_task.rb'
15
+ require 'hr_deploy/tasks/restart_task.rb'
16
+
1
17
  module HR_Deploy
2
18
 
3
19
  class Deployer
4
20
 
5
- DEBUG = false
6
-
7
- attr_reader :target
8
- attr_reader :pinging_interaction
9
- attr_reader :perform_db_backup
10
- attr_reader :confirm
11
-
12
- def initialize(target, confirm)
13
- @target = target
14
- @pinging_interaction = target.requires_pinging_interaction?
15
- @perform_db_backup = target.backup_db?
16
- @confirm = confirm
21
+ def initialize
22
+ @target = nil
23
+ @confirm = nil
24
+ @dry_run = nil
25
+ @start_time = nil
26
+ @tasks_to_run = []
27
+ @finished_tasks = []
17
28
  end
18
29
 
19
- def deploy
20
- confirm_deploy
21
-
22
- start_time = Time.now
23
- puts "#{target.description} deploy started at #{Time.now.strftime('%H:%M:%S')}"
24
- puts
30
+ def deploy(target_name, confirm, dry_run, db_change)
25
31
 
26
- check_connectivity
27
- check_heroku_status
32
+ self.confirm = confirm
33
+ self.dry_run = dry_run
34
+ self.db_change = db_change
28
35
 
29
- if pinging_interaction
30
- disable_pinging
31
- continue?(:disabling_pinger) { enable_maintenance }
32
- else
33
- enable_maintenance
36
+ unless HR_Deploy::ConfigHandler.config_exists?
37
+ puts "No deployment config is present. Run 'hr_deploy generate' to generate a config example"
38
+ exit 1
34
39
  end
35
40
 
36
- if perform_db_backup
37
- continue?(:enabling_maintenance) { backup_db }
38
- continue?(:backing_up_db) { push_code }
39
- else
40
- continue?(:enabling_maintenance) { push_code }
41
- end
41
+ config_handler = HR_Deploy::ConfigHandler.new
42
42
 
43
- continue?(:pushing_code) { migrate_db }
44
- continue?(:migrating_db) { restart }
43
+ # This will set up targets or error in 'config_handler'
44
+ config_handler.attempt_to_load_targets
45
45
 
46
- if pinging_interaction
47
- continue?(:restarting) { enable_pinging }
48
- continue?(:enabling_pinger) { disable_maintenance }
49
- else
50
- continue?(:restarting) { disable_maintenance }
51
- end
46
+ if config_handler.error
47
+ puts config_handler.error
48
+ exit 1
52
49
 
53
- continue?(:disabling_maintenance) do
54
- puts 'Waiting 10 seconds for application to go live...'
55
- sleep 10
56
- puts 'Opening application...'
57
- execute_system_command("heroku open --remote #{target.name}")
58
- puts
50
+ else
51
+ if target_name == nil
59
52
 
60
- puts "Deploy completed in #{(Time.now - start_time).round} seconds"
61
- end
62
- end
53
+ # Deploy to default target
54
+ default_targets = config_handler.targets.find_all { |target| target.default? }
55
+ if default_targets.count == 0
63
56
 
64
- private
57
+ puts 'No default target specified. Edit config to specify one'
58
+ exit 1
59
+ elsif default_targets.count > 1
65
60
 
66
- def confirm_deploy
67
- answered = false
68
-
69
- until answered
70
- print "Deploy to #{target.description}? (Yes / No) > "
71
- answer = $stdin.gets.chomp
61
+ puts 'Multiple default targets specified. Edit config to specify only one'
62
+ exit 1
63
+ else
72
64
 
73
- if answer == 'Yes' || answer == 'yes'
74
- answered = true
75
- puts
76
- elsif answer == 'No' || answer == 'no'
77
- abort_deploy(nil)
65
+ self.target = default_targets.first
66
+ deploy_to_target
67
+ end
78
68
  else
79
- puts 'Unknown answer'
80
- puts
69
+
70
+ target = config_handler.targets.find { |target| target.name == target_name }
71
+ if target
72
+ self.target = target
73
+ deploy_to_target
74
+ else
75
+ puts "No such target: #{target_name}"
76
+ exit 1
77
+ end
81
78
  end
82
79
  end
83
80
  end
84
81
 
85
- def abort_deploy(message, stage = nil)
82
+ private
86
83
 
87
- if stage
88
- puts 'Notes:'
89
- puts
84
+ attr_accessor :target
85
+ attr_accessor :confirm
86
+ attr_accessor :dry_run
87
+ attr_accessor :db_change
88
+ attr_accessor :start_time
89
+ attr_reader :tasks_to_run
90
+ attr_reader :finished_tasks
90
91
 
91
- case stage
92
- when :disabling_pinger
93
- puts '1) Application state did not change'
94
- puts '2) Availability pinging could not be disabled'
92
+ def deploy_to_target
93
+ tools = ['git', 'heroku']
94
+ tools << 'curl' if target.requires_curl?
95
+ tools << 'rake' if target.requires_rake?
95
96
 
96
- when :enabling_maintenance
97
- puts '1) Application state did not change'
98
- puts
99
- puts '2) Maintenance mode could not be enabled'
100
- if pinging_interaction
101
- puts
102
- puts '3) Availability pinging is disabled'
103
- puts "* To enable: curl #{target.enable_pinger_url}"
104
- end
97
+ # Will print out an error and halt app if a required tool is unavailable
98
+ assert_tools_available(tools)
105
99
 
106
- when :backing_up_db
107
- puts '1) Application state did not change'
108
- puts
109
- puts '2) Maintenance mode is enabled'
110
- puts "* To disable: heroku maintenance:off --remote #{target.name}"
111
- puts
112
- puts '3) Database backup could not be created'
113
- puts
114
- if pinging_interaction
115
- puts '4) Availability pinging is disabled'
116
- puts "* To enable: curl #{target.enable_pinger_url}"
117
- end
100
+ if dry_run
101
+ puts 'Dry run started'
118
102
 
119
- when :pushing_code
120
- puts '1) Application state is unknown'
121
- puts
122
- puts '2) Maintenance mode is enabled'
123
- puts "* To disable: heroku maintenance:off --remote #{target.name}"
124
- puts
125
- puts '3) Application code was not pushed properly'
126
- puts "* Check releases with 'heroku releases --remote #{target.name}' and rollback with " +
127
- "'heroku rollback (version) --remote #{target.name}' if needed"
128
- if pinging_interaction
129
- puts
130
- puts '4) Availability pinging is disabled'
131
- puts "* To enable: curl #{target.enable_pinger_url}"
132
- end
103
+ elsif confirm_deploy
104
+ puts
105
+ self.start_time = Time.now
106
+ puts "Deploy started at #{start_time.strftime('%H:%M:%S')}"
133
107
 
134
- when :migrating_db
135
- puts '1) Application state is unknown'
136
- puts
137
- puts '2) Maintenance mode is enabled'
138
- puts "* To disable: heroku maintenance:off --remote #{target.name}"
139
- puts
140
- puts '3) Code was pushed but database was not migrated properly'
141
- puts "* Check releases with 'heroku releases --remote #{target.name}' and rollback " +
142
- "with 'heroku rollback (version) --remote #{target.name}' if needed"
143
- if perform_db_backup
144
- puts "* List database backups with 'heroku pgbackups --remote #{target.name}' and restore " +
145
- "with 'heroku pgbackups:restore (database name) (version) --remote #{target.name}' if needed"
146
- end
147
- if pinging_interaction
148
- puts
149
- puts '4) Availability pinging is disabled'
150
- puts "* To enable: curl #{target.enable_pinger_url}"
151
- end
108
+ else
109
+ print_fail 'Deploy aborted'
110
+ exit 1
111
+ end
152
112
 
153
- when :restarting
154
- puts '1) Application state is unknown'
155
- puts
156
- puts '2) Maintenance mode is enabled'
157
- puts "* To disable: heroku maintenance:off --remote #{target.name}"
158
- puts
159
- puts '3) Database was migrated but could not restart'
160
- puts "* Try to restart manually with 'heroku restart --remote #{target.name}'"
161
- puts "* Check releases with 'heroku releases --remote #{target.name}' and rollback " +
162
- "with 'heroku rollback (version) --remote #{target.name}' if needed"
163
- if perform_db_backup
164
- puts "* List database backups with 'heroku pgbackups --remote #{target.name}' and restore " +
165
- "with 'heroku pgbackups:restore (database name) (version) --remote #{target.name}' if needed"
166
- end
167
- if pinging_interaction
168
- puts
169
- puts '4) Availability pinging is disabled'
170
- puts "* To enable: curl #{target.enable_pinger_url}"
171
- end
113
+ puts
172
114
 
173
- when :enabling_pinger
174
- puts '1) Application has been deployed'
175
- puts
176
- puts '2) Maintenance mode is enabled'
177
- puts "* To disable: heroku maintenance:off --remote #{target.name}"
178
- if pinging_interaction
179
- puts
180
- puts '3) Availability pinging could not be enabled'
181
- puts "* To enable: curl #{target.enable_pinger_url}"
182
- end
115
+ load_tasks
116
+ run_tasks
117
+ end
183
118
 
184
- when :disabling_maintenance
185
- puts '1) Application has been deployed'
186
- puts
187
- puts '2) Maintenance mode could not be disabled'
188
- puts "* To disable: heroku maintenance:off --remote #{target.name}"
119
+ def assert_tools_available(tools)
189
120
 
190
- else
191
- raise 'Unknown stage'
121
+ def command_available?(cmd)
122
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
123
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
124
+ exts.each do |ext|
125
+ exe = File.join(path, "#{cmd}#{ext}")
126
+ return true if File.executable? exe
127
+ end
192
128
  end
193
-
194
- puts
129
+ false
195
130
  end
196
131
 
197
- if message
198
- puts "Deploy aborted with message: #{message}"
199
- else
200
- puts 'Deploy aborted'
132
+ all_tools_available = true
133
+
134
+ tools.each do |tool|
135
+ unless command_available?(tool)
136
+ puts "You don't have access to '#{tool}' command"
137
+ all_tools_available = false
138
+ end
201
139
  end
202
140
 
203
- exit
141
+ exit 1 unless all_tools_available
204
142
  end
205
143
 
206
- def continue?(stage)
207
-
208
- if confirm
209
- # User wants confirmations
210
-
211
- answered = false
212
- until answered
213
-
214
- case stage
215
- when :disabling_pinger
216
- print 'Pinger disabled? (Yes / No) > '
217
- when :enabling_maintenance
218
- print 'Maintenance mode enabled? (Yes / No) > '
219
- when :backing_up_db
220
- print 'Database backed up? (Yes / No) > '
221
- when :pushing_code
222
- print 'Code pushed successfully? (Yes / No) > '
223
- when :migrating_db
224
- print 'Database migrated? (Yes / No) > '
225
- when :restarting
226
- print 'Restarted? (Yes / No) > '
227
- when :enabling_pinger
228
- print 'Pinger enabled? (Yes / No) > '
229
- when :disabling_maintenance
230
- print 'Maintenance mode disabled? (Yes / No) > '
231
- else
232
- raise 'Unknown stage'
233
- end
234
-
235
- answer = $stdin.gets.chomp
144
+ def confirm_deploy
236
145
 
237
- if answer == 'Yes' || answer == 'yes'
238
- answered = true
239
- puts
240
- yield
241
- elsif answer == 'No' || answer == 'no'
242
- puts
243
- abort_deploy(nil, stage)
244
- else
245
- puts 'Unknown answer'
246
- puts
247
- end
146
+ loop do
147
+ if db_change
148
+ print "Deploy to #{target.description}? (changing DB structure) [Yes / No] > "
149
+ else
150
+ print "Deploy to #{target.description}? (not changing DB structure) [Yes / No] > "
248
151
  end
152
+ answer = $stdin.gets.chomp.downcase
249
153
 
250
- else
251
- # User does not want confirmations, continue
252
- yield
253
- end
254
- end
154
+ if answer == 'yes'
155
+ return true
255
156
 
256
- def execute_system_command(cmd)
257
- if DEBUG
258
- puts "Executing command: #{cmd}"
259
- else
260
- system(cmd)
157
+ elsif answer == 'no'
158
+ return false
159
+
160
+ else
161
+ puts 'Unknown answer'
162
+ puts
163
+ end
261
164
  end
262
165
  end
263
166
 
264
- def check_connectivity
265
- require 'open-uri'
167
+ def load_tasks
168
+ tasks_to_run << HR_Deploy::CheckNetworkTask.new(dry_run: dry_run)
169
+ tasks_to_run << HR_Deploy::CheckHerokuTask.new(dry_run: dry_run)
266
170
 
267
- puts 'Checking Internet connection...'
171
+ if target.pinging_interaction?
172
+ tasks_to_run << HR_Deploy::DisablePingerTask.new(target: target, confirm: confirm, dry_run: dry_run)
173
+ end
268
174
 
269
- begin
270
- open('http://www.google.com')
271
- rescue
272
- puts 'Not connected to the Internet'
273
- puts
274
- abort_deploy('Not connected to the Internet')
275
- else
276
- puts 'Internet connection is fine'
277
- puts
175
+ if target.maintenance_interaction?
176
+ tasks_to_run << HR_Deploy::EnableMaintenanceTask.new(target: target, confirm: confirm, dry_run: dry_run)
278
177
  end
279
- end
280
178
 
281
- def check_heroku_status
179
+ if target.backup_db? && db_change
180
+ tasks_to_run << HR_Deploy::BackupDBTask.new(target: target, confirm: confirm, dry_run: dry_run)
181
+ end
282
182
 
283
- require 'uri'
284
- require 'net/http'
285
- require 'net/https'
286
- require 'json'
183
+ if target.s3_asset_sync?
184
+ tasks_to_run << HR_Deploy::CleanOldAssetsTask.new(confirm: confirm, dry_run: dry_run)
185
+ tasks_to_run << HR_Deploy::PrecompileAssetsTask.new(confirm: confirm, dry_run: dry_run)
186
+ end
287
187
 
288
- puts 'Checking Heroku status...'
188
+ tasks_to_run << HR_Deploy::PushCodeTask.new(target: target, confirm: confirm, dry_run: dry_run)
289
189
 
290
- uri = URI.parse('https://status.heroku.com/api/v3/current-status')
190
+ if db_change
191
+ tasks_to_run << HR_Deploy::MigrateDBTask.new(target: target, confirm: confirm, dry_run: dry_run)
192
+ tasks_to_run << HR_Deploy::RestartTask.new(target: target, confirm: confirm, dry_run: dry_run)
193
+ end
291
194
 
292
- http = Net::HTTP.new(uri.host, uri.port)
293
- http.use_ssl = true
195
+ if target.maintenance_interaction?
196
+ tasks_to_run << HR_Deploy::DisableMaintenanceTask.new(target: target, confirm: confirm, dry_run: dry_run)
197
+ end
294
198
 
295
- request = Net::HTTP::Get.new(uri.request_uri)
199
+ if target.pinging_interaction?
200
+ tasks_to_run << HR_Deploy::EnablePingerTask.new(target: target, confirm: confirm, dry_run: dry_run)
201
+ end
296
202
 
297
- begin
298
- response = http.request(request)
203
+ if target.s3_asset_sync?
204
+ tasks_to_run << HR_Deploy::CleanNewAssetsTask.new(confirm: confirm, dry_run: dry_run)
205
+ end
206
+ end
299
207
 
300
- rescue
301
- puts 'Could not fetch Heroku status'
302
- puts
303
- abort_deploy('Could not fetch Heroku status')
208
+ def run_tasks
209
+ tasks_to_run.each do |task|
304
210
 
305
- else
306
- if response.code == '200'
211
+ task.run
307
212
 
308
- status = JSON.parse(response.body)
309
- status = status['status'] if status
213
+ if task.successful?
214
+ finished_tasks << task
215
+ puts
216
+ else
217
+ puts
218
+ abort_deploy(task)
219
+ end
220
+ end
310
221
 
311
- if status && status['Production'] && status['Development']
222
+ # Successfully finished running all tasks
223
+ deploy_succeeded
224
+ end
312
225
 
313
- production_ok = status['Production'] == 'green'
314
- development_ok = status['Development'] == 'green'
226
+ def abort_deploy(failed_task)
315
227
 
316
- if production_ok && development_ok
317
- puts 'Heroku is operating correctly'
318
- puts
319
- else
320
- puts 'Heroku is having problems'
321
- puts
322
- abort_deploy('Heroku is having problems')
323
- end
324
- else
228
+ puts '-' * 50
229
+ puts
230
+ print_app_status "* #{application_state(failed_task)}"
231
+ puts
232
+ print_fail "* #{failed_task.error}"
233
+ puts
325
234
 
326
- puts 'Could not parse Heroku status'
327
- puts
328
- abort_deploy('Could not parse Heroku status')
329
- end
235
+ # Logic to remove tasks for which it doesn't make sense to print reversal instruction
236
+ filter_finished_tasks
330
237
 
331
- else
332
- puts 'Could not fetch Heroku status'
238
+ finished_tasks.each do |task|
239
+ if task.reversal_instruction
240
+ puts "* #{task.reversal_instruction}"
333
241
  puts
334
- abort_deploy('Could not fetch Heroku status')
335
242
  end
336
243
  end
337
- end
338
244
 
339
- def disable_pinging
340
- puts 'Disabling availability pinger...'
341
- execute_system_command("curl #{target.disable_pinger_url}")
342
- puts
245
+ print_fail 'Deploy aborted'
246
+ exit 1
343
247
  end
344
248
 
345
- def enable_maintenance
346
- puts 'Enabling maintenance mode...'
347
- execute_system_command("heroku maintenance:on --remote #{target.name}")
348
- puts
349
- end
249
+ def deploy_succeeded
250
+ if dry_run
251
+ print_deploy_successful 'Dry run complete'
252
+ else
253
+ print_deploy_successful "Deploy complete in #{(Time.now - start_time).round} seconds"
254
+ end
350
255
 
351
- def backup_db
352
- puts 'Backing up database...'
353
- execute_system_command("heroku pgbackups:capture --expire --remote #{target.name}")
354
- puts
256
+ # Execution is finished
257
+ exit 0
355
258
  end
356
259
 
357
- def push_code
358
- puts 'Pushing code...'
359
- execute_system_command("git push #{target.name} master")
360
- puts
260
+ def application_state(failed_task)
261
+
262
+ did_not_change = 'Application state did not change'
263
+ unknown = 'Application state is unknown'
264
+ deployed = 'Application has been deployed'
265
+
266
+ case failed_task
267
+ when HR_Deploy::CheckNetworkTask
268
+ did_not_change
269
+ when HR_Deploy::CheckHerokuTask
270
+ did_not_change
271
+ when HR_Deploy::DisablePingerTask
272
+ did_not_change
273
+ when HR_Deploy::EnableMaintenanceTask
274
+ did_not_change
275
+ when HR_Deploy::BackupDBTask
276
+ did_not_change
277
+ when HR_Deploy::CleanOldAssetsTask
278
+ did_not_change
279
+ when HR_Deploy::PrecompileAssetsTask
280
+ did_not_change
281
+ when HR_Deploy::PushCodeTask
282
+ did_not_change
283
+ when HR_Deploy::MigrateDBTask
284
+ unknown
285
+ when HR_Deploy::RestartTask
286
+ unknown
287
+ when HR_Deploy::DisableMaintenanceTask
288
+ deployed
289
+ when HR_Deploy::EnablePingerTask
290
+ deployed
291
+ when HR_Deploy::CleanNewAssetsTask
292
+ deployed
293
+ else
294
+ raise "Can't determine application state: failed task type is unknown"
295
+ end
361
296
  end
362
297
 
363
- def migrate_db
364
- puts 'Migrating database...'
365
- execute_system_command("heroku run rake db:migrate --remote #{target.name}")
366
- puts
298
+ def filter_finished_tasks
299
+
300
+ # Doesn't make sense to show instruction to enable pinger if it has been enabled
301
+ enable_pinger = finished_tasks.find { |task| task.class == HR_Deploy::EnablePingerTask }
302
+ disable_pinger = finished_tasks.find { |task| task.class == HR_Deploy::DisablePingerTask }
303
+ if enable_pinger && disable_pinger
304
+ finished_tasks.delete(enable_pinger)
305
+ finished_tasks.delete(disable_pinger)
306
+ end
307
+
308
+ # Doesn't make sense to show instruction to disable maintenance if it has already been disabled
309
+ enable_maintenance = finished_tasks.find { |task| task.class == HR_Deploy::EnableMaintenanceTask }
310
+ disable_maintenance = finished_tasks.find { |task| task.class == HR_Deploy::DisableMaintenanceTask }
311
+ if enable_maintenance && disable_maintenance
312
+ finished_tasks.delete(enable_maintenance)
313
+ finished_tasks.delete(disable_maintenance)
314
+ end
315
+
316
+ # Doesn't make sense to show instruction to reverse pushing code and db migration
317
+ # if has already pushed, migrated and restarted (deploy itself is complete)
318
+ if finished_tasks.find { |task| task.class == HR_Deploy::RestartTask }
319
+ push_code = finished_tasks.find { |task| task.class == HR_Deploy::PushCodeTask }
320
+ migrate_db = finished_tasks.find { |task| task.class == HR_Deploy::MigrateDBTask }
321
+ restart = finished_tasks.find { |task| task.class == HR_Deploy::RestartTask }
322
+ finished_tasks.delete(push_code)
323
+ finished_tasks.delete(migrate_db)
324
+ finished_tasks.delete(restart)
325
+ end
367
326
  end
368
327
 
369
- def restart
370
- puts 'Restarting...'
371
- execute_system_command("heroku restart --remote #{target.name}")
372
- puts
328
+ def print_fail(msg)
329
+ puts msg.red
373
330
  end
374
331
 
375
- def enable_pinging
376
- puts 'Enabling availability pinger...'
377
- execute_system_command("curl #{target.enable_pinger_url}")
378
- puts
332
+ def print_deploy_successful(msg)
333
+ puts msg.green
379
334
  end
380
335
 
381
- def disable_maintenance
382
- puts 'Disabling maintenance mode...'
383
- execute_system_command("heroku maintenance:off --remote #{target.name}")
384
- puts
336
+ def print_app_status(msg)
337
+ puts msg.yellow
385
338
  end
386
339
  end
387
340
  end