heroku_hatchet 7.4.0 → 8.0.1

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: 4e33ffcf73b42d412f81525fdce9b77baa34da4083a6662cb14ddb098a0535dc
4
- data.tar.gz: 3d73f585c00dd74cd4733802af979ba75e2c04e4cb93118128fdaf6efe9bc53b
3
+ metadata.gz: f4233a137df7685f1e411471db8c53a72c9c6e77e5b9a901d0d8bb1c0147f06a
4
+ data.tar.gz: ffd93abf69b7a46ccbdda5148710e237b9c41171abef209ea56f187d7a52c501
5
5
  SHA512:
6
- metadata.gz: f9ef93379dd21e7808f30d9a2ed6179a76aa12679d5a1129a1ed394682c3e046abcc6c8b87ae8d3112edc5ff77965f90d42550be6d09a49ed89b7ad7270fdf14
7
- data.tar.gz: 6cd06944d64ca9ae96ed8b25744000de748a5235dc52fab838368b9afc75c28af6dc9c1432106a99f972b4044d94762ce573fcaef2fa91f062a68aad2000b05a
6
+ metadata.gz: 61cc153424acbdf88c61b41ef9b4206fbb1e59c171ad30fded428d40bfe2edc728cdb2db72ca95526d1d5c5225d38eacacb6794b8c811b40988cac58480b0e74
7
+ data.tar.gz: 615e586767703298020a5d2a0172b9793a681a7cc681f45e6c6e42082e9db8c4a32f6e647fb006d8e9b7d8618b6c0dace7df6109cbca20e5e7fe0c34f991ed62
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## HEAD
2
2
 
