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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +2 -0
- data/CHANGELOG.md +36 -1
- data/README.md +774 -174
- data/bin/hatchet +13 -6
- data/hatchet.gemspec +1 -2
- data/hatchet.json +2 -1
- data/hatchet.lock +2 -0
- data/lib/hatchet.rb +2 -3
- data/lib/hatchet/api_rate_limit.rb +6 -17
- data/lib/hatchet/app.rb +150 -41
- data/lib/hatchet/config.rb +1 -1
- data/lib/hatchet/git_app.rb +29 -2
- data/lib/hatchet/reaper.rb +159 -56
- data/lib/hatchet/reaper/app_age.rb +49 -0
- data/lib/hatchet/reaper/reaper_throttle.rb +55 -0
- data/lib/hatchet/shell_throttle.rb +71 -0
- data/lib/hatchet/test_run.rb +2 -1
- data/lib/hatchet/version.rb +1 -1
- data/spec/hatchet/allow_failure_git_spec.rb +42 -2
- data/spec/hatchet/app_spec.rb +145 -6
- data/spec/hatchet/ci_spec.rb +10 -1
- data/spec/hatchet/git_spec.rb +9 -3
- data/spec/hatchet/lock_spec.rb +63 -1
- data/spec/unit/reaper_spec.rb +169 -0
- data/spec/unit/shell_throttle.rb +28 -0
- metadata +16 -23
data/bin/hatchet
CHANGED
@@ -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]
|
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
|
-
|
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}
|
167
|
+
cmd("cd #{path} && git pull --rebase #{git_repo} --quiet")
|
161
168
|
end
|
162
169
|
|
163
170
|
def clone(path, git_repo, quiet: true)
|
data/hatchet.gemspec
CHANGED
@@ -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
|
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"
|
data/hatchet.json
CHANGED
data/hatchet.lock
CHANGED
@@ -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
|
data/lib/hatchet.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
-
#
|
1
|
+
# Legacy class
|
2
2
|
#
|
3
|
-
#
|
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
|
|
data/lib/hatchet/app.rb
CHANGED
@@ -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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
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
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
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
|
-
|
223
|
-
|
224
|
-
|
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
|
-
|
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
|
-
|
284
|
-
|
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
|
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
|
405
|
+
def retry_error_message(error, attempt)
|
298
406
|
attempt += 1
|
299
|
-
return "" if attempt ==
|
300
|
-
msg = "\nRetrying failed Attempt ##{attempt}/#{
|
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
|
-
|
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
|
-
|
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
|
-
|
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'
|