heroku_hatchet 7.2.0 → 7.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2dd4a304828fca43576fed5a71b7be305142c3220c7dd020bff44b17a4a070a0
4
- data.tar.gz: c0dee0a8ffb794f10a3dbb96beeafa26dd5f95b489eb9638a40bf408dce44c11
3
+ metadata.gz: f133f985be889073e9f62caf69e73e05373eecaed11cb13a209fe6bd80874f1c
4
+ data.tar.gz: 910dd76398351f6ecf19e6a882b7652fe531e6a19843fa64d3f5e57ef5e68696
5
5
  SHA512:
6
- metadata.gz: f6fa0cef6167e9b4985b08f01e42f33410632d4dfb083551d2641e7009745a079044f606cc4456ed8b8f0dfb0ec087b9b3197d1dabb9a0c41cd7f96ee978e22c
7
- data.tar.gz: 50689c87372d4df2fc7d354ccc0f6f870285283869a190f3272f76d0d238e2d3cd13ca485ca6436916a6976c847fbd2be057fbe4e99ed1aa8ac97abdfef9eeb9
6
+ metadata.gz: 952b664cbb78a6b7250f98a3532a5754bf3fe16526ef915594504d6c9ee77ddf97ac204ab9438d3223da33f431c27e335c9495c86c74baeb69db45fd22682f63
7
+ data.tar.gz: e282d36a9196b74f9999c7c7b57ae55cd1da508f09187058baf92fa711800e882c806299f472ac06cceda88cfe2eba63b3e7eb67c0ac0e657436fc1c207f0e02
@@ -1,6 +1,26 @@
1
1
  ## HEAD
2
2
 
