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 +4 -4
- data/hu.gemspec +15 -3
- data/lib/hu/cli.rb +4 -12
- data/lib/hu/collab.rb +13 -2
- data/lib/hu/common.rb +19 -0
- data/lib/hu/deploy.rb +587 -0
- data/lib/hu/version.rb +1 -1
- metadata +164 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 175e30869a5caaf6629de09a9b92aa270aa726a5
|
4
|
+
data.tar.gz: 2028694a799523666fcc690f6652899bc71e02d3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 "
|
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
|
data/lib/hu/cli.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/hu/collab.rb
CHANGED
@@ -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", "
|
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::
|
251
|
+
@h ||= PlatformAPI.connect_oauth(Hu::API_TOKEN)
|
241
252
|
end
|
242
253
|
|
243
254
|
def pb(show_opts)
|
data/lib/hu/common.rb
ADDED
@@ -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
|
data/lib/hu/deploy.rb
ADDED
@@ -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
|
+
|
data/lib/hu/version.rb
CHANGED
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.
|
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-
|
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:
|
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:
|
182
|
+
name: tty-table
|
71
183
|
requirement: !ruby/object:Gem::Requirement
|
72
184
|
requirements:
|
73
185
|
- - ">="
|
74
186
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
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:
|
194
|
+
version: '0'
|
83
195
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
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:
|
283
|
+
version: 2.3.0
|
128
284
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
285
|
requirements:
|
130
286
|
- - ">="
|