heroku_hatchet 7.2.0 → 7.3.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 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