heroku_hatchet 6.0.0 → 7.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -46,7 +46,7 @@ class HatchetCLI < Thor
46
46
  Threaded.later do
47
47
  commit = lock_hash[directory]
48
48
  directory = File.expand_path(directory)
49
- if Dir[directory].present?
49
+ if !Dir[directory]&.empty?
50
50
  puts "== pulling '#{git_repo}' into '#{directory}'\n"
51
51
  pull(directory, git_repo)
52
52
  else
@@ -67,7 +67,7 @@ class HatchetCLI < Thor
67
67
  desc "locks to specific git commits", "updates hatchet.lock"
68
68
  def lock
69
69
  lock_hash = {}
70
- lockfile_hash = load_lockfile
70
+ lockfile_hash = load_lockfile(create_if_does_not_exist: true)
71
71
  dirs.map do |directory, git_repo|
72
72
  Threaded.later do
73
73
  puts "== locking #{directory}"
@@ -78,6 +78,8 @@ class HatchetCLI < Thor
78
78
 
79
79
  if lockfile_hash[directory] == "master"
80
80
  lock_hash[directory] = "master"
81
+ elsif lockfile_hash[directory] == "main"
82
+ lock_hash[directory] = "main"
81
83
  else
82
84
  commit = commit_at_directory(directory)
83
85
  lock_hash[directory] = commit
@@ -116,10 +118,15 @@ class HatchetCLI < Thor
116
118
  end
117
119
 
118
120
  private
119
- def load_lockfile
121
+ def load_lockfile(create_if_does_not_exist: false)
120
122
  return YAML.safe_load(File.read('hatchet.lock')).to_h
121
123
  rescue Errno::ENOENT
122
- raise "No such file found `hatchet.lock` please run `$ bundle exec hatchet lock`"
124
+ if create_if_does_not_exist
125
+ FileUtils.touch('hatchet.lock')
126
+ {}
127
+ else
128
+ raise "No such file found `hatchet.lock` please run `$ bundle exec hatchet lock`"
129
+ end
123
130
  end
124
131
 
125
132
  def bad_repo?(url)
@@ -149,7 +156,7 @@ class HatchetCLI < Thor
149
156
  end
150
157
 
151
158
  def checkout_commit(directory, commit)
152
- cmd("cd #{directory} && git reset --hard #{commit}")
159
+ cmd("cd #{directory} && git fetch origin #{commit} && git checkout #{commit} && git checkout - && git reset --hard #{commit}")
153
160
  end
154
161
 
155
162
  def commit_at_directory(directory)
@@ -157,7 +164,7 @@ class HatchetCLI < Thor
157
164
  end
158
165
 
159
166
  def pull(path, git_repo, commit: false)
160
- cmd("cd #{path} && git pull --rebase #{git_repo} master --quiet")
167
+ cmd("cd #{path} && git pull --rebase #{git_repo} --quiet")
161
168
  end
162
169
 
163
170
  def clone(path, git_repo, quiet: true)
@@ -18,11 +18,10 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
- gem.add_dependency "platform-api", "3.0.0.pre.1 "
21
+ gem.add_dependency "platform-api", "~> 3"
22
22
  gem.add_dependency "rrrretry", "~> 1"
23
23
  gem.add_dependency "excon", "~> 0"
24
24
  gem.add_dependency "thor", "~> 0"
25
- gem.add_dependency "repl_runner", "~> 0.0.3"
26
25
  gem.add_dependency "threaded", "~> 0"
27
26
 
28
27
  gem.add_development_dependency "rspec"
@@ -12,6 +12,7 @@
12
12
  ],
13
13
  "lock": [
14
14
  "sharpstone/lock_fail",
15
- "sharpstone/lock_fail_master"
15
+ "sharpstone/lock_fail_master",
16
+ "sharpstone/lock_fail_main"
16
17
  ]
17
18
  }
@@ -9,6 +9,8 @@
9
9
  - 6e642963acec0ff64af51bd6fba8db3c4176ed6e
10
10
  - - repo_fixtures/repos/lock/lock_fail
11
11
  - da748a59340be8b950e7bbbfb32077eb67d70c3c
12
+ - - repo_fixtures/repos/lock/lock_fail_main
13
+ - main
12
14
  - - repo_fixtures/repos/lock/lock_fail_master
