hu 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4d3e7d5397ca1407be4e8e0a651d65e10fb6f733
4
- data.tar.gz: 7c1b1be7cba228576e3403d8259ec654b4aa8861
3
+ metadata.gz: 175e30869a5caaf6629de09a9b92aa270aa726a5
4
+ data.tar.gz: 2028694a799523666fcc690f6652899bc71e02d3
5
5
  SHA512:
6
- metadata.gz: ffafe29f5abba66540fee1a76b27f01b3b8a99ce0b03b8aa32ad35f1bacf854279eed320618a787dba9176b1715d7771b472da628f4afda527c4b2acf8a75f30
7
- data.tar.gz: ee98357ee12c80f4ab4920647b10be3891e4e7a352f0ea0bc78a83b4970a2571e74c85cdabffc078274175f77fd9950825c128bac9182d83174ef79cb660464d
6
+ metadata.gz: 62c265376e687408df35e6840aacc6b6971b2fa4075ea8eb04fb7bbaa4afb7759a9c5876d49202314b8dfc8357b3f6d2cbc69ff97320cc2f33705d4e34d579d7
7
+ data.tar.gz: aa2746b42d19567e25e033a981ad002591c2316b2821f28a964b08ac6fcb468ad04a7fd7ae095c6b4da8a3d74be929d807ace118e4c50517bb6944abf6547db3
data/hu.gemspec CHANGED
@@ -17,12 +17,24 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
+ spec.required_ruby_version = '>= 2.3.0'
20
21
 
21
22
  spec.add_development_dependency "bundler", "~> 1.5"
22
23
  spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "bump"
23
25
 
24
- spec.add_dependency "optix"
25
- spec.add_dependency "platform-api"
26
+ spec.add_dependency "optix", "~> 1.2.4"
27
+ spec.add_dependency "blackbox", "~> 3.1.4"
28
+ spec.add_dependency "platform-api", "~> 0.7.0"
26
29
  spec.add_dependency "powerbar", ">= 1.0.16"
27
- spec.add_dependency "hashdiff"
30
+ spec.add_dependency "hashdiff", "~> 0.3.0"
31
+ spec.add_dependency "version_sorter", "~> 2.0.0"
32
+ spec.add_dependency "versionomy", "~> 0.5.0"
33
+ spec.add_dependency "tty-prompt"
34
+ spec.add_dependency "tty-spinner"
35
+ spec.add_dependency "tty-table"
36
+ spec.add_dependency "rainbow"
37
+ spec.add_dependency "netrc"
38
+ spec.add_dependency "chronic_duration"
39
+ spec.add_dependency "thread_safe"
28
40
  end
@@ -2,30 +2,22 @@ require 'hu/version'
2
2
  require 'optix'
3
3
  require 'powerbar'
4
4
  require 'yaml'
5
+ require 'netrc'
5
6
  require 'platform-api'
6
7
 
8
+ require 'hu/common'
7
9
  require 'hu/collab'
10
+ require 'hu/deploy'
8
11
 
9
12
  module Hu
10
13
  class Cli < Optix::Cli
11
- API_TOKEN = ENV['HEROKU_API_TOKEN']
12
14
  Optix::command do
13
15
  text "Hu v#{Hu::VERSION} - Heroku Utility"
14
- if API_TOKEN.nil?
15
- text ""
16
- text "\e[1mWARNING: Environment variable 'HEROKU_API_TOKEN' must be set.\e[0m"
17
- end
18
- opt :quiet, "Don't show progress bar", :default => false
16
+ opt :quiet, "Quiet mode (no progress output)", :default => false
19
17
  opt :version, "Print version and exit", :short => :none
20
18
  trigger :version do
21
19
  puts "Hu v#{Hu::VERSION}"
22
20
  end
23
- filter do
24
- if API_TOKEN.nil?
25
- STDERR.puts "\e[0;31;1mERROR: Environment variable 'HEROKU_API_TOKEN' must be set.\e[0m"
26
- exit 1
27
- end
28
- end
29
21
  filter do |cmd, opts, argv|
30
22
  $quiet = opts[:quiet]
31
23
  $quiet = true unless STDOUT.isatty
