hu 1.1.2 → 1.2.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.
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
  - - ">="