13
15
  - master
14
16
  - - repo_fixtures/repos/rails2/rails2blog
@@ -1,4 +1,3 @@
1
- require 'active_support/core_ext/object/blank'
2
1
  require 'rrrretry'
3
2
 
4
3
  require 'json'
@@ -8,7 +7,7 @@ require 'stringio'
8
7
  require 'date'
9
8
 
10
9
  module Hatchet
11
- APP_PREFIX = (ENV['HATCHET_APP_PREFIX'] || "hatchet-t-")
10
+ APP_PREFIX = (ENV['HATCHET_APP_PREFIX'] || "hatchet-t-")
12
11
  end
13
12
 
14
13
  require 'hatchet/version'
@@ -31,7 +30,7 @@ module Hatchet
31
30
  return ENV['TRAVIS_PULL_REQUEST_BRANCH'] if ENV['TRAVIS_PULL_REQUEST_BRANCH'] && !ENV['TRAVIS_PULL_REQUEST_BRANCH'].empty?
32
31
  return ENV['TRAVIS_BRANCH'] if ENV['TRAVIS_BRANCH']
33
32
 
34
- out = `git describe --contains --all HEAD`.strip
33
+ out = `git rev-parse --abbrev-ref HEAD`.strip
35
34
  raise "Attempting to find current branch name. Error: Cannot describe git: #{out}" unless $?.success?
36
35
  out
37
36
  end
@@ -1,14 +1,11 @@
1
- # Wraps platform-api and adds API rate limits
1
+ # Legacy class
2
2
  #
3
- # Instead of:
4
- #
5
- # platform_api.pipeline.create(name: @name)
6
- #
7
- # Use:
8
- #
9
- # api_rate_limit = ApiRateLimit.new(platform_api)
10
- # api_rate_limit.call.pipeline.create(name: @name)
3
+ # Not needed since rate throttling went directly into the platform-api gem.
4
+ # This class is effectively now a no-op
11
5
  #
6
+ # It's being left in as it's interface was public and it's hard-ish to
7
+ # deprecate/remove. Since it's so small there's not much value in removal
8
+ # so it's probably fine to keep around for quite some time.
12
9
  class ApiRateLimit
13
10
  def initialize(platform_api)
14
11
  @platform_api = platform_api
@@ -16,14 +13,6 @@ class ApiRateLimit
16
13
  @called = 0
17
14
  end
18
15
 
19
-
20
- # Sleeps for progressively longer when api rate limit capacity
21
- # is lower.
22
- #
23
- # Unfortunatley `@platform_api.rate_limit` is an extra API
24
- # call, so by checking our limit, we also are using our limit 😬
25
- # to partially mitigate this, only check capacity every 5
26
- # api calls, or if the current capacity is under 1000
27
16
  def call
28
17
  # @called += 1
29
18
 
@@ -9,14 +9,34 @@ module Hatchet
9
9
  HATCHET_BUILDPACK_BRANCH = -> { ENV['HATCHET_BUILDPACK_BRANCH'] || ENV['HEROKU_TEST_RUN_BRANCH'] || Hatchet.git_branch }
10
10
  BUILDPACK_URL = "https://github.com/heroku/heroku-buildpack-ruby.git"
11
11
 
12
- attr_reader :name, :stack, :directory, :repo_name, :app_config, :buildpacks
13
-
14
- class FailedDeploy < StandardError
15
- def initialize(app, output)
16
- msg = "Could not deploy '#{app.name}' (#{app.repo_name}) using '#{app.class}' at path: '#{app.directory}'\n" <<
17
- " if this was expected add `allow_failure: true` to your deploy hash.\n" <<
18
- "output:\n" <<
19
- "#{output}"
12
+ attr_reader :name, :stack, :directory, :repo_name, :app_config, :buildpacks, :reaper, :max_retries_count
13
+
14
+ class FailedDeploy < StandardError; end
15
+
16
+ class FailedDeployError < FailedDeploy
17
+ attr_reader :output
18
+
19
+ def initialize(app, message, output: )
20
+ @output = output
21
+ msg = "Could not deploy '#{app.name}' (#{app.repo_name}) using '#{app.class}' at path: '#{app.directory}'\n"
22
+ msg << "if this was expected add `allow_failure: true` to your deploy hash.\n"
23
+ msg << "#{message}\n"
24
+ msg << "output:\n"
25
+ msg << "#{output}"
26
+ super(msg)
27
+ end
28
+ end
29
+
30
+ class FailedReleaseError < FailedDeploy
31
+ attr_reader :output
32
+
33
+ def initialize(app, message, output: )
34
+ @output = output
35
+ msg = "Could not release '#{app.name}' (#{app.repo_name}) using '#{app.class}' at path: '#{app.directory}'\n"
36
+ msg << "if this was expected add `allow_failure: true` to your deploy hash.\n"
37
+ msg << "#{message}\n"
38
+ msg << "output:\n"
39
+ msg << "#{output}"
20
40
  super(msg)