@@ -2,6 +2,7 @@ require 'powerbar'
2
2
  require 'yaml'
3
3
  require 'hashdiff'
4
4
  require 'set'
5
+ require 'netrc'
5
6
  require 'platform-api'
6
7
 
7
8
  module Hu
@@ -28,6 +29,16 @@ module Hu
28
29
  text ""
29
30
  text "WARNING: If you remove yourself from an application"
30
31
  text " then hu won't be able to see it anymore."
32
+ if Hu::API_TOKEN.nil?
33
+ text ""
34
+ text "\e[1mWARNING: Environment variable 'HEROKU_API_KEY' must be set.\e[0m"
35
+ end
36
+ filter do
37
+ if Hu::API_TOKEN.nil?
38
+ STDERR.puts "\e[0;31;1mERROR: Environment variable 'HEROKU_API_KEY' must be set.\e[0m"
39
+ exit 1
40
+ end
41
+ end
31
42
  def collab; end
32
43
 
33
44
  OP_COLORS = {
@@ -66,7 +77,7 @@ module Hu
66
77
  desc "Print current mapping to stdout"
67
78
  text "Print current mapping to stdout"
68
79
  opt :format, "yaml|json", :default => 'yaml'
69
- parent "collab", "Application collaborators"
80
+ parent "collab", "Manage application collaborators"
70
81
  def export(cmd, opts, argv)
71
82
  puts heroku_state.send("to_#{opts[:format]}".to_sym)
72
83
  end
@@ -237,7 +248,7 @@ module Hu
237
248
  end
238
249
 
239
250
  def h
240
- @h ||= PlatformAPI.connect_oauth(Hu::Cli::API_TOKEN)
251
+ @h ||= PlatformAPI.connect_oauth(Hu::API_TOKEN)
241
252
  end
242
253
 
243
254
  def pb(show_opts)
@@ -0,0 +1,19 @@
1
+ require 'blackbox/gem'
2
+
3
+ module Hu
4
+ API_TOKEN = ENV['HEROKU_API_KEY'] || ENV['HEROKU_API_TOKEN'] || Netrc.read['api.heroku.com']&.password
5
+ end
6
+
7
+ class String
8
+ def strip_heredoc
9
+ indent = scan(/^[ \t]*(?=\S)/).min&.size || 0
10
+ gsub(/^[ \t]{#{indent}}/, '')
11
+ end
12
+ end
13
+
14
+ version_info = BB::Gem.version_info(check_interval: 900)
15
+ unless version_info[:installed_is_latest] == true
16
+ puts "\e[33;1mWARNING: \e[0mA newer version of #{version_info[:gem_name]} is available."
17
+ puts " Please type '\e[1mgem install #{version_info[:gem_name]}\e[0m' to upgrade (v#{version_info[:gem_installed_version]} -> v#{version_info[:gem_latest_version]})."
18
+ sleep 1
19
+ end
@@ -0,0 +1,587 @@
1
+ require 'version_sorter'
2
+ require 'versionomy'
3
+ require 'tty-prompt'
4
+ require 'tty-spinner'
5
+ require 'tty-table'
6
+ require 'rainbow'
7
+ require 'rainbow/ext/string'
8
+ require 'open3'
9
+ require 'json'
10
+ require 'awesome_print'
11
+ require 'chronic_duration'
12
+ require 'tempfile'
13
+ require 'thread_safe'
14
+ require 'io/console'
15
+
16
+ module Hu
17
+ class Cli < Optix::Cli
18
+ class Deploy < Optix::Cli
19
+ @@shutting_down = false
20
+
21
+ text "Interactive deployment."
22
+ desc "Interactive deployment"
23
+ if Hu::API_TOKEN.nil?
24
+ text ""
25
+ text "\e[1mWARNING: Environment variable 'HEROKU_API_KEY' must be set.\e[0m"
26
+ end
27
+ filter do
28
+ if Hu::API_TOKEN.nil?
29
+ STDERR.puts "\e[0;31;1mERROR: Environment variable 'HEROKU_API_KEY' must be set.\e[0m"
30
+ exit 1
31
+ end
32
+ end
33
+
34
+ def deploy(cmd, opts, argv)
35
+ trap('INT') { shutdown; safe_abort }
36
+ at_exit {
37
+ if 130 == $!.status
38
+ shutdown
39
+ puts
40
+ safe_abort
41
+ end
42
+ }
43
+ push_url = get_heroku_git_remote
44
+
45
+ wc_update = Thread.new { update_working_copy }
46
+
47
+ app = heroku_app_by_git(push_url)
48
+
49
+ if app.nil?
50
+ puts
51
+ puts "FATAL: Found no heroku app for git remote #{push_url}".color(:red)
52
+ puts " Are you logged into the right heroku account?".color(:red)
53
+ puts
54
+ puts " Please run 'git remote rm heroku'. Then run 'hu deploy' again to select a new remote."
55
+ puts
56
+ exit 1
57
+ end
58
+
59
+ pipeline_name, stag_app_id, prod_app_id = heroku_pipeline_details(app)
60
+
61
+ if app['id'] != stag_app_id
62
+ puts
63
+ puts "FATAL: The git remote 'heroku' points to app '#{app['name']}'".color(:red)
64
+ puts " which is not in stage 'staging'".color(:red)+
65
+ " of pipeline '#{pipeline_name}'.".color(:red)
66
+ puts
67
+ puts " The referenced app MUST be the staging member of the pipeline."
68
+
69
+ puts " Please run 'git remote rm heroku'. Then run 'hu deploy' again to select a new remote."
70
+ puts
71
+ sleep 2
72
+ exit 1
73
+ end
74
+
75
+ stag_app_name = app['name']
76
+ busy "fetching heroku app #{prod_app_id}", :dots
77
+ prod_app_name = h.app.info(prod_app_id)['name']
78
+ unbusy
79
+
80
+ busy 'update working copy', :dots
81
+ wc_update.join
82
+ unbusy
83
+
84
+ highest_version = find_highest_version_tag
85
+ likely_next_version = Versionomy.parse(highest_version).bump(:tiny).to_s
86
+ release_tag, branch_already_exists = prompt_for_release_tag(likely_next_version, likely_next_version, true)
87
+
88
+ prompt = TTY::Prompt.new
89
+
90
+ clearscreen = true
91
+ loop do
92
+ git_revisions = show_pipeline_status(pipeline_name, stag_app_name, prod_app_name, release_tag, clearscreen)
93
+ clearscreen = true
94
+
95
+ changelog='Initial revision'
96
+ release_branch_exists = branch_exists?("release/#{release_tag}")
97
+
98
+ if release_branch_exists
99
+ puts "\nThis release will be "+release_tag.color(:red).bright
100
+ unless highest_version == 'v0.0.0'
101
+ changelog=`git log --pretty=format:" - %s" #{highest_version}..HEAD` unless highest_version == 'v0.0.0'
102
+ puts "\nChanges since "+highest_version.bright+":"
103
+ puts changelog
104
+ end
105
+ puts
106
+ else
107
+ puts "\nThis is release "+release_tag.color(:green).bright
108
+ puts
109
+ end
110
+
111
+ choice = prompt.select("Choose your destiny") do |menu|
112
+ menu.enum '.'
113
+ menu.choice "Refresh", :refresh
114
+ menu.choice "Quit", :abort_ask
115
+ unless git_revisions[:release] == git_revisions[stag_app_name] or !release_branch_exists
116
+ menu.choice "Push release/#{release_tag} to #{stag_app_name}", :push_to_staging
117
+ end
118
+ if release_branch_exists
119
+ menu.choice "Delete release/#{release_tag} and start new release from develop", :retag
120
+ menu.choice "Finish release (merge, tag and final stage)", :finish_release
121
+ elsif git_revisions[prod_app_name] != git_revisions[stag_app_name]
122
+ menu.choice "DEPLOY (promote #{stag_app_name} to #{prod_app_name})", :DEPLOY
123
+ end
124
+ end
125
+
126
+ puts
127
+
128
+ case choice
129
+ when :DEPLOY
130
+ promote_to_production
131
+ anykey
132
+ when :finish_release
133
+ old_editor = ENV['EDITOR']
134
+ tf = Tempfile.new('hu-tag')
135
+ tf.write "#{release_tag}\n#{changelog}"
136
+ tf.close
137
+ ENV['EDITOR'] = "cp #{tf.path}"
138
+ unless 0 == finish_release(release_tag)
139
+ abort_merge
140
+ puts "*** ERROR! Push did not complete. *** ".color(:red)
141
+ end
142
+ ENV['EDITOR'] = old_editor
143
+ anykey
144
+ when :push_to_staging
145
+ push_command = "git push #{push_url} release/#{release_tag}:master -f"
146
+ `#{push_command}`
147
+ puts
148
+ anykey
149
+ when :abort_ask
150
+ delete_branch("release/#{release_tag}")
151
+ puts
152
+ exit 0
153
+ when :retag
154
+ if delete_branch("release/#{release_tag}")
155
+ release_tag, branch_already_exists = prompt_for_release_tag(likely_next_version)
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ def show_pipeline_status(pipeline_name, stag_app_name, prod_app_name, release_tag, clear=true)
162
+ table = TTY::Table.new header: %w{location commit tag app_last_modified app_last_modified_by dynos# state}
163
+ busy '♪♫♬ elevator music ', :pulse
164
+ ts = []
165
+ tpl_row = ['?', '', '', '', '', '', '']
166
+ revs = ThreadSafe::Hash.new
167
+
168
+ [[0,stag_app_name],[1,prod_app_name]].each do |idx, app_name|
169
+ ts << Thread.new do
170
+ table_row = tpl_row.dup
171
+ table_row[0] = app_name
172
+ loop do
173
+ dynos = h.dyno.list(app_name)
174
+ break if dynos.nil?
175
+ dp :dynos, dynos
176
+
177
+ table_row[5] = dynos.length
178
+
179
+ release_version = dynos.dig(0, 'release', 'version')
180
+ break if release_version.nil?
181
+
182
+ state = Set.new(dynos.collect{|d| d['state']}).sort.join(', ')
183
+ state_color = (state == 'up') ? 32 : 31
184
+ table_row[6] = "\e[#{state_color};1m#{state}"
185
+
186
+ release_info = h.release.info(app_name, release_version)
187
+ dp :release_info, release_info
188
+ break if release_info.nil?
189
+
190
+ slug_info = h.slug.info(app_name, release_info['slug']['id'])
191
+ dp :slug_info, slug_info
192
+ break if slug_info.nil?
193
+
194
+ revs[app_name] = table_row[1] = slug_info['commit'][0..5]
195
+
196
+ table_row[2] = `git tag --points-at #{slug_info['commit']} 2>/dev/null`
197
+ table_row[2] = '' if $? != 0
198
+
199
+ # heroku uses wrong timezone offset in the slug api... /facepalm
200
+ #table_row[3] = ChronicDuration.output(Time.now.utc - Time.parse(slug_info['updated_at']), :units => 1)
201
+
202
+ table_row[3] = ChronicDuration.output(Time.now.utc - Time.parse(release_info['updated_at']), :units => 1)
203
+ table_row[3] += " ago"
204
+ #table_row[3] += "\n\e[30;1m" + slug_info['updated_at']
205
+
206
+ table_row[4] = release_info['user']['email']
207
+ table_row[5] = dynos.length
208
+ break
209
+ end
210
+ [idx, table_row]
211
+ end
212
+ end
213
+
214
+ rows = []
215
+ ts.each do |t|
216
+ idx, table_row = t.value
217
+ rows[idx] = table_row
218
+ end
219
+
220
+ row = tpl_row.dup
221
+ row[0] = 'master'
222
+ revs[:master] = row[1] = `git rev-parse master`[0..5]
223
+ row[2] = `git tag --points-at master`
224
+ rows.unshift row
225
+
226
+ if branch_exists? "release/#{release_tag}"
227
+ row = tpl_row.dup
228
+ row[0] = "release/#{release_tag}"
229
+ revs["release/#{release_tag}"] = revs[:release] = row[1] = `git rev-parse release/#{release_tag}`[0..5]
230
+ row[2] = `git tag --points-at release/#{release_tag} 2>/dev/null`
231
+ rows.unshift row
232
+ end
233
+
234
+ row = tpl_row.dup
235
+ row[0] = 'develop'
236
+ revs[:develop] = row[1] = `git rev-parse develop`[0..5]
237
+ row[2] = `git tag --points-at develop`
238
+ rows.unshift row
239
+
240
+ unbusy
241
+
242
+ rows.each do |row|
243
+ table << row
244
+ end
245
+
246
+ puts "\e[H\e[2J" if clear
247
+ puts " PIPELINE #{pipeline_name} ".inverse
248
+ puts
249
+
250
+ puts table.render(:unicode, padding: [0,1,0,1], multiline: true)
251
+ revs
252
+ end
253
+
254
+ def heroku_app_by_git(git_url)
255
+ busy('fetching heroku apps', :dots)
256
+ r = h.app.list.select{ |e| e['git_url'] == git_url }
257
+ unbusy
258
+ raise "FATAL: Found multiple heroku apps with git_url=#{git_url}" if r.length > 1
259
+ r[0]
260
+ end
261
+
262
+ def heroku_pipeline_details(app)
263
+ busy('fetching heroku pipelines', :dots)
264
+ couplings = h.pipeline_coupling.list
265
+ unbusy
266
+ r = couplings.select{ |e| e['app']['id'] == app['id'] }
267
+ raise "FATAL: Found multiple heroku pipelines with app.id=#{r['id']}" if r.length > 1
268
+ raise "FATAL: Found no heroku pipeline for app.id=#{r['id']}" if r.length != 1
269
+ r = r[0]
270
+ pipeline_name = r['pipeline']['name']
271
+
272
+ r = couplings.select{ |e| e['pipeline']['id'] == r['pipeline']['id'] and e['stage'] == 'staging' }[0]
273
+ staging_app_id = r['app']['id']
274
+
275
+ r = couplings.select{ |e| e['pipeline']['id'] == r['pipeline']['id'] and e['stage'] == 'production' }[0]
276
+ raise "FATAL: No production app in pipeline #{pipeline_name}" if r.nil?
277
+ prod_app_id = r['app']['id']
278
+ [pipeline_name, staging_app_id, prod_app_id]
279
+ end
280
+
281
+ def h
282
+ @h ||= PlatformAPI.connect_oauth(Hu::API_TOKEN)
283
+ end
284
+
285
+ def run_each(script)
286
+ quiet = false
287
+ failfast = true
288
+ spinner = true
289
+ script.lines.each_with_index do |line, i|
290
+ line.chomp!
291
+ case line[0]
292
+ when '#'
293
+ puts "\n" + line.bright unless quiet
294
+ when ':'
295
+ quiet = true if line == ':quiet'
296
+ failfast = false if line == ':return'
297
+ spinner = false if line == ':nospinner'
298
+ end
299
+ next if line.empty? or ['#', ':'].include? line[0]
300
+ busy line if spinner
301
+ output, status = Open3.capture2e(line)
302
+ unbusy if spinner
303
+ color = (status.exitstatus == 0) ? :green : :red
304
+ if status.exitstatus != 0 or !quiet
305
+ puts "\n> ".color(color) + line.color(:black).bright
306
+ puts output
307
+ end
308
+ if status.exitstatus != 0
309
+ shutdown if failfast
310
+ puts "Error on line #{i}: #{line}"
311
+ puts "Exit code: #{status.exitstatus}"
312
+ exit status.exitstatus if failfast
313
+ return status.exitstatus
314
+ end
315
+ end
316
+ 0
317
+ end
318
+
319
+ def find_highest_version_tag
320
+ output, status = Open3.capture2e('git tag')
321
+ if status.exitstatus != 0
322
+ puts "Error fetching git tags."
323
+ exit status.exitstatus
324
+ end
325
+
326
+ versions = VersionSorter.sort(output.lines.map(&:chomp))
327
+ latest = versions[-1] || 'v0.0.0'
328
+ latest = "v#{latest}" unless latest[0] == "v"
329
+ latest
330
+ end
331
+
332
+ def branch_exists?(branch_name)
333
+ branches = `git for-each-ref refs/heads/ --format='%(refname:short)'`.lines.map(&:chomp)
334
+ branches.include? branch_name
335
+ end
336
+
337
+ def delete_branch(branch_name)
338
+ return false unless branch_exists? branch_name
339
+ return false if TTY::Prompt.new.no?("Delete branch #{branch_name}?")
340
+ run_each <<-EOS.strip_heredoc
341
+ :quiet
342
+ # Delete branch #{branch_name}
343
+ git co develop
344
+ git branch -D #{branch_name}
345
+ EOS
346
+ puts "Branch #{branch_name} deleted.".color(:red)
347
+ true
348
+ end
349
+
350
+ def checkout_branch(branch_name)
351
+ run_each <<-EOS.strip_heredoc
352
+ :quiet
353
+ # Checkout branch #{branch_name}
354
+ git co #{branch_name}
355
+ EOS
356
+ end
357
+
358
+ def start_release(release_tag)
359
+ run_each <<-EOS.strip_heredoc
360
+ # Starting release #{release_tag.color(:green)}
361
+ git flow release start #{release_tag} >/dev/null
362
+ EOS
363
+ end
364
+
365
+ def update_working_copy
366
+ run_each <<-EOS.strip_heredoc
367
+ :quiet
368
+ :nospinner
369
+ # Ensure local repository is up to date
370
+ git checkout develop && git pull
371
+ git checkout master && git pull --rebase origin master
372
+ EOS
373
+ end
374
+
375
+ def get_heroku_git_remote
376
+ ensure_repo_has_heroku_remote
377
+ `git remote show -n heroku | grep Push`.chomp.split(':', 2)[1][1..-1]
378
+ end
379
+
380
+ def ensure_repo_has_heroku_remote
381
+ exit_code = run_each <<-EOS.strip_heredoc
382
+ :quiet
383
+ :return
384
+ # Ensure we have a 'heroku' git remote
385
+ git remote | grep -q "^heroku$"
386
+ EOS
387
+ return if exit_code == 0
388
+
389
+ # Setup git remote
390
+ puts
391
+ puts "This repository has no 'heroku' remote.".color(:red)
392
+ puts "We will set one up now. Please select the pipeline that you"
393
+ puts "wish to deploy to, and we will set the 'heroku' remote"
394
+ puts "to the staging application in that pipeline."
395
+ puts
396
+
397
+ busy
398
+ heroku_apps=JSON.parse(`heroku pipelines:list --json`)
399
+ unbusy
400
+
401
+ prompt = TTY::Prompt.new
402
+ pipeline_name = prompt.select("Select pipeline:") do |menu|
403
+ menu.enum '.'
404
+ heroku_apps.each do |app|
405
+ menu.choice app['name']
406
+ end
407
+ end
408
+ staging_app=JSON.parse(`heroku pipelines:info #{pipeline_name} --json`)['apps'].select{|e| e['coupling']['stage'] == 'staging'}[0]
409
+ if staging_app.nil?
410
+ puts "Error: Pipeline #{pipeline_name} has no staging app.".color(:red)
411
+ exit 1
412
+ end
413
+
414
+ run_each <<-EOS.strip_heredoc
415
+ # Add git remote
416
+ git remote add heroku #{staging_app['git_url']}
417
+ EOS
418
+ end
419
+
420
+ def prompt_for_release_tag(propose_version='v0.0.1', try_version=nil, keep_existing=false)
421
+ prompt = TTY::Prompt.new
422
+ loop do
423
+ if try_version
424
+ release_tag = try_version
425
+ try_version = nil
426
+ else
427
+ show_existing_git_tags
428
+ #puts
429
+ release_tag = prompt.ask("Please enter a tag for this release", default: propose_version)
430
+ begin
431
+ unless release_tag[0] == 'v'
432
+ raise ArgumentError, "Version string must start with the letter v"
433
+ end
434
+ if release_tag.length < 5
435
+ raise ArgumentError, "too short"
436
+ end
437
+ Versionomy.parse(release_tag)
438
+ rescue => e
439
+ puts "Error: Tag does not look like a semantic version (#{e})".color(:red)
440
+ next
441
+ end
442
+ end
443
+
444
+ branches = `git for-each-ref refs/heads/ --format='%(refname:short)'`.lines.map(&:chomp)
445
+ existing_branch = branches.find {|e| e.start_with? 'release/'}
446
+ branch_already_exists = !existing_branch.nil?
447
+ release_tag = existing_branch[8..-1] if keep_existing && branch_already_exists
448
+ if branch_already_exists && !keep_existing
449
+ choice = prompt.expand("The branch '"+"release/#{release_tag}".color(:red)+"' already exists. What shall we do?",
450
+ {default: 0}) do |q|
451
+ q.choice key: 'k', name: 'Keep, continue with the existing branch', value: :keep
452
+ q.choice key: 'D', name: "Delete branch release/#{release_tag} and retry", value: :delete
453
+ q.choice key: 'q', name: 'Quit', value: :quit
454
+ end
455
+
456
+ case choice
457
+ when :quit
458
+ puts
459
+ exit 0
460
+ when :delete
461
+ delete_branch("release/#{release_tag}")
462
+ next
463
+ end
464
+ end
465
+
466
+ if branch_already_exists
467
+ checkout_branch("release/#{release_tag}")
468
+ else
469
+ develop_tag=`git tag --points-at develop 2>/dev/null`.lines.find { |e| e[0] == 'v' }&.chomp
470
+ if develop_tag
471
+ release_tag = develop_tag
472
+ else
473
+ start_release(release_tag)
474
+ end
475
+ end
476
+
477
+ return release_tag, branch_already_exists
478
+ end
479
+ end
480
+
481
+ def show_existing_git_tags
482
+ run_each <<-EOS.strip_heredoc
483
+ # Show existing git tags (previous releases)
484
+ git tag
485
+ EOS
486
+ end
487
+
488
+ def promote_to_production
489
+ run_each <<-EOS.strip_heredoc
490
+ :return
491
+
492
+ # Promote staging to production
493
+ heroku pipelines:promote -r heroku
494
+ EOS
495
+ end
496
+
497
+ def finish_release(release_tag)
498
+ run_each <<-EOS.strip_heredoc
499
+ :return
500
+ # Finish release
501
+ git flow release finish #{release_tag}
502
+
503
+ # Push final master (#{release_tag}) to origin
504
+ git push origin master
505
+ git push origin --tags
506
+
507
+ # Push final master (#{release_tag}) to staging
508
+ git push heroku master:master -f
509
+
510
+ # Merge master back into develop
511
+ git checkout develop
512
+ git merge master
513
+
514
+ # Push develop to origin
515
+ git push origin develop
516
+ EOS
517
+ end
518
+
519
+ def abort_merge
520
+ run_each <<-EOS.strip_heredoc
521
+ # Abort failed merge
522
+ git merge --abort
523
+ EOS
524
+ end
525
+
526
+ def shutdown
527
+ @@shutting_down = true
528
+ unbusy
529
+ end
530
+
531
+ def busy(msg='', format=:classic)
532
+ return if @@shutting_down
533
+ format ||= TTY::Formats::FORMATS.keys.sample
534
+ options = {format: format, hide_cursor: true, error_mark: "\e[31;1m✖\e[0m", success_mark: "\e[32;1m✔\e[0m", clear: true}
535
+ @@spinner = TTY::Spinner.new("\e[0;1m#{msg}#{msg.empty? ? '' : ' '}\e[0m\e[32;1m:spinner\e[0m", options)
536
+ @@spinner.start
537
+ end
538
+
539
+ def unbusy
540
+ @@spinner.stop
541
+ printf "\e[?25h"
542
+ end
543
+
544
+ def with_spinner(msg='', format=:classic, &block)
545
+ busy(msg, format)
546
+ block.call
547
+ unbusy
548
+ end
549
+
550
+ def anykey
551
+ puts TTY::Cursor.hide
552
+ print "--- Press any key to continue ---".color(:cyan).inverse
553
+ STDIN.getch
554
+ print TTY::Cursor.clear_line + TTY::Cursor.show
555
+ end
556
+
557
+ def dp(label, *args)
558
+ return unless ENV['DEBUG']
559
+ puts "--- DEBUG #{label} ---"
560
+ ap *args
561
+ puts "--- ^#{label}^ ---"
562
+ end
563
+
564
+ def safe_abort
565
+ @@spinner.stop
566
+ printf "\e[0m\e[?25l"
567
+ printf '(ヘ・_・)ヘ┳━┳'
568
+ sleep 0.5
569
+ printf "\e[12D(ヘ・_・)-┳━┳"
570
+ sleep 0.1
571
+ printf "\e[12D\e[31;1m(╯°□°)╯ ┻━┻"
572
+ sleep 0.1
573
+ printf "\e[1;31m\e[14D(╯°□°)╯ ┻━┻"
574
+ sleep 0.05
575
+ printf "\e[0;31m\e[15D(╯°□°)╯ ┻━┻"
576
+ sleep 0.05
577
+ printf "\e[30;1m\e[16D(╯°□°)╯ ┻━┻"
578
+ sleep 0.05
579
+ printf "\e[17D "
580
+ printf "\e[?25h"
581
+ puts
582
+ exit 1
583
+ end
584
+ end
585
+ end
586
+ end
587
+
@@ -1,3 +1,3 @@
1
1
  module Hu
2
- VERSION = "1.1.2"
2
+ VERSION = "1.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hu
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - moe
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-02-10 00:00:00.000000000 Z
11
+ date: 2016-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -38,8 +38,120 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bump
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: optix
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.2.4
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.2.4
69
+ - !ruby/object:Gem::Dependency
70
+ name: blackbox
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 3.1.4
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 3.1.4
83
+ - !ruby/object:Gem::Dependency
84
+ name: platform-api
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.7.0
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.7.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: powerbar
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 1.0.16
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 1.0.16
111
+ - !ruby/object:Gem::Dependency
112
+ name: hashdiff
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.3.0
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.3.0
125
+ - !ruby/object:Gem::Dependency
126
+ name: version_sorter
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 2.0.0
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 2.0.0
139
+ - !ruby/object:Gem::Dependency
140
+ name: versionomy
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 0.5.0
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 0.5.0
153
+ - !ruby/object:Gem::Dependency
154
+ name: tty-prompt
43
155
  requirement: !ruby/object:Gem::Requirement
44
156
  requirements:
45
157
  - - ">="
@@ -53,7 +165,7 @@ dependencies:
53
165
  - !ruby/object:Gem::Version
54
166
  version: '0'
55
167
  - !ruby/object:Gem::Dependency
56
- name: platform-api
168
+ name: tty-spinner
57
169
  requirement: !ruby/object:Gem::Requirement
58
170
  requirements:
59
171
  - - ">="
@@ -67,21 +179,63 @@ dependencies:
67
179
  - !ruby/object:Gem::Version
68
180
  version: '0'
69
181
  - !ruby/object:Gem::Dependency
70
- name: powerbar
182
+ name: tty-table
71
183
  requirement: !ruby/object:Gem::Requirement
72
184
  requirements:
73
185
  - - ">="
74
186
  - !ruby/object:Gem::Version
75
- version: 1.0.16
187
+ version: '0'
76
188
  type: :runtime
77
189
  prerelease: false
78
190
  version_requirements: !ruby/object:Gem::Requirement
79
191
  requirements:
80
192
  - - ">="
81
193
  - !ruby/object:Gem::Version
82
- version: 1.0.16
194
+ version: '0'
83
195
  - !ruby/object:Gem::Dependency
84
- name: hashdiff
196
+ name: rainbow
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :runtime
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: netrc
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :runtime
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ - !ruby/object:Gem::Dependency
224
+ name: chronic_duration
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ type: :runtime
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
237
+ - !ruby/object:Gem::Dependency
238
+ name: thread_safe
85
239
  requirement: !ruby/object:Gem::Requirement
86
240
  requirements:
87
241
  - - ">="
@@ -111,6 +265,8 @@ files:
111
265
  - hu.gemspec
112
266
  - lib/hu/cli.rb
113
267
  - lib/hu/collab.rb
268
+ - lib/hu/common.rb
269
+ - lib/hu/deploy.rb
114
270
  - lib/hu/version.rb
115
271
  homepage: https://github.com/busyloop/hu
116
272
  licenses:
@@ -124,7 +280,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
124
280
  requirements:
125
281
  - - ">="
126
282
  - !ruby/object:Gem::Version
127
- version: '0'
283
+ version: 2.3.0
128
284
  required_rubygems_version: !ruby/object:Gem::Requirement
129
285
  requirements:
130
286
  - - ">="