3
- ## 7.1.4
3
+ ## 7.3.0
4
+
5
+ - Deprecations
6
+ - Deprecation: Calling `App#before_deploy` as a way to clear/replace the existing block should now be done with `App#before_deploy(:replace)` (https://github.com/heroku/hatchet/pull/126)
7
+ - Deprecation: HATCHET_BUILDPACK_BASE default (https://github.com/heroku/hatchet/pull/133)
8
+ - Deprecation: App#directory (https://github.com/heroku/hatchet/pull/135)
9
+
10
+ - Flappy test improvements
11
+ - Increase CI timeout limit to 900 seconds (15 minutes) (https://github.com/heroku/hatchet/pull/137)
12
+ - Empty string returns from App#run now trigger retries (https://github.com/heroku/hatchet/pull/132)
13
+ - Rescue 403 on pipeline delete (https://github.com/heroku/hatchet/pull/130)
14
+ - Additional rate throttle cases handled (https://github.com/heroku/hatchet/pull/128)
15
+
16
+ - Usability
17
+ - Annotate rspec expectation failures inside of deploy blocks with hatchet debug information (https://github.com/heroku/hatchet/pull/136)
18
+ - Hatchet#new raises a helpful error when no source code location is provided (https://github.com/heroku/hatchet/pull/134)
19
+ - Lazy evaluation of HATCHET_BUILDPACK_BASE env var (https://github.com/heroku/hatchet/pull/133)
20
+ - Allow multiple `App#before_deploy` blocks to be set and called (https://github.com/heroku/hatchet/pull/126)
21
+ - Performance improvement when running without an explicit HEROKU_API_KEY set (https://github.com/heroku/hatchet/pull/128)
22
+
23
+ ## 7.2.0
4
24
 
5
25
  - App#setup! no longer modifies files on disk. (https://github.com/heroku/hatchet/pull/125)
6
26
  - Add `$ hatchet init` command for bootstrapping new projects (https://github.com/heroku/hatchet/pull/123)
data/README.md CHANGED
@@ -525,7 +525,7 @@ end
525
525
 
526
526
  In this example, the app would use the nodejs buildpack, and then `:default` gets replaced by your Git url and branch name.
527
527
 
528
- - before_deploy (Block): Instead of using the `tap` syntax you can provide a block directly to hatchet app initialization:
528
+ - before_deploy (Block): Instead of using the `tap` syntax you can provide a block directly to hatchet app initialization. Example:
529
529
 
530
530
  ```ruby
531
531
  Hatchet::Runner.new("default_ruby", before_deploy: ->{ FileUtils.touch("foo.txt")}).deploy do
@@ -630,6 +630,23 @@ Hatchet::Runner.new("default_ruby", before_deploy: before_deploy_proc).deploy do
630
630
  end
631
631
  ```
632
632
 
633
+ You can call multiple blocks by specifying (`:prepend` or `:append`):
634
+
635
+ ```ruby
636
+ Hatchet::Runner.new("default_ruby").tap do |app|
637
+ app.before_deploy do
638
+ FileUtils.touch("foo.txt")
639
+ end
640
+
641
+ app.before_deploy(:append) do
642
+ FileUtils.touch("bar.txt")
643
+ end
644
+ app.deploy do
645
+ end
646
+ end
647
+ ```
648
+
649
+
633
650
  - `app.commit!`: Will updates the contents of your local git dir if you've modified files on disk
634
651
 
635
652
  ```ruby
@@ -660,7 +677,7 @@ end
660
677
  > Note: If you want to execute tests in this temp directory, you likely want to use `in_directory_fork` otherwise, you might accidentally contaminate the current environment's variables if you modify them.
661
678
 
662
679
  - `app.in_directory_fork`: Runs the given block in a temp directory and inside of a forked process, an example given above.
663
- - `app.directory`: Returns the directory of the example application on disk, this is NOT the temp directory that you're currently executing against. It's probably not what you want.
680
+ - `app.original_source_code_directory`: Returns the directory of the example application on disk, this is NOT the temp directory that you're currently executing against. It's probably not what you want.
664
681
  - `app.deploy`: Your main method takes a block to execute after the deploy is successful. If no block is provided, you must manually call `app.teardown!` (see below for an example).
665
682
  - `app.output`: The output contents of the deploy
666
683
  - `app.platform_api`: Returns an instance of the [platform-api Heroku client](https://github.com/heroku/platform-api). If Hatchet doesn't give you access to a part of Heroku that you need, you can likely do it with the platform-api client.
@@ -19,6 +19,7 @@ require 'hatchet/git_app'
19
19
  require 'hatchet/config'
20
20
  require 'hatchet/api_rate_limit'
21
21
  require 'hatchet/init_project'
22
+ require 'hatchet/heroku_run'
22
23
 
23
24
  module Hatchet
24
25
  RETRIES = Integer(ENV['HATCHET_RETRIES'] || 1)
@@ -5,11 +5,15 @@ require 'tmpdir'
5
5
 
6
6
  module Hatchet
7
7
  class App
8
- HATCHET_BUILDPACK_BASE = (ENV['HATCHET_BUILDPACK_BASE'] || "https://github.com/heroku/heroku-buildpack-ruby.git")
8
+ HATCHET_BUILDPACK_BASE = -> {
9
+ ENV.fetch('HATCHET_BUILDPACK_BASE') {
10
+ warn "ENV HATCHET_BUILDPACK_BASE is not set. It currently defaults to the ruby buildpack. In the future this env var will be required"
11
+ "https://github.com/heroku/heroku-buildpack-ruby.git"
12
+ }
13
+ }
9
14
  HATCHET_BUILDPACK_BRANCH = -> { ENV['HATCHET_BUILDPACK_BRANCH'] || ENV['HEROKU_TEST_RUN_BRANCH'] || Hatchet.git_branch }
10
- BUILDPACK_URL = "https://github.com/heroku/heroku-buildpack-ruby.git"
11
15
 
12
- attr_reader :name, :stack, :directory, :repo_name, :app_config, :buildpacks, :reaper, :max_retries_count
16
+ attr_reader :name, :stack, :repo_name, :app_config, :buildpacks, :reaper, :max_retries_count
13
17
 
14
18
  class FailedDeploy < StandardError; end
15
19
 
@@ -18,7 +22,7 @@ module Hatchet
18
22
 
19
23
  def initialize(app, message, output: )
20
24
  @output = output
21
- msg = "Could not deploy '#{app.name}' (#{app.repo_name}) using '#{app.class}' at path: '#{app.directory}'\n"
25
+ msg = "Could not deploy '#{app.name}' (#{app.repo_name}) using '#{app.class}' at path: '#{app.original_source_code_directory}'\n"
22
26
  msg << "if this was expected add `allow_failure: true` to your deploy hash.\n"
23
27
  msg << "#{message}\n"
24
28
  msg << "output:\n"
@@ -32,7 +36,7 @@ module Hatchet
32
36
 
33
37
  def initialize(app, message, output: )
34
38
  @output = output
35
- msg = "Could not release '#{app.name}' (#{app.repo_name}) using '#{app.class}' at path: '#{app.directory}'\n"
39
+ msg = "Could not release '#{app.name}' (#{app.repo_name}) using '#{app.class}' at path: '#{app.original_source_code_directory}'\n"
36
40
  msg << "if this was expected add `allow_failure: true` to your deploy hash.\n"
37
41
  msg << "#{message}\n"
38
42
  msg << "output:\n"
@@ -42,8 +46,9 @@ module Hatchet
42
46
  end
43
47
 
44
48
  SkipDefaultOption = Object.new
49
+ DEFAULT_REPO_NAME = Object.new
45
50
 
46
- def initialize(repo_name,
51
+ def initialize(repo_name = DEFAULT_REPO_NAME,
47
52
  stack: "",
48
53
  name: default_name,
49
54
  debug: nil,
@@ -58,6 +63,7 @@ module Hatchet
58
63
  retries: RETRIES,
59
64
  config: {}
60
65
  )
66
+ raise "You tried creating a Hatchet::App instance without source code, pass in a path to an app to deploy or the name of an app in your hatchet.json" if repo_name == DEFAULT_REPO_NAME
61
67
  @repo_name = repo_name
62
68
  @directory = self.config.path_for_name(@repo_name)
63
69
  @name = name
@@ -78,13 +84,36 @@ module Hatchet
78
84
  @already_in_dir = nil
79
85
  @app_is_setup = nil
80
86
 
81
- @before_deploy = before_deploy
87
+ @before_deploy_array = []
88
+ @before_deploy_array << before_deploy if before_deploy
82
89
  @app_config = config
83
90
  @reaper = Reaper.new(api_rate_limit: api_rate_limit)
84
91
  end
85
92
 
93
+ private def test_failure_classes
94
+ class_array = []
95
+ class_array << RSpec::Expectations::ExpectationNotMetError if defined?(RSpec::Expectations::ExpectationNotMetError)
96
+ class_array
97
+ end
98
+
99
+ def annotate_failures
100
+ yield
101
+ rescue *test_failure_classes => e
102
+ raise e, "App: #{name} (#{@repo_name})\n#{e.message}"
103
+ end
104
+
105
+ def directory
106
+ warn "Calling App#directory returns the original location of the app's source code that should not be modified, if this is really what you want use `original_source_code_directory` instead."
107
+ warn caller
108
+ @directory
109
+ end
110
+
111
+ def original_source_code_directory
112
+ @directory
113
+ end
114
+
86
115
  def self.default_buildpack
87
- [HATCHET_BUILDPACK_BASE, HATCHET_BUILDPACK_BRANCH.call].join("#")
116
+ [HATCHET_BUILDPACK_BASE.call, HATCHET_BUILDPACK_BRANCH.call].join("#")
88
117
  end
89
118
 
90
119
  def allow_failure?
@@ -157,32 +186,17 @@ module Hatchet
157
186
  command = command.to_s
158
187
  end
159
188
 
160
- heroku_command = build_heroku_command(command, options)
161
-
162
189
  allow_run_multi! if @run_multi
163
190
 
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 = {})
175
- command = command.shellescape unless options.delete(:raw)
176
-
177
- default_options = { "app" => name, "exit-code" => nil }
178
- heroku_options = (default_options.merge(options.delete(:heroku) || {})).map do |k,v|
179
- next if v == Hatchet::App::SkipDefaultOption # for forcefully removing e.g. --exit-code, a user can pass this
180
- arg = "--#{k.to_s.shellescape}"
181
- arg << "=#{v.to_s.shellescape}" unless v.nil? # nil means we include the option without an argument
182
- arg
183
- end.join(" ")
191
+ run_obj = Hatchet::HerokuRun.new(
192
+ command,
193
+ app: self,
194
+ retry_on_empty: options.fetch(:retry_on_empty, !ENV["HATCHET_DISABLE_EMPTY_RUN_RETRY"]),
195
+ heroku: options[:heroku],
196
+ raw: options[:raw]
197
+ ).call
184
198
 
185
- "heroku run #{heroku_options} -- #{command}"
199
+ return run_obj.output
186
200
  end
187
201
 
188
202
  private def allow_run_multi!
@@ -191,7 +205,6 @@ module Hatchet
191
205
  @run_multi_is_setup ||= platform_api.formation.update(name, "web", {"size" => "Standard-1X"})
192
206
  end
193
207
 
194
-
195
208
  # Allows multiple commands to be run concurrently in the background.
196
209
  #
197
210
  # WARNING! Using the feature requres that the underlying app is not on the "free" Heroku
@@ -229,23 +242,15 @@ module Hatchet
229
242
  allow_run_multi!
230
243
 
231
244
  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
245
+ run_obj = Hatchet::HerokuRun.new(
246
+ command,
247
+ app: self,
248
+ retry_on_empty: options.fetch(:retry_on_empty, !ENV["HATCHET_DISABLE_EMPTY_RUN_RETRY"]),
249
+ heroku: options[:heroku],
250
+ raw: options[:raw]
251
+ ).call
252
+
253
+ yield run_obj.output, run_obj.status
249
254
  end
250
255
  run_thread.abort_on_exception = true
251
256
 
@@ -318,9 +323,25 @@ module Hatchet
318
323
  end
319
324
  end
320
325
 
321
- def before_deploy(&block)
326
+ def before_deploy(behavior = :default, &block)
322
327
  raise "block required" unless block
323
- @before_deploy = block
328
+
329
+ case behavior
330
+ when :default, :replace
331
+ if @before_deploy_array.any? && behavior == :default
332
+ STDERR.puts "Calling App#before_deploy multiple times will overwrite the contents. If you intended this: use `App#before_deploy(:replace)`"
333
+ STDERR.puts "In the future, calling this method with no arguements will default to `App#before_deploy(:append)` behavior.\n#{caller.join("\n")}"
334
+ end
335
+
336
+ @before_deploy_array.clear
337
+ @before_deploy_array << block
338
+ when :prepend
339
+ @before_deploy_array = [block] + @before_deploy_array
340
+ when :append
341
+ @before_deploy_array << block
342
+ else
343
+ raise "Unrecognized behavior: #{behavior.inspect}, valid inputs are :append, :prepend, and :replace"
344
+ end
324
345
 
325
346
  self
326
347
  end
@@ -346,14 +367,14 @@ module Hatchet
346
367
  @reaper.cycle if @app_is_setup
347
368
  end
348
369
 
349
- def in_directory(directory = self.directory)
350
- yield directory and return if @already_in_dir
370
+ def in_directory
371
+ yield and return if @already_in_dir
351
372
 
352
373
  Dir.mktmpdir do |tmpdir|
353
- FileUtils.cp_r("#{directory}/.", "#{tmpdir}/.")
374
+ FileUtils.cp_r("#{original_source_code_directory}/.", "#{tmpdir}/.")
354
375
  Dir.chdir(tmpdir) do
355
376
  @already_in_dir = true
356
- yield directory
377
+ yield
357
378
  @already_in_dir = false
358
379
  end
359
380
  end
@@ -387,9 +408,11 @@ module Hatchet
387
408
 
388
409
  def deploy(&block)
389
410
  in_directory do
390
- in_dir_setup!
391
- self.push_with_retry!
392
- block.call(self, api_rate_limit.call, output) if block_given?
411
+ annotate_failures do
412
+ in_dir_setup!
413
+ self.push_with_retry!
414
+ block.call(self, api_rate_limit.call, output) if block_given?
415
+ end
393
416
  end
394
417
  ensure
395
418
  self.teardown! if block_given?
@@ -423,21 +446,21 @@ module Hatchet
423
446
  end
424
447
 
425
448
  def api_key
426
- @api_key ||= ENV['HEROKU_API_KEY'] || `heroku auth:token`.chomp
449
+ @api_key ||= ENV['HEROKU_API_KEY'] ||= `heroku auth:token`.chomp
427
450
  end
428
451
 
429
452
  def heroku
430
453
  raise "Not supported, use `platform_api` instead."
431
454
  end
432
455
 
433
- def run_ci(timeout: 300, &block)
456
+ def run_ci(timeout: 900, &block)
434
457
  in_directory do
435
458
  max_retries_count.times.retry do
436
459
  result = create_pipeline
437
460
  @pipeline_id = result["id"]
438
461
  end
439
462
 
440
- # when the CI run finishes, the associated ephemeral app created for the test run internally gets removed almost immediately
463
+ # When the CI run finishes, the associated ephemeral app created for the test run internally gets removed almost immediately
441
464
  # the system then sees a pipeline with no apps, and deletes it, also almost immediately
442
465
  # that would, with bad timing, mean our test run info poll in wait! would 403, and/or the delete_pipeline at the end
443
466
  # that's why we create an app explictly (or maybe it already exists), and then associate it with with the pipeline
@@ -495,6 +518,9 @@ module Hatchet
495
518
 
496
519
  def delete_pipeline(pipeline_id)
497
520
  api_rate_limit.call.pipeline.delete(pipeline_id)
521
+ rescue Excon::Error::Forbidden
522
+ warn "Error deleting pipeline id: #{pipeline_id.inspect}, status: 403"
523
+ # Means the pipeline likely doesn't exist, not sure why though
498
524
  end
499
525
 
500
526
  def platform_api
@@ -534,14 +560,17 @@ module Hatchet
534
560
  end
535
561
 
536
562
  private def call_before_deploy
537
- return unless @before_deploy
538
- raise "before_deploy: #{@before_deploy.inspect} must respond to :call" unless @before_deploy.respond_to?(:call)
539
- raise "before_deploy: #{@before_deploy.inspect} must respond to :arity" unless @before_deploy.respond_to?(:arity)
563
+ return unless @before_deploy_array.any?
540
564
 
541
- if @before_deploy.arity == 1
542
- @before_deploy.call(self)
543
- else
544
- @before_deploy.call
565
+ @before_deploy_array.each do |block|
566
+ raise "before_deploy: #{block.inspect} must respond to :call" unless block.respond_to?(:call)
567
+ raise "before_deploy: #{block.inspect} must respond to :arity" unless block.respond_to?(:arity)
568
+
569
+ if block.arity == 1
570
+ block.call(self)
571
+ else
572
+ block.call
573
+ end
545
574
  end
546
575
 
547
576
  commit! if needs_commit?
@@ -5,25 +5,28 @@ module Hatchet
5
5
  "https://git.heroku.com/#{name}.git"
6
6
  end
7
7
 
8
-
9
8
  def push_without_retry!
10
9
  output = ""
11
10
 
12
11
  ShellThrottle.new(platform_api: @platform_api).call do
13
12
  output = git_push_heroku_yall
14
13
  rescue FailedDeploy => e
15
- if e.output.match?(/reached the API rate limit/)
14
+ case e.output
15
+ when /reached the API rate limit/, /429 Too Many Requests/
16
16
  throw(:throttle)
17
- elsif @allow_failure
18
- output = e.output
19
17
  else
20
- raise e
18
+ raise e unless @allow_failure
19
+ output = e.output
21
20
  end
22
21
  end
23
22
 
24
23
  return output
25
24
  end
26
25
 
26
+ def releases
27
+ platform_api.release.list(name)
28
+ end
29
+
27
30
  private def git_push_heroku_yall
28
31
  output = `git push #{git_repo} HEAD:main 2>&1`
29
32
 
@@ -31,7 +34,6 @@ module Hatchet
31
34
  raise FailedDeployError.new(self, "Buildpack: #{@buildpack.inspect}\nRepo: #{git_repo}", output: output)
32
35
  end
33
36
 
34
- releases = platform_api.release.list(name)
35
37
  if releases.last["status"] == "failed"
36
38
  commit! # An empty commit allows us to deploy again
37
39
  raise FailedReleaseError.new(self, "Buildpack: #{@buildpack.inspect}\nRepo: #{git_repo}", output: output)
@@ -0,0 +1,96 @@
1
+ module Hatchet
2
+ # Used for running Heroku commands
3
+ #
4
+ # Example:
5
+ #
6
+ # run_obj = HerokuRun.new("ruby -v", app: app).call
7
+ # puts run_obj.output #=> "ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux]"
8
+ # puts run_obj.status.success? #=> true
9
+ #
10
+ # There's a bug in specs sometimes where App#run will return an empty
11
+ # value. When that's detected then the command will be re-run. This can be
12
+ # optionally disabled by setting `retry_on_empty: false` if you're expecting
13
+ # the command to be empty.
14
+ #
15
+ class HerokuRun
16
+ attr_reader :command
17
+
18
+ def initialize(
19
+ command,
20
+ app: ,
21
+ heroku: {},
22
+ retry_on_empty: !ENV["HATCHET_DISABLE_EMPTY_RUN_RETRY"],
23
+ raw: false,
24
+ stderr: $stderr)
25
+
26
+ @raw = raw
27
+ @app = app
28
+ @command = build_heroku_command(command, heroku || {})
29
+ @retry_on_empty = retry_on_empty
30
+ @stderr = stderr
31
+ @output = ""
32
+ @status = nil
33
+ @empty_fail_count = 0
34
+ end
35
+
36
+ def output
37
+ raise "You must run `call` on this object first" unless @status
38
+ @output
39
+ end
40
+
41
+ def status
42
+ raise "You must run `call` on this object first" unless @status
43
+ @status
44
+ end
45
+
46
+ def call
47
+ loop do
48
+ execute!
49
+
50
+ break unless output.empty?
51
+ break unless @retry_on_empty
52
+
53
+ @empty_fail_count += 1
54
+
55
+ break if @empty_fail_count >= 3
56
+
57
+ message = String.new("Empty output from command #{@command}, retrying the command.")
58
+ message << "\nTo disable pass in `retry_on_empty: false` or set HATCHET_DISABLE_EMPTY_RUN_RETRY=1 globally"
59
+ message << "\nfailed_count: #{@empty_fail_count}"
60
+ message << "\nreleases: #{@app.releases}"
61
+ message << "\n#{caller.join("\n")}"
62
+ @stderr.puts message
63
+ end
64
+
65
+ self
66
+ end
67
+
68
+ private def execute!
69
+ ShellThrottle.new(platform_api: @app.platform_api).call do |throttle|
70
+ run_shell!
71
+ throw(:throttle) if output.match?(/reached the API rate limit/)
72
+ end
73
+ end
74
+
75
+ private def run_shell!
76
+ @output = `#{@command}`
77
+ @status = $?
78
+ end
79
+
80
+ private def build_heroku_command(command, options = {})
81
+ command = command.shellescape unless @raw
82
+
83
+ default_options = { "app" => @app.name, "exit-code" => nil }
84
+ heroku_options_array = (default_options.merge(options)).map do |k,v|
85
+ # This was a bad interface decision
86
+ next if v == Hatchet::App::SkipDefaultOption # for forcefully removing e.g. --exit-code, a user can pass this
87
+
88
+ arg = "--#{k.to_s.shellescape}"
89
+ arg << "=#{v.to_s.shellescape}" unless v.nil? # nil means we include the option without an argument
90
+ arg
91
+ end
92
+
93
+ "heroku run #{heroku_options_array.compact.join(' ')} -- #{command}"
94
+ end
95
+ end
96
+ end
@@ -1,7 +1,7 @@
1
1
  module Hatchet
2
2
  class FailedTestError < StandardError
3
3
  def initialize(app, output)
4
- msg = "Could not run tests on pipeline id: '#{app.pipeline_id}' (#{app.repo_name}) at path: '#{app.directory}'\n" <<
4
+ msg = "Could not run tests on pipeline id: '#{app.pipeline_id}' (#{app.repo_name}) at path: '#{app.original_source_code_directory}'\n" <<
5
5
  " if this was expected add `allow_failure: true` to your hatchet initialization hash.\n" <<
6
6
  "output:\n" <<
7
7
  "#{output}"
@@ -140,8 +140,9 @@ module Hatchet
140
140
  end
141
141
  end
142
142
  rescue Timeout::Error
143
- puts "Timed out status: #{@status}, timeout: #{@timeout}"
144
- raise FailedTestError.new(self.app, self.output) unless app.allow_failure?
143
+ message = "Timed out status: #{@status}, timeout: #{@timeout}, app: #{app.name}"
144
+ puts message
145
+ raise FailedTestError.new(self.app, "#{message}, output:\n#{self.output}") unless app.allow_failure?
145
146
  yield self
146
147
  return self
147
148
  end
@@ -1,3 +1,3 @@
1
1
  module Hatchet
2
- VERSION = "7.2.0"
2
+ VERSION = "7.3.0"
3
3
  end
@@ -1,6 +1,20 @@
1
1
  require("spec_helper")
2
2
 
3
3
  describe "AppTest" do
4
+ it "annotates rspec expectation failures" do
5
+ app = Hatchet::Runner.new("default_ruby")
6
+ error = nil
7
+ begin
8
+ app.annotate_failures do
9
+ expect(true).to eq(false)
10
+ end
11
+ rescue RSpec::Expectations::ExpectationNotMetError => e
12
+ error = e
13
+ end
14
+
15
+ expect(error.message).to include(app.name)
16
+ end
17
+
4
18
  it "does not modify local files by mistake" do
5
19
  Dir.mktmpdir do |dir_1|
6
20
  app = Hatchet::Runner.new(dir_1)
@@ -23,26 +37,6 @@ describe "AppTest" do
23
37
  end
24
38
  end
25
39
 
26
- it "rate throttles `git push` " do
27
- app = Hatchet::GitApp.new("default_ruby")
28
- def app.git_push_heroku_yall
29
- @_git_push_heroku_yall_call_count ||= 0
30
- @_git_push_heroku_yall_call_count += 1
31
- if @_git_push_heroku_yall_call_count >= 2
32
- "Success"
33
- else
34
- raise Hatchet::App::FailedDeployError.new(self, "message", output: "Your account reached the API rate limit Please wait a few minutes before making new requests")
35
- end
36
- end
37
-
38
- def app.sleep_called?; @sleep_called; end
39
-
40
- def app.what_is_git_push_heroku_yall_call_count; @_git_push_heroku_yall_call_count; end
41
- app.push_without_retry!
42
-
43
- expect(app.what_is_git_push_heroku_yall_call_count).to be(2)
44
- end
45
-
46
40
  it "calls reaper if cannot create an app" do
47
41
  app = Hatchet::App.new("default_ruby", buildpacks: [:default])
48
42
  def app.heroku_api_create_app(*args); raise StandardError.new("made you look"); end
@@ -102,22 +96,75 @@ describe "AppTest" do
102
96
  expect(app_update_info["maintenance"]).to be_truthy
103
97
  end
104
98
 
105
- it "before deploy" do
106
- @called = false
107
- @dir = false
108
- app = Hatchet::App.new("default_ruby")
109
- def app.push_with_retry!
110
- # do nothing
99
+ describe "before deploy" do
100
+ it "dir" do
101
+ @called = false
102
+ @dir = false
103
+ app = Hatchet::App.new("default_ruby")
104
+ def app.push_with_retry!
105
+ # do nothing
106
+ end
107
+ app.before_deploy do
108
+ @called = true
109
+ @dir = Dir.pwd
110
+ end
111
+ app.deploy do
112
+ expect(@called).to eq(true)
113
+ expect(@dir).to eq(Dir.pwd)
114
+ end
115
+ expect(@dir).to_not eq(Dir.pwd)
111
116
  end
112
- app.before_deploy do
113
- @called = true
114
- @dir = Dir.pwd
117
+
118
+ it "prepend" do
119
+ @value = ""
120
+ app = Hatchet::App.new("default_ruby")
121
+ def app.push_with_retry!; end
122
+ app.before_deploy do
123
+ @value << "there"
124
+ end
125
+
126
+ app.before_deploy(:prepend) do
127
+ @value << "hello "
128
+ end
129
+ app.deploy do
130
+ end
131
+
132
+ expect(@value).to eq("hello there")
115
133
  end
116
- app.deploy do
117
- expect(@called).to eq(true)
118
- expect(@dir).to eq(Dir.pwd)
134
+
135
+ it "append" do
136
+ @value = ""
137
+ app = Hatchet::App.new("default_ruby")
138
+ def app.push_with_retry!; end
139
+ app.before_deploy do
140
+ @value << "there"
141
+ end
142
+
143
+ app.before_deploy(:append) do
144
+ @value << " hello"
145
+ end
146
+ app.deploy do
147
+ end
148
+
149
+ expect(@value).to eq("there hello")
150
+ end
151
+
152
+ it "replace" do
153
+ @value = ""
154
+ app = Hatchet::App.new("default_ruby")
155
+ def app.push_with_retry!; end
156
+ app.before_deploy do
157
+ @value << "there"
158
+ end
159
+
160
+ app.before_deploy(:replace) do
161
+ @value << "hello"
162
+ end
163
+ app.deploy do
164
+ end
165
+
166
+ expect(@value).to eq("hello")
119
167
  end
120
- expect(@dir).to_not eq(Dir.pwd)
121
168
  end
122
169
 
123
170
  it "auto commits code" do
@@ -14,7 +14,8 @@ RSpec.configure do |config|
14
14
  end
15
15
  end
16
16
 
17
- ENV['HATCHET_BUILDPACK_BRANCH'] = "master"
17
+ ENV['HATCHET_BUILDPACK_BASE'] = "https://github.com/heroku/heroku-buildpack-ruby.git"
18
+ ENV['HATCHET_BUILDPACK_BRANCH'] = "main"
18
19
 
19
20
  require 'parallel_tests/test/runtime_logger' if ENV['RECORD_RUNTIME']
20
21
 
@@ -0,0 +1,115 @@
1
+ require "spec_helper"
2
+
3
+ describe "HerokuRun" do
4
+ def fake_app
5
+ app = Object.new
6
+ def app.name; "fake_app"; end
7
+ app
8
+ end
9
+
10
+ describe "options" do
11
+ it "escapes by default" do
12
+ run_obj = Hatchet::HerokuRun.new("ruby -v", app: fake_app)
13
+ expect(run_obj.command).to eq("heroku run --app=fake_app --exit-code -- ruby\\ -v")
14
+ end
15
+
16
+ it "escapes by default" do
17
+ run_obj = Hatchet::HerokuRun.new("ruby -v", app: fake_app, heroku: { "exit-code" => Hatchet::App::SkipDefaultOption })
18
+ expect(run_obj.command).to eq("heroku run --app=fake_app -- ruby\\ -v")
19
+ end
20
+
21
+ it "allows setting switch values by default" do
22
+ run_obj = Hatchet::HerokuRun.new("ruby -v", app: fake_app, heroku: { "no-tty" => nil })
23
+ expect(run_obj.command).to eq("heroku run --app=fake_app --exit-code --no-tty -- ruby\\ -v")
24
+ end
25
+
26
+ it "can be used to pass env vars" do
27
+ run_obj = Hatchet::HerokuRun.new("ruby -v", app: fake_app, heroku: { "env" => "HELLO=ohai;NAME=world" })
28
+ expect(run_obj.command).to eq("heroku run --app=fake_app --exit-code --env=HELLO\\=ohai\\;NAME\\=world -- ruby\\ -v")
29
+ end
30
+
31
+
32
+ it "lets me use raw values" do
33
+ run_obj = Hatchet::HerokuRun.new("ruby -v", app: fake_app, raw: true )
34
+ expect(run_obj.command).to eq("heroku run --app=fake_app --exit-code -- ruby -v")
35
+ end
36
+ end
37
+
38
+ describe "retry on empty" do
39
+ before(:all) do
40
+ @app = Hatchet::Runner.new("default_ruby")
41
+ @app.setup!
42
+ end
43
+
44
+ after(:all) do
45
+ @app.teardown!
46
+ end
47
+
48
+ it "retries 3 times on empty result" do
49
+ stderr = StringIO.new
50
+ run_obj = Hatchet::HerokuRun.new("ruby -v", app: @app, stderr: stderr)
51
+
52
+ def run_obj.run_shell!
53
+ @output = ""
54
+ @status = Object.new
55
+ end
56
+
57
+ run_obj.call
58
+
59
+ expect(run_obj.instance_variable_get(:@empty_fail_count)).to eq(3)
60
+ expect(stderr.string).to include("retrying the command.")
61
+ end
62
+
63
+ it "retries 0 times on NON empty result" do
64
+ stderr = StringIO.new
65
+ run_obj = Hatchet::HerokuRun.new("ruby -v", app: @app, stderr: stderr)
66
+
67
+ def run_obj.run_shell!
68
+ @output = "not empty"
69
+ @status = Object.new
70
+ end
71
+
72
+ run_obj.call
73
+
74
+ expect(run_obj.instance_variable_get(:@empty_fail_count)).to eq(0)
75
+ expect(run_obj.output).to eq("not empty")
76
+ end
77
+
78
+ it "retries 0 times on empty result when disabled" do
79
+ stderr = StringIO.new
80
+ run_obj = Hatchet::HerokuRun.new("ruby -v", app: @app, stderr: stderr, retry_on_empty: false)
81
+
82
+ def run_obj.run_shell!
83
+ @output = ""
84
+ @status = Object.new
85
+ end
86
+
87
+ run_obj.call
88
+
89
+ expect(run_obj.instance_variable_get(:@empty_fail_count)).to eq(0)
90
+ expect(stderr.string).to_not include("retrying the command.")
91
+ end
92
+
93
+ it "retries 0 times on empty result when disabled via ENV var" do
94
+ begin
95
+ original_env = ENV["HATCHET_DISABLE_EMPTY_RUN_RETRY"]
96
+ ENV["HATCHET_DISABLE_EMPTY_RUN_RETRY"] = "1"
97
+ stderr = StringIO.new
98
+ run_obj = Hatchet::HerokuRun.new("ruby -v", app: @app, stderr: stderr)
99
+
100
+ def run_obj.run_shell!
101
+ @output = ""
102
+ @status = Object.new
103
+ end
104
+
105
+ run_obj.call
106
+
107
+ expect(run_obj.instance_variable_get(:@empty_fail_count)).to eq(0)
108
+ expect(stderr.string).to_not include("retrying the command.")
109
+ ensure
110
+ ENV["HATCHET_DISABLE_EMPTY_RUN_RETRY"] = original_env
111
+ end
112
+ end
113
+ end
114
+ end
115
+
@@ -0,0 +1,94 @@
1
+ require "spec_helper"
2
+
3
+ describe "ShellThrottle" do
4
+ before(:each) do
5
+ throttle = PlatformAPI.rate_throttle
6
+ def throttle.sleep(value)
7
+ # No sleep, faster tests
8
+ end
9
+ end
10
+
11
+ after(:each) do
12
+ throttle = PlatformAPI.rate_throttle
13
+ def throttle.sleep(value)
14
+ super # Unstub
15
+ end
16
+ end
17
+
18
+ describe "class unit test" do
19
+ before(:all) do
20
+ @platform_api = Hatchet::Runner.new("default_ruby").platform_api
21
+ end
22
+
23
+ it "throttles when throw is called" do
24
+ @count = 0
25
+ Hatchet::ShellThrottle.new(platform_api: @platform_api).call do
26
+ @count += 1
27
+ if @count >= 2
28
+ # No throttle
29
+ else
30
+ throw(:throttle)
31
+ end
32
+ end
33
+ expect(@count).to eq(2)
34
+ end
35
+
36
+ it "does not throttle when throw is NOT called" do
37
+ @count = 0
38
+ Hatchet::ShellThrottle.new(platform_api: @platform_api).call do
39
+ @count += 1
40
+ end
41
+ expect(@count).to eq(1)
42
+ end
43
+ end
44
+
45
+ describe "git push throttle" do
46
+ it "rate throttles `git push` " do
47
+ app = Hatchet::GitApp.new("default_ruby")
48
+ def app.git_push_heroku_yall
49
+ @_git_push_heroku_yall_call_count ||= 0
50
+ @_git_push_heroku_yall_call_count += 1
51
+ if @_git_push_heroku_yall_call_count >= 2
52
+ "Success"
53
+ else
54
+ raise Hatchet::App::FailedDeployError.new(
55
+ self,
56
+ "message",
57
+ output: "Your account reached the API rate limit Please wait a few minutes before making new requests"
58
+ )
59
+ end
60
+ end
61
+
62
+ def app.sleep_called?; @sleep_called; end
63
+ def app.what_is_git_push_heroku_yall_call_count; @_git_push_heroku_yall_call_count; end
64
+
65
+ app.push_without_retry!
66
+
67
+ expect(app.what_is_git_push_heroku_yall_call_count).to be(2)
68
+ end
69
+
70
+ it "rate throttles `git push` with different output" do
71
+ app = Hatchet::GitApp.new("default_ruby")
72
+ def app.git_push_heroku_yall
73
+ @_git_push_heroku_yall_call_count ||= 0
74
+ @_git_push_heroku_yall_call_count += 1
75
+ if @_git_push_heroku_yall_call_count >= 2
76
+ "Success"
77
+ else
78
+ raise Hatchet::App::FailedDeployError.new(
79
+ self,
80
+ "message",
81
+ output: "RPC failed; HTTP 429 curl 22 The requested URL returned error: 429 Too Many Requests"
82
+ )
83
+ end
84
+ end
85
+
86
+ def app.sleep_called?; @sleep_called; end
87
+ def app.what_is_git_push_heroku_yall_call_count; @_git_push_heroku_yall_call_count; end
88
+
89
+ app.push_without_retry!
90
+
91
+ expect(app.what_is_git_push_heroku_yall_call_count).to be(2)
92
+ end
93
+ end
94
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heroku_hatchet
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.2.0
4
+ version: 7.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Schneeman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-16 00:00:00.000000000 Z
11
+ date: 2020-09-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: platform-api
@@ -192,6 +192,7 @@ files:
192
192
  - lib/hatchet/app.rb
193
193
  - lib/hatchet/config.rb
194
194
  - lib/hatchet/git_app.rb
195
+ - lib/hatchet/heroku_run.rb
195
196
  - lib/hatchet/init_project.rb
196
197
  - lib/hatchet/reaper.rb
197
198
  - lib/hatchet/reaper/app_age.rb
@@ -219,9 +220,10 @@ files:
219
220
  - spec/hatchet/local_repo_spec.rb
220
221
  - spec/hatchet/lock_spec.rb
221
222
  - spec/spec_helper.rb
223
+ - spec/unit/heroku_run_spec.rb
222
224
  - spec/unit/init_spec.rb
223
225
  - spec/unit/reaper_spec.rb
224
- - spec/unit/shell_throttle.rb
226
+ - spec/unit/shell_throttle_spec.rb
225
227
  - tmp/parallel_runtime_test.log
226
228
  homepage: https://github.com/heroku/hatchet
227
229
  licenses:
@@ -257,6 +259,7 @@ test_files:
257
259
  - spec/hatchet/local_repo_spec.rb
258
260
  - spec/hatchet/lock_spec.rb
259
261
  - spec/spec_helper.rb
262
+ - spec/unit/heroku_run_spec.rb
260
263
  - spec/unit/init_spec.rb
261
264
  - spec/unit/reaper_spec.rb
262
- - spec/unit/shell_throttle.rb
265
+ - spec/unit/shell_throttle_spec.rb
@@ -1,28 +0,0 @@
1
- require "spec_helper"
2
-
3
- describe "ShellThrottle" do
4
- it "throttles when throw is called" do
5
- platform_api = Hatchet::Runner.new("default_ruby").platform_api
6
-
7
- @count = 0
8
- Hatchet::ShellThrottle.new(platform_api: platform_api).call do
9
- @count += 1
10
- if @count >= 2
11
- # No throttle
12
- else
13
- throw(:throttle)
14
- end
15
- end
16
- expect(@count).to eq(2)
17
- end
18
-
19
- it "does not throttle when throw is NOT called" do
20
- platform_api = Hatchet::Runner.new("default_ruby").platform_api
21
-
22
- @count = 0
23
- Hatchet::ShellThrottle.new(platform_api: platform_api).call do
24
- @count += 1
25
- end
26
- expect(@count).to eq(1)
27
- end
28
- end