21
41
  end
22
42
  end
@@ -34,6 +54,8 @@ module Hatchet
34
54
  buildpacks: nil,
35
55
  buildpack_url: nil,
36
56
  before_deploy: nil,
57
+ run_multi: ENV["HATCHET_RUN_MULTI"],
58
+ retries: RETRIES,
37
59
  config: {}
38
60
  )
39
61
  @repo_name = repo_name
@@ -46,6 +68,13 @@ module Hatchet
46
68
  @buildpacks = buildpack || buildpacks || buildpack_url || self.class.default_buildpack
47
69
  @buildpacks = Array(@buildpacks)
48
70
  @buildpacks.map! {|b| b == :default ? self.class.default_buildpack : b}
71
+ @run_multi = run_multi
72
+ @max_retries_count = retries
73
+
74
+ if run_multi && !ENV["HATCHET_EXPENSIVE_MODE"]
75
+ raise "You're attempting to enable `run_multi: true` mode, but have not enabled `HATCHET_EXPENSIVE_MODE=1` env var to verify you understand the risks"
76
+ end
77
+ @run_multi_array = []
49
78
  @already_in_dir = nil
50
79
  @app_is_setup = nil
51
80
 
@@ -102,7 +131,7 @@ module Hatchet
102
131
  end
103
132
 
104
133
  def add_database(plan_name = 'heroku-postgresql:dev', match_val = "HEROKU_POSTGRESQL_[A-Z]+_URL")
105
- Hatchet::RETRIES.times.retry do
134
+ max_retries_count.times.retry do
106
135
  # heroku.post_addon(name, plan_name)
107
136
  api_rate_limit.call.addon.create(name, plan: plan_name )