3
+ ## 8.0.1
4
+
5
+ - Bugfix: Lock and sleep and refresh API when duplicate app deletion detected (https://github.com/heroku/hatchet/pull/198)
6
+
7
+ ## 8.0.0
8
+
9
+ - Breaking change: Delete apps on teardown. Previously hatchet would delete apps lazily to help with debugging. This behavior allowed developers to inspect logs and `heroku run bash` in the event of an unexpected failure. In practice, it is rarely needed and causes accounts to retain apps indefinitely. Previously there was no cost to retaining applications, but now `basic` applications incur a charge. Change details:
10
+ - The application teardown process now deletes applications directly.
11
+ - To skip destroying applications on teardown, set `HEROKU_DEBUG_EXPENSIVE=1`. This env var will cause `App#teardown!` to skip deletion so you can introspect why one failed.
12
+ - When hatchet needs a new application, it will first delete all applications that are created at least `HATCHET_ALIVE_TTL_MINUTES` ago. If it cannot delete any applications and the account is already at `HATCHET_APP_LIMIT`, it will sleep and try again later.
13
+ - Introduce `--older-than` flag to `hatchet destroy` CLI command. For example, the command `hatchet destroy --older-than=7`will remove any apps older than the provided value (in minutes).
14
+ - Add support for GitHub Actions env vars (https://github.com/heroku/hatchet/pull/189)
15
+
3
16
  ## 7.4.0
4
17
 
5
18
  - Supports "basic" scaledown (https://github.com/heroku/hatchet/pull/193)
data/README.md CHANGED
@@ -266,16 +266,17 @@ Hatchet::Runner.new("minimal_webpacker", buildpacks: buildpacks).deploy do |app,
266
266
  expect($?.exitstatus).to eq(0)
267
267
  expect($?.success?).to be_truthy
268
268
 
269
- # In Ruby all objects except `nil` and `false` are "truthy" in this case it could also be tested using `be_true` but
270
- # it's best practice to use this test helper in rspec
269
+ # In Ruby all objects except `nil` and `false` are "truthy"
270
+ # in this case it could also be tested using `be_true` but
271
+ # it's best practice to use this `be_truthy` test helper instead
271
272
  end
272
273
  ```
273
274
 
274
- You can disable this behavior [see how to do it in the reference tests](https://github.com/heroku/hatchet/blob/master/spec/hatchet/app_spec.rb)
275
+ You can disable error on exit status behavior, [see how to do it in the reference tests](https://github.com/heroku/hatchet/blob/master/spec/hatchet/app_spec.rb)
275
276
 
276
277
  - **Escaping and raw mode:**
277
278
 
278
- By default `app.run()` will escape the input so you can safely call `app.run("cmd && cmd")` and it works as expected. But if you want to do something custom, you can enable raw mode by passing in `raw: true` [see how to do it in the reference tests](https://github.com/heroku/hatchet/blob/master/spec/hatchet/app_spec.rb)
279
+ By default `app.run()` will escape the input so you can safely call `app.run("cmd && cmd")` and it works as expected. But if you want to do something custom, you can enable raw mode by passing in `raw: true` [see how to do it in the reference tests](https://github.com/heroku/hatchet/blob/master/spec/hatchet/app_spec.rb). If you do that, you'll need to ensure your inputs are propperly shell escaped.
279
280
 
280
281
  - **Heroku options:**
281
282
 
@@ -358,21 +359,9 @@ And later:
358
359
  Destroying "hatchet-t-fd25e3626b". Hatchet app limit: 80
359
360
  ```
360
361
 
361
- By default, Hatchet does not destroy your app at the end of the test run, that way if your test failed unexpectedly if it's not destroyed yet, you can:
362
+ If an app is not deleted for some reason, it will be deleted by a future test run. Applications not deleted by calling `teardown` will be allowed to live for `HATCHET_ALIVE_TTL_MINUTES`. This behavior allows multiple test runs on the same hatchet account without one test run deleting apps that another one still needs.
362
363
 
363
- ```
364
- $ heroku run bash -a hatchet-t-bed73940a6
365
- ```
366
-
367
- And use that to debug. Hatchet deletes old apps on demand. You tell it what your limits are and it will stay within those limits:
368
-
369
- ```
370
- HATCHET_APP_LIMIT=20
371
- ```
372
-
373
- With these env vars, Hatchet will "reap" older hatchet apps when it sees there are 20 or more hatchet apps. For CI, it's recommended that you increase the `HATCHET_APP_LIMIT` to 80-100. Hatchet will mark apps as safe for deletion once they've finished, and the `teardown!` method has been called on them (it tracks this by enabling maintenance mode on apps). Hatchet only tracks its apps. Hatchet uses a regex pattern on the name of apps to see which ones it can manage. If your account has reached the maximum number of global Heroku apps, you'll need to remove some manually.
374
-
375
- If an app is not marked as being in maintenance mode for some reason, it can be deleted, but only after it has been allowed to live for some time. This behavior is configured by the `HATCHET_ALIVE_TTL_MINUTES` env var. For example, if you set it for `7`, Hatchet will ensure that any apps that are not marked as being in maintenance mode are allowed to live for at least seven minutes. This should give the app time to finish the test's execution, so it is not deleted mid-deploy. When this deletion happens, you'll see a warning in your output. It could indicate you're not properly cleaning up and calling `teardown!` on some of your apps, or it could mean that you're attempting to execute more tests concurrently than your `HATCHET_APP_LIMIT` allows. This deletion-mid-test behavior might otherwise be triggered if you have multiple CI runs executing at the same time.
364
+ For example, if you set `HATCHET_ALIVE_TTL_MINUTES=7`, Hatchet will ensure that any apps that are older than 7 minutes will be deleted. If all of your tests finish in under 7 minutes then apps won't be deleted mid-deploy. When apps are deleted, you'll see a warning in your output. It could indicate you're not properly cleaning up and calling `teardown!` on some of your apps, or it could mean that you're attempting to execute more tests concurrently than your `HATCHET_APP_LIMIT` allows. Or that something else prevented apps from getting deleted on teardown, such as a Heroku API outage.
376
365
 
377
366
  It's recommended you don't use your personal Heroku API key for running tests on a CI server since the hatchet apps count against your account maximum limits. Running tests using your account locally is fine for debugging one or two tests.
378
367
 
@@ -684,7 +673,9 @@ end
684
673
  - `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.
685
674
  - `app.push!`: Push code to your Heroku app. It can be used inside of a `deploy` block to re-deploy.
686
675
  - `app.run_ci`: Runs Heroku CI against the app returns a TestRun object in the block
687
- - `app.teardown!`: This method is called automatically when using `app.deploy` in block mode after the deploy block finishes. When called it will clean up resources, mark the app as being finished (by setting `{"maintenance" => true}` on the app) so that the reaper knows it is safe to delete later. Here is an example of a test that creates and deploys an app manually, then later tears it down manually. If you deploy an application without calling `teardown!` then Hatchet will not know it is safe to delete and may keep it around for much longer than required for the test to finish.
676
+ - `app.teardown!`: This method is called automatically when using `app.deploy` in block mode after the deploy block finishes. When called it will delete the application.
677
+
678
+ Here is an example of a test that creates and deploys an app manually (without a block), then later tears it down manually. If you deploy an application without calling `teardown!` then hatchet will leak apps.
688
679
 
689
680
  ```ruby
690
681
  before(:each) do
@@ -693,7 +684,7 @@ before(:each) do
693
684
  end
694
685
 
695
686
  after(:each) do
696
- @app.teardown! if @app
687
+ @app&.teardown!
697
688
  end
698
689
 
699
690
  it "uses ruby" do
@@ -718,6 +709,7 @@ HATCHET_ALIVE_TTL_MINUTES=7
718
709
 
719
710
  # HATCHET_RUN_MULTI=1 # WARNING: Setting this env var will incur charges against your account. To use this env var you must also enable `HATCHET_EXPENSIVE_MODE`
720
711
  # HATCHET_EXPENSIVE_MODE=1 # WARNING: Do not set this environment variable unless you're okay with possibly large bills
712
+ # HEROKU_DEBUG_EXPENSIVE=1 # WARNING: This will prevent apps from being cleaned up automatically and you will be billed for any apps left running
721
713
  ```
722
714
 
723
715
  > The syntax to set an env var in Ruby is `ENV["HATCHET_RETRIES"] = "2"` all env vars are strings.
@@ -725,12 +717,13 @@ HATCHET_ALIVE_TTL_MINUTES=7
725
717
  - `HATCHET_BUILDPACK_BASE`: This is the URL where Hatchet can find your buildpack. It must be public for Heroku to be able to use your buildpack.
726
718
  - `HATCHET_BUILDPACK_BRANCH`: By default, Hatchet will use your current git branch name. If, for some reason, git is not available or you want to manually specify it like `ENV["HATCHET_BUILDPACK_BRANCH'] = ENV[`MY_CI_BRANCH`]` then you can.
727
719
  - `HATCHET_RETRIES` If the `ENV['HATCHET_RETRIES']` is set to a number, deploys are expected to work and automatically retry that number of times. Due to testing using a network and random failures, setting this value to `3` retries seems to work well. If an app cannot be deployed within its allotted number of retries, an error will be raised. The downside of a larger number is that your suite will keep running for much longer when there are legitimate failures.
728
- - `HATCHET_APP_LIMIT`: The maximum number of **hatchet** apps that Hatchet will allow in the given account before running the reaper. For local execution, keep this low as you don't want your account dominated by hatchet apps. For CI, you want it to be much larger, 80-100 since it's not competing with non-hatchet apps. Your test runner account needs to be a dedicated account.
720
+ - `HATCHET_APP_LIMIT`: The maximum number of applications that hatchet is allowed to utilize on your account. If you hit this limit mid-test then Hatchet will need to wait until other tests finish and delete their apps. Keep in mind that you may have several concurrent CI executions on the same account competing for the same limited number of apps.
729
721
  - `HEROKU_API_KEY`: The API key of your test account user. If you run locally without this set, it will use your personal credentials.
730
722
  - `HEROKU_API_USER`: The email address of your user account. If you run locally without this set, it will use your personal credentials.
731
723
  - `HATCHET_DEFAULT_STACK`: The default Heroku stack to be used when an explicit `stack` is not passed to `App.new`. If this is not set, apps will instead use Heroku platform's [default stack](https://devcenter.heroku.com/articles/stack#default-stack).
732
724
  - `HATCHET_RUN_MULTI`: If enabled, this will scale up deployed apps to "standard-1x" once deployed instead of running on the free tier. This enables the `run_multi` method capability, however scaling up is not free. WARNING: Setting this env var will incur charges to your Heroku account. We recommended never to enable this setting unless you work for Heroku. To use this you must also set `HATCHET_EXPENSIVE_MODE=1`
733
725
  - `HATCHET_EXPENSIVE_MODE`: This is intended to be a "safety" environment variable. If it is not set, Hatchet will prevent you from using the `run_multi: true` setting or the `HATCHET_RUN_MULTI` environment variables. There are still ways to incur charges without this feature, but unless you're absolutely confident your test setup will not leave "orphan" apps that are billing you, do not enable this setting. Even then, only set this value if you work for Heroku. To recap WARNING: setting this is expensive.
726
+ - `HEROKU_DEBUG_EXPENSIVE`: If set, hatchet will not delete applications on teardown. This can be used to introspect build logs or run `heroku run bash` on the application created for the failed test. Note that if another hatchet process runs after `HATCHET_ALIVE_TTL_MINUTES` then it will delete your applications.
734
727
 
735
728
  ## Basic
736
729
 
@@ -1037,7 +1030,7 @@ Ruby is full of multitudes, this isn't even close to being exhaustive, just enou
1037
1030
  Hatchet has a CLI for installing and maintaining external repos you're
1038
1031
  using to test against. If you have Hatchet installed as a gem run
1039
1032
 
1040
- $ Hatchet --help
1033
+ $ hatchet --help
1041
1034
 
1042
1035
  For more info on commands. If you're using the source code you can run
1043
1036
  the command by going to the source code directory and running:
data/bin/hatchet CHANGED
@@ -106,20 +106,31 @@ class HatchetCLI < Thor
106
106
  end
107
107
  end
108
108
 
109
- desc "destroy [NAME]", "Deletes applications"
110
- option :all, :type => :boolean
111
- def destroy(name=nil)
109
+ desc "destroy", "Deletes application(s)"
110
+ option :all, type: :boolean, desc: "Delete ALL hatchet apps"
111
+ option :older_than, type: :numeric, desc: "Delete all hatchet apps older than N minutes"
112
+ def destroy
112
113
  api_key = ENV['HEROKU_API_KEY'] || `heroku auth:token`.chomp
113
114
  platform_api = PlatformAPI.connect_oauth(api_key, cache: Moneta.new(:Null))
114
115
  api_rate_limit = ApiRateLimit.new(platform_api)
115
116
  reaper = Hatchet::Reaper.new(api_rate_limit: api_rate_limit)
116
117
 
117
- if options[:all]
118
+ case
119
+ when options[:all]
120
+ puts "Destroying ALL apps"
118
121
  reaper.destroy_all
119
- elsif !name.nil?
120
- reaper.destroy_by_name(name)
122
+ puts "Done"
123
+ when options[:older_than]
124
+ minutes = options[:older_than].to_i
125
+ puts "Destroying apps older than #{minutes}m"
126
+ reaper.destroy_older_apps(
127
+ minutes: minutes,
128
+ force_refresh: true,
129
+ on_conflict: :refresh_api_and_continue
130
+ )
131
+ puts "Done"
121
132
  else
122
- raise "Must provide an app name or --all"
133
+ raise "No flags given run `hatchet help destroy` for options"
123
134
  end
124
135
  end
125
136
 
data/lib/hatchet/app.rb CHANGED
@@ -11,7 +11,7 @@ module Hatchet
11
11
  "https://github.com/heroku/heroku-buildpack-ruby.git"
12
12
  }
13
13
  }
14
- HATCHET_BUILDPACK_BRANCH = -> { ENV['HATCHET_BUILDPACK_BRANCH'] || ENV['HEROKU_TEST_RUN_BRANCH'] || Hatchet.git_branch }
14
+ HATCHET_BUILDPACK_BRANCH = -> { ENV['HATCHET_BUILDPACK_BRANCH'] || Hatchet.git_branch }
15
15
 
16
16
  attr_reader :name, :stack, :repo_name, :app_config, :buildpacks, :reaper, :max_retries_count
17
17
 
@@ -67,6 +67,7 @@ module Hatchet
67
67
  @repo_name = repo_name
68
68
  @directory = self.config.path_for_name(@repo_name)
69
69
  @name = name
70
+ @heroku_id = nil
70
71
  @stack = stack
71
72
  @debug = debug || debugging
72
73
  @allow_failure = allow_failure
@@ -82,7 +83,6 @@ module Hatchet
82
83
  end
83
84
  @run_multi_array = []
84
85
  @already_in_dir = nil
85
- @app_is_setup = nil
86
86
 
87
87
  @before_deploy_array = []
88
88
  @before_deploy_array << before_deploy if before_deploy
@@ -280,11 +280,31 @@ module Hatchet
280
280
  def create_app
281
281
  3.times.retry do
282
282
  begin
283
+ # Remove any obviously old apps first
284
+ # Try to use existing cache of apps to
285
+ # minimize API calls
286
+ @reaper.destroy_older_apps(
287
+ force_refresh: false,
288
+ on_conflict: :stop_if_under_limit,
289
+ )
283
290
  hash = { name: name, stack: stack }
284
291
  hash.delete_if { |k,v| v.nil? }
285
- heroku_api_create_app(hash)
292
+ result = heroku_api_create_app(hash)
293
+ @heroku_id = result["id"]
286
294
  rescue => e
287
- @reaper.cycle(app_exception_message: e.message)
295
+ # If we can't create an app assume
296
+ # it might be due to resource constraints
297
+ #
298
+ # Try to delete existing apps
299
+ @reaper.destroy_older_apps(
300
+ force_refresh: true,
301
+ on_conflict: :stop_if_under_limit,
302
+ )
303
+ # If we're still not under the limit, sleep a bit
304
+ # retry later.
305
+ @reaper.sleep_if_over_limit(
306
+ reason: "Could not create app #{e.message}"
307
+ )
288
308
  raise e
289
309
  end
290
310
  end
@@ -301,7 +321,7 @@ module Hatchet
301
321
 
302
322
  # creates a new heroku app via the API
303
323
  def setup!
304
- return self if @app_is_setup
324
+ return self if @heroku_id
305
325
  puts "Hatchet setup: #{name.inspect} for #{repo_name.inspect}"
306
326
  create_app
307
327
  set_labs!
@@ -309,7 +329,6 @@ module Hatchet
309
329
  api_rate_limit.call.buildpack_installation.update(name, updates: buildpack_list)
310
330
  set_config @app_config
311
331
 
312
- @app_is_setup = true
313
332
  self
314
333
  end
315
334
  alias :setup :setup!
@@ -356,16 +375,11 @@ module Hatchet
356
375
  end
357
376
 
358
377
  def teardown!
359
- return false unless @app_is_setup
360
-
361
- if @run_multi_is_setup
362
378
  @run_multi_array.map(&:join)
363
- platform_api.formation.update(name, "web", {"size" => "basic"})
364
- end
365
-
366
379
  ensure
367
- @app_update_info = platform_api.app.update(name, { maintenance: true }) if @app_is_setup
368
- @reaper.cycle if @app_is_setup
380
+ if @heroku_id && !ENV["HEROKU_DEBUG_EXPENSIVE"]
381
+ @reaper.destroy_with_log(name: @name, id: @heroku_id, reason: "teardown")
382
+ end
369
383
  end
370
384
 
371
385
  def in_directory
@@ -1,138 +1,172 @@
1
1
  require 'tmpdir'
2
2
 
3
3
  module Hatchet
4
- # This class lazilly deletes hatchet apps
4
+ # Delete apps
5
5
  #
6
- # When the reaper is called, it will check if the system has too many apps (Bassed off of HATCHET_APP_LIMIT), if so it will attempt
7
- # to delete an app to free up capacity. The goal of lazilly deleting apps is to temporarilly keep
8
- # apps around for debugging if they fail.
6
+ # Delete a single app:
9
7
  #
10
- # When App#teardown! is called on an app it is marked as being in a "finished" state by turning
11
- # on maintenance mode. The reaper will delete these in order (oldest first).
8
+ # @reaper.destroy_with_log(id: id, name: name, reason: "console")
12
9
  #
13
- # If no apps are marked as being "finished" then the reaper will check to see if the oldest app
14
- # has been alive for a long enough period for it's tests to finish (configured by HATCHET_ALIVE_TTL_MINUTES env var).
15
- # If the "unfinished" app has been alive that long it will be deleted. If not, the system will sleep for a period of time
16
- # in an attempt to allow other apps to move to be "finished".
10
+ # Clear out all apps older than HATCHET_ALIVE_TTL_MINUTES:
11
+ #
12
+ # @reaper.destroy_older_apps
13
+ #
14
+ # If you need to clear up space or wait for space to be cleared
15
+ # up then:
16
+ #
17
+ # @reaper.clean_old_or_sleep
17
18
  #
18
- # This class only limits and the number of "hatchet" apps on the system. Prevously there was a maximum of 100 apps on a
19
- # Heroku account. Now a user can belong to multiple orgs and the total number of apps they have access to is no longer
20
- # fixed at 100. Instead of hard coding a maximum limit, this failure mode is handled by forcing deletion of
21
- # an app when app creation fails. In the future we may find a better way of detecting this failure mode
22
19
  #
23
20
  # Notes:
24
21
  #
25
22
  # - The class uses a file mutex so that multiple processes on the same machine do not attempt to run the
26
23
  # reaper at the same time.
27
- # - AlreadyDeletedError will be raised if an app has already been deleted (possibly by another test run on
28
- # another machine). When this happens, the system will automatically attempt to reap another app.
29
24
  class Reaper
30
25
  class AlreadyDeletedError < StandardError; end
31
26
 
32
27
  HATCHET_APP_LIMIT = Integer(ENV["HATCHET_APP_LIMIT"] || 20) # the number of apps hatchet keeps around
33
28
  DEFAULT_REGEX = /^#{Regexp.escape(Hatchet::APP_PREFIX)}[a-f0-9]+/
29
+ TTL_MINUTES = ENV.fetch("HATCHET_ALIVE_TTL_MINUTES", "7").to_i
30
+
31
+ # Protect against parallel deletion on the same machine
32
+ # via concurrent processes
33
+ #
34
+ # Does not protect against distributed systems on different
35
+ # machines trying to delete the same applications
36
+ MUTEX_FILE = File.open(File.join(Dir.tmpdir(), "hatchet_reaper_mutex"), File::CREAT)
34
37
 
35
38
  attr_accessor :io, :hatchet_app_limit
36
39
 
37
40
  def initialize(api_rate_limit: , regex: DEFAULT_REGEX, io: STDOUT, hatchet_app_limit: HATCHET_APP_LIMIT, initial_sleep: 10)
38
- @api_rate_limit = api_rate_limit
39
- @regex = regex
40
41
  @io = io
41
- @finished_hatchet_apps = []
42
- @unfinished_hatchet_apps = []
43
- @app_count = 0
44
- @hatchet_app_limit = hatchet_app_limit
42
+ @apps = []
43
+ @regex = regex
44
+ @limit = hatchet_app_limit
45
+ @api_rate_limit = api_rate_limit
45
46
  @reaper_throttle = ReaperThrottle.new(initial_sleep: initial_sleep)
46
47
  end
47
48
 
48
- def cycle(app_exception_message: false)
49
- # Protect against parallel deletion of the same app on the same system
50
- mutex_file = File.open("#{Dir.tmpdir()}/hatchet_reaper_mutex", File::CREAT)
51
- mutex_file.flock(File::LOCK_EX)
52
-
53
- refresh_app_list if @finished_hatchet_apps.empty?
54
-
55
- # To be safe try to delete an app even if we're not over the limit
56
- # since the exception may have been caused by going over the maximum account limit
57
- if app_exception_message
49
+ def sleep_if_over_limit(reason: )
50
+ if @apps.length >= @limit
51
+ age = AppAge.new(created_at: @apps.last["created_at"], ttl_minutes: TTL_MINUTES)
52
+ @reaper_throttle.call(max_sleep: age.sleep_for_ttl) do |sleep_for|
58
53
  io.puts <<-EOM.strip_heredoc
59
- WARNING: Running reaper due to exception on app
60
- #{stats_string}
61
- Exception: #{app_exception_message}
54
+ WARNING: Hatchet app limit reached (#{@apps.length}/#{@limit})
55
+ All known apps are younger than #{TTL_MINUTES} minutes.
56
+ Sleeping (#{sleep_for}s)
57
+
58
+ Reason: #{reason}
62
59
  EOM
63
- reap_once
64
- end
65
60
 
66
- while over_limit?
67
- reap_once
61
+ sleep(sleep_for)
62
+ end
68
63
  end
69
- ensure
70
- mutex_file.close
71
64
  end
72
65
 
73
- def stats_string
74
- "total_app_count: #{@app_count}, hatchet_app_count: #{hatchet_app_count}/#{HATCHET_APP_LIMIT}, finished: #{@finished_hatchet_apps.length}, unfinished: #{@unfinished_hatchet_apps.length}"
75
- end
76
-
77
- def over_limit?
78
- hatchet_app_count > hatchet_app_limit
66
+ # Destroys apps that are older than the given argument (expecting integer minutes)
67
+ #
68
+ # This method might be running concurrently on multiple processes or multiple
69
+ # machines.
70
+ #
71
+ # When a duplicate destroy is detected we can move forward with a conflict strategy:
72
+ #
73
+ # - `:refresh_api_and_continue`: Sleep to see if another process will clean up everything for
74
+ # us and then re-populate apps from the API and continue.
75
+ # - `:stop_if_under_limit`: Sleep to allow other processes to continue. Then if apps list
76
+ # is under the limit, assume someone else is already cleaning up for us and that we're
77
+ # good to move ahead to try to create an app. Otherwise if we're at or
78
+ # over the limit sleep, refresh the app list, and continue attempting to delete apps.
79
+ def destroy_older_apps(minutes: TTL_MINUTES, force_refresh: @apps.empty?, on_conflict: :refresh_api_and_continue)
80
+ MUTEX_FILE.flock(File::LOCK_EX)
81
+
82
+ refresh_app_list if force_refresh
83
+
84
+ while app = @apps.pop
85
+ age = AppAge.new(created_at: app["created_at"], ttl_minutes: minutes)
86
+ if !age.can_delete?
87
+ @apps.push(app)
88
+ break
89
+ else
90
+ begin
91
+ destroy_with_log(
92
+ id: app["id"],
93
+ name: app["name"],
94
+ reason: "app age (#{age.in_minutes}m) is older than #{minutes}m"
95
+ )
96
+ rescue AlreadyDeletedError => e
97
+ if handle_conflict(
98
+ strategy: on_conflict,
99
+ conflict_message: e.message,
100
+ ) == :stop
101
+ break
102
+ end
103
+ end
104
+ end
105
+ end
106
+ ensure
107
+ MUTEX_FILE.flock(File::LOCK_UN)
79
108
  end
80
109
 
81
110
  # No guardrails, will delete all apps that match the hatchet namespace
82
- def destroy_all
83
- refresh_app_list
111
+ def destroy_all(force_refresh: @apps.empty?)
112
+ MUTEX_FILE.flock(File::LOCK_EX)
84
113
 
85
- (@finished_hatchet_apps + @unfinished_hatchet_apps).each do |app|
114
+ refresh_app_list if force_refresh
115
+
116
+ while app = @apps.pop
86
117
  begin
87
- destroy_with_log(name: app["name"], id: app["id"])
88
- rescue AlreadyDeletedError
89
- # Ignore, keep going
118
+ destroy_with_log(name: app["name"], id: app["id"], reason: "destroy all")
119
+ rescue AlreadyDeletedError => e
120
+ handle_conflict(
121
+ conflict_message: e.message,
122
+ strategy: :refresh_api_and_continue
123
+ )
90
124
  end
91
125
  end
126
+ ensure
127
+ MUTEX_FILE.flock(File::LOCK_UN)
92
128
  end
93
129
 
94
- private def reap_once
95
- refresh_app_list if @finished_hatchet_apps.empty?
96
-
97
- if (app = @finished_hatchet_apps.pop)
98
- destroy_with_log(name: app["name"], id: app["id"])
99
- elsif (app = @unfinished_hatchet_apps.pop)
100
- destroy_if_old_enough(app)
130
+ # Will sleep with backoff and emit a warning message
131
+ # returns :continue or :stop symbols
132
+ # :stop indicates execution should stop
133
+ private def handle_conflict(conflict_message:, strategy:)
134
+ message = String.new(<<-EOM.strip_heredoc)
135
+ WARNING: Possible race condition detected: #{conflict_message}
136
+ Hatchet app limit (#{@apps.length}/#{@limit}), using strategy #{strategy}
137
+ EOM
138
+
139
+ conflict_state = if :refresh_api_and_continue == strategy
140
+ message << "\nSleeping, refreshing app list, and continuing."
141
+ :continue
142
+ elsif :stop_if_under_limit == strategy && @apps.length >= @limit
143
+ message << "\nSleeping, refreshing app list, and continuing. Not under limit."
144
+ :continue
145
+ elsif :stop_if_under_limit == strategy
146
+ message << "\nHalting deletion of older apps. Under limit."
147
+ :stop
148
+ else
149
+ raise "No such strategy: #{strategy}, plese use :stop_if_under_limit or :refresh_api_and_continue"
101
150
  end
102
- rescue AlreadyDeletedError
103
- retry
104
- end
105
151
 
106
- # Checks to see if the given app is older than the HATCHET_ALIVE_TTL_MINUTES
107
- # if so, then the app is deleted, otherwise the reaper sleeps for a period of time after which
108
- # It can try again to delete another app. The hope is that some apps will be marked as finished
109
- # in that time
110
- private def destroy_if_old_enough(app)
111
- age = AppAge.new(
112
- created_at: app["created_at"],
113
- ttl_minutes: ENV.fetch("HATCHET_ALIVE_TTL_MINUTES", "7").to_i
114
- )
115
- if age.can_delete?
116
- io.puts "WARNING: Destroying an app without maintenance mode on, app: #{app["name"]}, app_age: #{age.in_minutes} minutes"
117
-
118
- destroy_with_log(name: app["name"], id: app["id"])
119
- else
120
- # We're not going to delete it yet, so put it back
121
- @unfinished_hatchet_apps << app
152
+ @reaper_throttle.call(max_sleep: TTL_MINUTES) do |sleep_for|
153
+ io.puts <<-EOM.strip_heredoc
154
+ #{message}
155
+ Sleeping (#{sleep_for}s)
156
+ EOM
122
157
 
123
- # Sleep, try again later
124
- @reaper_throttle.call(max_sleep: age.sleep_for_ttl) do |sleep_for|
125
- io.puts <<-EOM.strip_heredoc
126
- WARNING: Attempting to destroy an app without maintenance mode on, but it is not old enough. app: #{app["name"]}, app_age: #{age.in_minutes} minutes
127
- This can happen if App#teardown! is not called on an application, which will leave it in an 'unfinished' state
128
- This can also happen if you're trying to run more tests concurrently than your currently set value for HATCHET_APP_COUNT
129
- Sleeping: #{sleep_for} seconds before trying to find another app to reap"
130
- #{stats_string}, HATCHET_ALIVE_TTL_MINUTES=#{age.ttl_minutes}
131
- EOM
158
+ sleep(sleep_for)
159
+ end
132
160
 
133
- sleep(sleep_for)
134
- end
161
+ case conflict_state
162
+ when :continue
163
+ refresh_app_list
164
+ when :stop
165
+ else
166
+ raise "Unknown state #{conflict_state}"
135
167
  end
168
+
169
+ conflict_state
136
170
  end
137
171
 
138
172
  private def get_heroku_apps
@@ -140,28 +174,15 @@ module Hatchet
140
174
  end
141
175
 
142
176
  private def refresh_app_list
143
- apps = get_heroku_apps.
177
+ @apps = get_heroku_apps.
178
+ filter {|app| app["name"].match(@regex) }.
144
179
  map {|app| app["created_at"] = DateTime.parse(app["created_at"].to_s); app }.
145
180
  sort_by { |app| app["created_at"] }.
146
181
  reverse # Ascending order, oldest is last
147
-
148
- @app_count = apps.length
149
-
150
- @finished_hatchet_apps.clear
151
- @unfinished_hatchet_apps.clear
152
- apps.each do |app|
153
- next unless app["name"].match(@regex)
154
-
155
- if app["maintenance"]
156
- @finished_hatchet_apps << app
157
- else
158
- @unfinished_hatchet_apps << app
159
- end
160
- end
161
182
  end
162
183
 
163
- private def destroy_with_log(name:, id:)
164
- message = "Destroying #{name.inspect}: #{id}, #{stats_string}"
184
+ def destroy_with_log(name:, id:, reason: )
185
+ message = "Destroying #{name.inspect}: #{id}, (#{@apps.length}/#{@limit}) reason: #{reason}"
165
186
 
166
187
  @api_rate_limit.call.app.delete(id)
167
188
 
@@ -170,19 +191,15 @@ module Hatchet
170
191
  body = e.response.body
171
192
  request_id = e.response.headers["Request-Id"]
172
193
  if body =~ /Couldn\'t find that app./
173
- io.puts "Duplicate destroy attempted #{name.inspect}: #{id}, status: 404, request_id: #{request_id}"
174
- raise AlreadyDeletedError.new
194
+ message = "Duplicate destroy attempted #{name.inspect}: #{id}, status: 404, request_id: #{request_id}"
195
+ raise AlreadyDeletedError.new(message)
175
196
  else
176
197
  raise e
177
198
  end
178
199
  rescue Excon::Error::Forbidden => e
179
200
  request_id = e.response.headers["Request-Id"]
180
- io.puts "Duplicate destroy attempted #{name.inspect}: #{id}, status: 403, request_id: #{request_id}"
181
- raise AlreadyDeletedError.new
182
- end
183
-
184
- private def hatchet_app_count
185
- @finished_hatchet_apps.length + @unfinished_hatchet_apps.length
201
+ message = "Duplicate destroy attempted #{name.inspect}: #{id}, status: 403, request_id: #{request_id}"
202
+ raise AlreadyDeletedError.new(message)
186
203
  end
187
204
  end
188
205
  end
@@ -1,3 +1,3 @@
1
1
  module Hatchet
2
- VERSION = "7.4.0"
2
+ VERSION = "8.0.1"
3
3
  end
data/lib/hatchet.rb CHANGED
@@ -21,20 +21,43 @@ require 'hatchet/api_rate_limit'
21
21
  require 'hatchet/init_project'
22
22
  require 'hatchet/heroku_run'
23
23
 
24
+ class DefaultCIBranch
25
+ def initialize(env: ENV)
26
+ @env = env
27
+ end
28
+
29
+ def call
30
+ # https://circleci.com/docs/variables
31
+ return @env['CIRCLE_BRANCH'] if @env['CIRCLE_BRANCH']
32
+ # https://docs.github.com/en/actions/learn-github-actions/environment-variables
33
+ # GITHUB_HEAD_REF is provided for PRs, but blank for branch actions.
34
+ return @env['GITHUB_HEAD_REF'] if @env['GITHUB_HEAD_REF'] && !@env['GITHUB_HEAD_REF']&.empty?
35
+ # GITHUB_REF_NAME is incorrect on PRs (`1371/merge`), but correct for branch actions.
36
+ return @env['GITHUB_REF_NAME'] if @env['GITHUB_REF_NAME']
37
+ # https://devcenter.heroku.com/articles/heroku-ci#immutable-environment-variables
38
+ return @env['HEROKU_TEST_RUN_BRANCH'] if @env['HEROKU_TEST_RUN_BRANCH']
39
+ # TRAVIS_BRANCH works fine unless the build is a pull-request. In that case, it will contain the target branch
40
+ # not the actual pull-request branch! TRAVIS_PULL_REQUEST_BRANCH contains the correct branch but will be empty
41
+ # for push builds. See: https://docs.travis-ci.com/user/environment-variables/
42
+ return @env['TRAVIS_PULL_REQUEST_BRANCH'] if @env['TRAVIS_PULL_REQUEST_BRANCH'] && !@env['TRAVIS_PULL_REQUEST_BRANCH']&.empty?
43
+ return @env['TRAVIS_BRANCH'] if @env['TRAVIS_BRANCH']
44
+ end
45
+ end
46
+
24
47
  module Hatchet
25
48
  RETRIES = Integer(ENV['HATCHET_RETRIES'] || 1)
26
49
  Runner = Hatchet::GitApp
27
50
 
28
51
  def self.git_branch
29
- # TRAVIS_BRANCH works fine unless the build is a pull-request. In that case, it will contain the target branch
30
- # not the actual pull-request branch! TRAVIS_PULL_REQUEST_BRANCH contains the correct branch but will be empty
31
- # for push builds. See: https://docs.travis-ci.com/user/environment-variables/
32
- return ENV['TRAVIS_PULL_REQUEST_BRANCH'] if ENV['TRAVIS_PULL_REQUEST_BRANCH'] && !ENV['TRAVIS_PULL_REQUEST_BRANCH'].empty?
33
- return ENV['TRAVIS_BRANCH'] if ENV['TRAVIS_BRANCH']
52
+ branch = DefaultCIBranch.new.call
34
53
 
35
- out = `git rev-parse --abbrev-ref HEAD`.strip
36
- raise "Attempting to find current branch name. Error: Cannot describe git: #{out}" unless $?.success?
37
- out
54
+ if branch
55
+ branch
56
+ else
57
+ out = `git rev-parse --abbrev-ref HEAD`.strip
58
+ raise "Attempting to find current branch name. Error: Cannot describe git: #{out}" unless $?.success?
59
+ out
60
+ end
38
61
  end
39
62
 
40
63
  if ENV["HATCHET_DEBUG_DEADLOCK"]
@@ -43,14 +43,14 @@ describe "AppTest" do
43
43
 
44
44
  reaper = app.reaper
45
45
 
46
- def reaper.cycle(app_exception_message: ); @app_exception_message = app_exception_message; end
47
- def reaper.recorded_app_exception_message; @app_exception_message; end
46
+ def reaper.destroy_older_apps(*args, **kwargs, &block); @app_exception_message = true; end
47
+ def reaper.clean_old_was_called?; @app_exception_message; end
48
48
 
49
49
  expect {
50
50
  app.create_app
51
51
  }.to raise_error("made you look")
52
52
 
53
- expect(reaper.recorded_app_exception_message).to match("made you look")
53
+ expect(reaper.clean_old_was_called?).to be_truthy
54
54
  end
55
55
 
56
56
  it "app with default" do
@@ -82,37 +82,6 @@ describe "AppTest" do
82
82
  end
83
83
  end
84
84
 
85
- it "marks itself 'finished' when done in block mode" do
86
- app = Hatchet::Runner.new("default_ruby")
87
-
88
- def app.push_with_retry!; nil; end
89
- app.deploy do |app|
90
- expect(app.platform_api.app.info(app.name)["maintenance"]).to be_falsey
91
- end
92
-
93
- # After the app is updated, there's no guarantee it will still exist
94
- # so we cannot rely on an api call to determine maintenance mode
95
- app_update_info = app.instance_variable_get(:"@app_update_info")
96
- expect(app_update_info["name"]).to eq(app.name)
97
- expect(app_update_info["maintenance"]).to be_truthy
98
- end
99
-
100
- it "marks itself 'finished' when done in non-block mode" do
101
- app = Hatchet::Runner.new("default_ruby")
102
-
103
- def app.push_with_retry!; nil; end
104
- app.deploy
105
- expect(app.platform_api.app.info(app.name)["maintenance"]).to be_falsey
106
-
107
- app.teardown!
108
-
109
- # After the app is updated, there's no guarantee it will still exist
110
- # so we cannot rely on an api call to determine maintenance mode
111
- app_update_info = app.instance_variable_get(:"@app_update_info")
112
- expect(app_update_info["name"]).to eq(app.name)
113
- expect(app_update_info["maintenance"]).to be_truthy
114
- end
115
-
116
85
  describe "before deploy" do
117
86
  it "dir" do
118
87
  @called = false
@@ -263,16 +232,14 @@ describe "AppTest" do
263
232
  @run_count = AtomicCount.new(0)
264
233
  app = Hatchet::GitApp.new("default_ruby", run_multi: true)
265
234
  app.deploy do
266
- app.run_multi("ls") { |out| expect(out).to include("Gemfile"); @run_count.add(1) }
267
- app.run_multi("blerg -v") { |_, status| expect(status.success?).to be_falsey; @run_count.add(1) }
235
+ app.run_multi("ls") { |out| expect(out).to include("Gemfile"); @run_count.add(1); }
236
+ app.run_multi("blerg -v") { |_, status| expect(status.success?).to be_falsey; @run_count.add(1); }
268
237
  app.run_multi("ruby -v") do |out, status|
269
238
  expect(out).to include("ruby")
270
239
  expect(status.success?).to be_truthy
271
240
 
272
241
  @run_count.add(1)
273
242
  end
274
-
275
- expect(app.platform_api.formation.list(app.name).detect {|ps| ps["type"] == "web"}["size"].downcase).to_not eq("free")
276
243
  end
277
244
 
278
245
  # After the deploy block exits `teardown!` is called
@@ -15,15 +15,7 @@ describe "CIFourTest" do
15
15
  expect(test_run.output).to match(string)
16
16
  expect(test_run.output).to match("Using rake")
17
17
  expect(test_run.output).to_not match("Installing rake")
18
-
19
- expect(app.platform_api.app.info(app.name)["maintenance"]).to be_falsey
20
18
  end
21
-
22
- # After the app is updated, there's no guarantee it will still exist
23
- # so we cannot rely on an api call to determine maintenance mode
24
- app_update_info = app.instance_variable_get(:"@app_update_info")
25
- expect(app_update_info["name"]).to eq(app.name)
26
- expect(app_update_info["maintenance"]).to be_truthy
27
19
  end
28
20
 
29
21
  it "error with bad app" do
@@ -0,0 +1,27 @@
1
+
2
+ require "spec_helper"
3
+
4
+ describe "DefaultCIBranch" do
5
+ it "doesn't error on empty env" do
6
+ out = DefaultCIBranch.new(env: {}).call
7
+ expect(out).to be_nil
8
+ end
9
+
10
+ it "GitHub PRs" do
11
+ out = DefaultCIBranch.new(env: {"GITHUB_HEAD_REF" => "iAmaPR"}).call
12
+ expect(out).to eq("iAmaPR")
13
+
14
+ out = DefaultCIBranch.new(env: {"GITHUB_HEAD_REF" => ""}).call
15
+ expect(out).to be_nil
16
+ end
17
+
18
+ it "GitHub branches" do
19
+ out = DefaultCIBranch.new(env: {"GITHUB_REF_NAME" => "iAmaBranch"}).call
20
+ expect(out).to eq("iAmaBranch")
21
+ end
22
+
23
+ it "heroku" do
24
+ out = DefaultCIBranch.new(env: {"HEROKU_TEST_RUN_BRANCH" => "iAmaBranch"}).call
25
+ expect(out).to eq("iAmaBranch")
26
+ end
27
+ end
@@ -18,7 +18,7 @@ describe "Reaper" do
18
18
  end
19
19
 
20
20
  describe "cycle" do
21
- it "does not delete anything if under the limit" do
21
+ it "does not delete anything if no old apps" do
22
22
  reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 1, io: StringIO.new)
23
23
 
24
24
  def reaper.get_heroku_apps
@@ -29,46 +29,11 @@ describe "Reaper" do
29
29
  def reaper.check_get_heroku_apps_called; @called_get_heroku_apps ; end
30
30
  def reaper.reap_once; raise "should not be called"; end
31
31
 
32
- reaper.cycle
32
+ reaper.destroy_older_apps
33
33
 
34
34
  expect(reaper.check_get_heroku_apps_called).to be_truthy
35
35
  end
36
36
 
37
- it "deletes a maintenance mode app on error" do
38
- reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 1, io: StringIO.new)
39
-
40
- def reaper.get_heroku_apps
41
- @mock_apps ||= [
42
- {"name" => "hatchet-t-unfinished", "id" => 2, "maintenance" => false, "created_at" => Time.now.to_s},
43
- {"name" => "hatchet-t-foo", "id" => 1, "maintenance" => true, "created_at" => Time.now.to_s}
44
- ]
45
- end
46
- def reaper.destroy_with_log(name: , id: )
47
- @reaper_destroy_called_with = {"name" => name, "id" => id}
48
- end
49
- def reaper.destroy_called_with; @reaper_destroy_called_with; end
50
-
51
- reaper.cycle(app_exception_message: true)
52
-
53
- expect(reaper.destroy_called_with).to eq({"name" => "hatchet-t-foo", "id" => 1})
54
- end
55
-
56
- it "deletes maintenance mode app when over limit" do
57
- reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 0, io: StringIO.new)
58
-
59
- def reaper.get_heroku_apps
60
- @mock_apps ||= [{"name" => "hatchet-t-foo", "id" => 1, "maintenance" => true, "created_at" => Time.now.to_s}]
61
- end
62
- def reaper.destroy_with_log(name: , id: )
63
- @reaper_destroy_called_with = {"name" => name, "id" => id}
64
- end
65
- def reaper.destroy_called_with; @reaper_destroy_called_with; end
66
-
67
- reaper.cycle
68
-
69
- expect(reaper.destroy_called_with).to eq({"name" => "hatchet-t-foo", "id" => 1})
70
- end
71
-
72
37
  it "deletes an old app that is past TLL" do
73
38
  reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 0, io: StringIO.new)
74
39
 
@@ -76,25 +41,30 @@ describe "Reaper" do
76
41
  two_days_ago = DateTime.now.new_offset(0) - 2
77
42
  @mock_apps ||= [{"name" => "hatchet-t-foo", "id" => 1, "maintenance" => false, "created_at" => two_days_ago.to_s }]
78
43
  end
79
- def reaper.destroy_with_log(name: , id: )
44
+ def reaper.destroy_with_log(name: , id: , reason: )
80
45
  @reaper_destroy_called_with = {"name" => name, "id" => id}
81
46
  end
82
47
  def reaper.destroy_called_with; @reaper_destroy_called_with; end
83
48
 
84
- reaper.cycle
49
+ reaper.destroy_older_apps
85
50
 
86
51
  expect(reaper.destroy_called_with).to eq({"name" => "hatchet-t-foo", "id" => 1})
87
52
  end
88
53
 
89
54
  it "sleeps, refreshes app list, and tries again when an old app is not past TTL" do
90
55
  warning = StringIO.new
91
- reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 1, initial_sleep: 0, io: warning)
56
+ reaper = Hatchet::Reaper.new(
57
+ io: warning,
58
+ initial_sleep: 0,
59
+ api_rate_limit: Object.new,
60
+ hatchet_app_limit: 0,
61
+ )
92
62
 
93
63
  def reaper.get_heroku_apps
94
64
  now = DateTime.now.new_offset(0)
95
65
  @mock_apps ||= [{"name" => "hatchet-t-foo", "id" => 1, "maintenance" => false, "created_at" => now.to_s }]
96
66
  end
97
- def reaper.destroy_with_log(name: , id: )
67
+ def reaper.destroy_with_log(name: , id: , reason: )
98
68
  @reaper_destroy_called_with = {"name" => name, "id" => id}
99
69
  end
100
70
  def reaper.destroy_called_with; @reaper_destroy_called_with; end
@@ -104,13 +74,13 @@ describe "Reaper" do
104
74
 
105
75
  def reaper.get_slept_for_val; @_slept_for; end
106
76
 
107
- reaper.cycle(app_exception_message: true)
77
+ reaper.destroy_older_apps
78
+ reaper.sleep_if_over_limit(reason: "test")
108
79
 
109
80
  expect(reaper.get_slept_for_val).to eq(0)
110
81
  expect(reaper.destroy_called_with).to eq(nil)
111
82
 
112
- expect(warning.string).to match("WARNING")
113
- expect(warning.string).to match("total_app_count: 1, hatchet_app_count: 1/#{Hatchet::Reaper::HATCHET_APP_LIMIT}, finished: 0, unfinished: 1")
83
+ expect(warning.string).to include("WARNING: Hatchet app limit reached (1/0)")
114
84
  end
115
85
  end
116
86
 
@@ -154,16 +124,4 @@ describe "Reaper" do
154
124
  end
155
125
  end
156
126
  end
157
-
158
- it "over limit" do
159
- reaper = Hatchet::Reaper.new(api_rate_limit: -> (){}, io: StringIO.new)
160
- def reaper.hatchet_app_count; Hatchet::Reaper::HATCHET_APP_LIMIT + 1; end
161
-
162
- expect(reaper.over_limit?).to be_truthy
163
-
164
- reaper = Hatchet::Reaper.new(api_rate_limit: -> (){}, io: StringIO.new)
165
- def reaper.hatchet_app_count; Hatchet::Reaper::HATCHET_APP_LIMIT - 1; end
166
-
167
- expect(reaper.over_limit?).to be_falsey
168
- end
169
127
  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.4.0
4
+ version: 8.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Schneeman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-23 00:00:00.000000000 Z
11
+ date: 2023-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: platform-api
@@ -206,6 +206,7 @@ files:
206
206
  - spec/hatchet/local_repo_spec.rb
207
207
  - spec/hatchet/lock_spec.rb
208
208
  - spec/spec_helper.rb
209
+ - spec/unit/default_ci_branch_spec.rb
209
210
  - spec/unit/heroku_run_spec.rb
210
211
  - spec/unit/init_spec.rb
211
212
  - spec/unit/reaper_spec.rb
@@ -245,6 +246,7 @@ test_files:
245
246
  - spec/hatchet/local_repo_spec.rb
246
247
  - spec/hatchet/lock_spec.rb
247
248
  - spec/spec_helper.rb
249
+ - spec/unit/default_ci_branch_spec.rb
248
250
  - spec/unit/heroku_run_spec.rb
249
251
  - spec/unit/init_spec.rb
250
252
  - spec/unit/reaper_spec.rb