heroku_hatchet 6.0.0 → 7.1.3

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.
@@ -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'