108
137
  _, value = get_config.detect {|k, v| k.match(/#{match_val}/) }
@@ -127,6 +156,22 @@ module Hatchet
127
156
  else
128
157
  command = command.to_s
129
158
  end
159
+
160
+ heroku_command = build_heroku_command(command, options)
161
+
162
+ allow_run_multi! if @run_multi
163
+
164
+ output = ""
165
+
166
+ ShellThrottle.new(platform_api: @platform_api).call do |throttle|
167
+ output = `#{heroku_command}`
168
+ throw(:throttle) if output.match?(/reached the API rate limit/)
169
+ end
170
+
171
+ return output
172
+ end
173
+
174
+ private def build_heroku_command(command, options = {})
130
175
  command = command.shellescape unless options.delete(:raw)
131
176
 
132
177
  default_options = { "app" => name, "exit-code" => nil }
@@ -136,16 +181,77 @@ module Hatchet
136
181
  arg << "=#{v.to_s.shellescape}" unless v.nil? # nil means we include the option without an argument
137
182
  arg
138
183
  end.join(" ")
139
- heroku_command = "heroku run #{heroku_options} -- #{command}"
140
184
 
141
- if block_given?
142
- STDERR.puts "Using App#run with a block is deprecated, support for ReplRunner is being removed.\n#{caller}"
143
- # When we deprecated this we can get rid of the "cmd_type" from the method signature
144
- require 'repl_runner'
145
- ReplRunner.new(cmd_type, heroku_command, options).run(&block)
146
- else
147
- `#{heroku_command}`
185
+ "heroku run #{heroku_options} -- #{command}"
186
+ end
187
+
188
+ private def allow_run_multi!
189
+ raise "Must explicitly enable the `run_multi: true` option. This requires scaling up a dyno and is not free, it may incur charges on your account" unless @run_multi
190
+
191
+ @run_multi_is_setup ||= platform_api.formation.update(name, "web", {"size" => "Standard-1X"})
192
+ end
193
+
194
+
195
+ # Allows multiple commands to be run concurrently in the background.
196
+ #
197
+ # WARNING! Using the feature requres that the underlying app is not on the "free" Heroku
198
+ # tier. This requires scaling up the dyno which is not free. If an app is
199
+ # scaled up and left in that state it can incur large costs.
200
+ #
201
+ # Enabling this feature should be done with extreme caution.
202
+ #
203
+ # Example:
204
+ #
205
+ # Hatchet::Runner.new("default_ruby", run_multi: true)
206
+ # app.run_multi("ls") { |out| expect(out).to include("Gemfile") }
207
+ # app.run_multi("ruby -v") { |out| expect(out).to include("ruby") }
208
+ # end
209
+ #
210
+ # This example will run `heroku run ls` as well as `ruby -v` at the same time in the background.
211
+ # The return result will be yielded to the block after they finish running.
212
+ #
213
+ # Order of execution is not guaranteed.
214
+ #
215
+ # If you need to assert a command was successful, you can yield a second status object like this:
216
+ #
217
+ # Hatchet::Runner.new("default_ruby", run_multi: true)
218
+ # app.run_multi("ls") do |out, status|
219
+ # expect(status.success?).to be_truthy
220
+ # expect(out).to include("Gemfile")
221
+ # end
222
+ # app.run_multi("ruby -v") do |out, status|
223
+ # expect(status.success?).to be_truthy
224
+ # expect(out).to include("ruby")
225
+ # end
226
+ # end
227
+ def run_multi(command, options = {}, &block)
228
+ raise "Block required" if block.nil?
229
+ allow_run_multi!
230
+
231
+ run_thread = Thread.new do
232
+ heroku_command = build_heroku_command(command, options)
233
+
234
+ out = nil
235
+ status = nil
236
+ ShellThrottle.new(platform_api: @platform_api).call do |throttle|
237
+ out = `#{heroku_command}`
238
+ throw(:throttle) if output.match?(/reached the API rate limit/)
239
+ status = $?
240
+ end
241
+
242
+ yield out, status
243
+
244
+ # if block.arity == 1
245
+ # block.call(out)
246
+ # else
247
+ # block.call(out, status)
248
+ # end
148
249
  end
250
+ run_thread.abort_on_exception = true
251
+
252
+ @run_multi_array << run_thread
253
+
254
+ true
149
255
  end
150
256
 
151
257
  # set debug: true when creating app if you don't want it to be
@@ -162,24 +268,26 @@ module Hatchet
162
268
  alias :no_debug? :not_debugging?
163
269
 
164
270
  def deployed?
165
- # !heroku.get_ps(name).body.detect {|ps| ps["process"].include?("web") }.nil?
166
271
  api_rate_limit.call.formation.list(name).detect {|ps| ps["type"] == "web"}
167
272
  end
168
273
 
169
274
  def create_app
170
275
  3.times.retry do
171
276
  begin
172
- # heroku.post_app({ name: name, stack: stack }.delete_if {|k,v| v.nil? })
173
277
  hash = { name: name, stack: stack }
174
278
  hash.delete_if { |k,v| v.nil? }
175
- api_rate_limit.call.app.create(hash)
279
+ heroku_api_create_app(hash)
176
280
  rescue => e
177
- @reaper.cycle
281
+ @reaper.cycle(app_exception_message: e.message)
178
282
  raise e
179
283
  end
180
284
  end
181
285
  end
182
286
 
287
+ private def heroku_api_create_app(hash)
288
+ api_rate_limit.call.app.create(hash)
289
+ end
290
+
183
291
  def update_stack(stack_name)
184
292
  @stack = stack_name
185
293
  api_rate_limit.call.app.update(name, build_stack: @stack)
@@ -210,7 +318,7 @@ module Hatchet
210
318
  end
211
319
 
212
320
  def commit!
213
- local_cmd_exec!('git add .; git commit -m next')
321
+ local_cmd_exec!('git add .; git commit --allow-empty -m next')
214
322
  end
215
323
 
216
324
  def push_without_retry!
@@ -219,11 +327,15 @@ module Hatchet
219
327
 
220
328
  def teardown!
221
329
  return false unless @app_is_setup
222
- if debugging?
223
- puts "Debugging App:#{name}"
224
- return false
330
+
331
+ if @run_multi_is_setup
332
+ @run_multi_array.map(&:join)
333
+ platform_api.formation.update(name, "web", {"size" => "free"})
225
334
  end
226
- @reaper.cycle
335
+
336
+ ensure
337
+ @app_update_info = platform_api.app.update(name, { maintenance: true }) if @app_is_setup
338
+ @reaper.cycle if @app_is_setup
227
339
  end
228
340
 
229
341
  def in_directory(directory = self.directory)
@@ -265,10 +377,6 @@ module Hatchet
265
377
  end
266
378
  end
267
379
 
268
- # creates a new app on heroku, "pushes" via anvil or git
269
- # then yields to self so you can call self.run or
270
- # self.deployed?
271
- # Allow deploy failures on CI server by setting ENV['HATCHET_RETRIES']
272
380
  def deploy(&block)
273
381
  in_directory do
274
382
  self.setup!
@@ -276,16 +384,16 @@ module Hatchet
276
384
  block.call(self, api_rate_limit.call, output) if block_given?
277
385
  end
278
386
  ensure
279
- self.teardown!
387
+ self.teardown! if block_given?
280
388
  end
281
389
 
282
390
  def push
283
- max_retries = @allow_failure ? 1 : RETRIES
284
- max_retries.times.retry do |attempt|
391
+ retry_count = allow_failure? ? 1 : max_retries_count
392
+ retry_count.times.retry do |attempt|
285
393
  begin
286
394
  @output = self.push_without_retry!
287
395
  rescue StandardError => error
288
- puts retry_error_message(error, attempt, max_retries)
396
+ puts retry_error_message(error, attempt) unless retry_count == 1
289
397
  raise error
290
398
  end
291
399
  end
@@ -294,10 +402,10 @@ module Hatchet
294
402
  alias :push_with_retry :push
295
403
  alias :push_with_retry! :push_with_retry
296
404
 
297
- def retry_error_message(error, attempt, max_retries)
405
+ def retry_error_message(error, attempt)
298
406
  attempt += 1
299
- return "" if attempt == max_retries
300
- msg = "\nRetrying failed Attempt ##{attempt}/#{max_retries} to push for '#{name}' due to error: \n"<<
407
+ return "" if attempt == max_retries_count
408
+ msg = "\nRetrying failed Attempt ##{attempt}/#{max_retries_count} to push for '#{name}' due to error: \n"<<
301
409
  "#{error.class} #{error.message}\n #{error.backtrace.join("\n ")}"
302
410
  return msg
303
411
  end
@@ -316,7 +424,7 @@ module Hatchet
316
424
 
317
425
  def run_ci(timeout: 300, &block)
318
426
  in_directory do
319
- Hatchet::RETRIES.times.retry do
427
+ max_retries_count.times.retry do
320
428
  result = create_pipeline
321
429
  @pipeline_id = result["id"]
322
430
  end
@@ -327,7 +435,7 @@ module Hatchet
327
435
  # that's why we create an app explictly (or maybe it already exists), and then associate it with with the pipeline
328
436
  # the app will be auto cleaned up later
329
437
  self.setup!
330
- Hatchet::RETRIES.times.retry do
438
+ max_retries_count.times.retry do
331
439
  couple_pipeline(@name, @pipeline_id)
332
440
  end
333
441
 
@@ -340,12 +448,13 @@ module Hatchet
340
448
  api_rate_limit: api_rate_limit
341
449
  )
342
450
 
343
- Hatchet::RETRIES.times.retry do
451
+ max_retries_count.times.retry do
344
452
  test_run.create_test_run
345
453
  end
346
454
  test_run.wait!(&block)
347
455
  end
348
456
  ensure
457
+ teardown! if block_given?
349
458
  delete_pipeline(@pipeline_id) if @pipeline_id
350
459
  @pipeline_id = nil
351
460
  end
@@ -381,7 +490,6 @@ module Hatchet
381
490
  end
382
491
 
383
492
  def platform_api
384
- puts "Deprecated: use `api_rate_limit.call` instead of platform_api"
385
493
  api_rate_limit
386
494
  return @platform_api
387
495
  end
@@ -433,3 +541,4 @@ module Hatchet
433
541
  end
434
542
  end
435
543
 
544
+ require_relative 'shell_throttle.rb'