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 +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
|
- - ">="
|