heroku_hatchet 5.0.0 → 7.0.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +69 -0
  3. data/.gitignore +2 -0
  4. data/CHANGELOG.md +32 -1
  5. data/Gemfile +0 -1
  6. data/README.md +772 -205
  7. data/bin/hatchet +11 -4
  8. data/etc/ci_setup.rb +21 -15
  9. data/etc/setup_heroku.sh +0 -2
  10. data/hatchet.gemspec +4 -5
  11. data/hatchet.json +6 -2
  12. data/hatchet.lock +12 -8
  13. data/lib/hatchet.rb +1 -2
  14. data/lib/hatchet/api_rate_limit.rb +13 -24
  15. data/lib/hatchet/app.rb +159 -53
  16. data/lib/hatchet/config.rb +1 -1
  17. data/lib/hatchet/git_app.rb +27 -1
  18. data/lib/hatchet/reaper.rb +159 -56
  19. data/lib/hatchet/reaper/app_age.rb +49 -0
  20. data/lib/hatchet/reaper/reaper_throttle.rb +55 -0
  21. data/lib/hatchet/shell_throttle.rb +71 -0
  22. data/lib/hatchet/test_run.rb +16 -9
  23. data/lib/hatchet/version.rb +1 -1
  24. data/{test → repo_fixtures}/different-folder-for-checked-in-repos/default_ruby/Gemfile +0 -0
  25. data/spec/hatchet/allow_failure_git_spec.rb +40 -0
  26. data/spec/hatchet/app_spec.rb +226 -0
  27. data/spec/hatchet/ci_spec.rb +67 -0
  28. data/spec/hatchet/config_spec.rb +34 -0
  29. data/spec/hatchet/edit_repo_spec.rb +17 -0
  30. data/spec/hatchet/git_spec.rb +9 -0
  31. data/spec/hatchet/heroku_api_spec.rb +30 -0
  32. data/spec/hatchet/local_repo_spec.rb +26 -0
  33. data/spec/hatchet/lock_spec.rb +30 -0
  34. data/spec/spec_helper.rb +25 -0
  35. data/spec/unit/reaper_spec.rb +153 -0
  36. data/spec/unit/shell_throttle.rb +28 -0
  37. metadata +57 -86
  38. data/.travis.yml +0 -16
  39. data/test/fixtures/buildpacks/null-buildpack/bin/compile +0 -4
  40. data/test/fixtures/buildpacks/null-buildpack/bin/detect +0 -5
  41. data/test/fixtures/buildpacks/null-buildpack/bin/release +0 -3
  42. data/test/fixtures/buildpacks/null-buildpack/hatchet.json +0 -4
  43. data/test/fixtures/buildpacks/null-buildpack/readme.md +0 -41
  44. data/test/hatchet/allow_failure_git_test.rb +0 -16
  45. data/test/hatchet/app_test.rb +0 -96
  46. data/test/hatchet/ci_four_test.rb +0 -19
  47. data/test/hatchet/ci_test.rb +0 -11
  48. data/test/hatchet/ci_three_test.rb +0 -9
  49. data/test/hatchet/ci_too_test.rb +0 -19
  50. data/test/hatchet/config_test.rb +0 -51
  51. data/test/hatchet/edit_repo_test.rb +0 -20
  52. data/test/hatchet/git_test.rb +0 -16
  53. data/test/hatchet/heroku_api_test.rb +0 -30
  54. data/test/hatchet/labs_test.rb +0 -20
  55. data/test/hatchet/local_repo_test.rb +0 -26
  56. data/test/hatchet/lock_test.rb +0 -9
  57. data/test/hatchet/multi_cmd_runner_test.rb +0 -30
  58. data/test/test_helper.rb +0 -28
@@ -181,10 +181,11 @@ module Hatchet
181
181
 
182
182
  source_put_url = @app.create_source
183
183
  Hatchet::RETRIES.times.retry do
184
- @api_rate_limit.call
185
- Excon.put(source_put_url,
186
- expects: [200],
187
- body: File.read('slug.tgz'))
184
+ PlatformAPI.rate_throttle.call do
185
+ Excon.put(source_put_url,
186
+ expects: [200],
187
+ body: File.read('slug.tgz'))
188
+ end
188
189
  end
189
190
  end
190
191
  return @app.source_get_url
@@ -192,8 +193,11 @@ module Hatchet
192
193
 
193
194
  private
194
195
  def get_contents_or_whatever(url)
195
- @api_rate_limit.call
196
- Excon.get(url, read_timeout: @pause).body
196
+ response = PlatformAPI.rate_throttle.call do
197
+ Excon.get(url, read_timeout: @pause)
198
+ end
199
+
200
+ return response.body
197
201
  rescue Excon::Error::Timeout
198
202
  ""
199
203
  end
@@ -214,11 +218,14 @@ module Hatchet
214
218
  "Content-Type" => "application/json"
215
219
  }.merge(options[:headers] || {})
216
220
  options[:body] = JSON.generate(options[:body]) if options[:body]
221
+ options[:expects] << 429 if options[:expects]
217
222
 
218
223
  Hatchet::RETRIES.times.retry do
219
- @api_rate_limit.call
220
- connection = Excon.new("https://api.heroku.com")
221
- return connection.request(options)
224
+ PlatformAPI.rate_throttle.call do
225
+ connection = Excon.new("https://api.heroku.com")
226
+
227
+ connection.request(options)
228
+ end
222
229
  end
223
230
  end
224
231
  end
@@ -1,3 +1,3 @@
1
1
  module Hatchet
2
- VERSION = "5.0.0"
2
+ VERSION = "7.0.0"
3
3
  end
@@ -0,0 +1,40 @@
1
+ require("spec_helper")
2
+
3
+ describe "AllowFailureGitTest" do
4
+ describe "release failures" do
5
+ let(:release_fail_proc) {
6
+ Proc.new do
7
+ File.open("Procfile", "w+") do |f|
8
+ f.write <<~EOM
9
+ release: echo "failing on release" && exit 1
10
+ EOM
11
+ end
12
+ end
13
+ }
14
+
15
+ it "is marked as a failure if the release fails" do
16
+ expect {
17
+ Hatchet::GitApp.new("default_ruby", before_deploy: release_fail_proc).deploy {}
18
+ }.to(raise_error(Hatchet::App::FailedReleaseError))
19
+ end
20
+
21
+ it "works when failure is allowed" do
22
+ Hatchet::GitApp.new("default_ruby", before_deploy: release_fail_proc, allow_failure: true).deploy do |app|
23
+ expect(app.output).to match("failing on release")
24
+ end
25
+ end
26
+ end
27
+
28
+ it "allowed failure" do
29
+ Hatchet::GitApp.new("no_lockfile", allow_failure: true).deploy do |app|
30
+ expect(app.deployed?).to be_falsey
31
+ expect(app.output).to match("Gemfile.lock required")
32
+ end
33
+ end
34
+
35
+ it "failure with no flag" do
36
+ expect {
37
+ Hatchet::GitApp.new("no_lockfile").deploy {}
38
+ }.to(raise_error(Hatchet::App::FailedDeploy))
39
+ end
40
+ end
@@ -0,0 +1,226 @@
1
+ require("spec_helper")
2
+
3
+ describe "AppTest" do
4
+ it "rate throttles `git push` " do
5
+ app = Hatchet::GitApp.new("default_ruby")
6
+ def app.git_push_heroku_yall
7
+ @_git_push_heroku_yall_call_count ||= 0
8
+ @_git_push_heroku_yall_call_count += 1
9
+ if @_git_push_heroku_yall_call_count >= 2
10
+ "Success"
11
+ else
12
+ raise Hatchet::App::FailedDeployError.new(self, "message", output: "Your account reached the API rate limit Please wait a few minutes before making new requests")
13
+ end
14
+ end
15
+
16
+ def app.sleep_called?; @sleep_called; end
17
+
18
+ def app.what_is_git_push_heroku_yall_call_count; @_git_push_heroku_yall_call_count; end
19
+ app.push_without_retry!
20
+
21
+ expect(app.what_is_git_push_heroku_yall_call_count).to be(2)
22
+ end
23
+
24
+ it "calls reaper if cannot create an app" do
25
+ app = Hatchet::App.new("default_ruby", buildpacks: [:default])
26
+ def app.heroku_api_create_app(*args); raise StandardError.new("made you look"); end
27
+
28
+ reaper = app.reaper
29
+
30
+ def reaper.cycle(app_exception_message: ); @app_exception_message = app_exception_message; end
31
+ def reaper.recorded_app_exception_message; @app_exception_message; end
32
+
33
+ expect {
34
+ app.create_app
35
+ }.to raise_error("made you look")
36
+
37
+ expect(reaper.recorded_app_exception_message).to match("made you look")
38
+ end
39
+
40
+ it "app with default" do
41
+ app = Hatchet::App.new("default_ruby", buildpacks: [:default])
42
+ expect(app.buildpacks.first).to match("https://github.com/heroku/heroku-buildpack-ruby")
43
+ end
44
+
45
+ it "create app with stack" do
46
+ stack = "heroku-16"
47
+ app = Hatchet::App.new("default_ruby", stack: stack)
48
+ app.create_app
49
+ expect(app.platform_api.app.info(app.name)["build_stack"]["name"]).to eq(stack)
50
+ end
51
+
52
+ it "marks itself 'finished' when done in block mode" do
53
+ app = Hatchet::Runner.new("default_ruby")
54
+
55
+ def app.push_with_retry!; nil; end
56
+ app.deploy do |app|
57
+ expect(app.platform_api.app.info(app.name)["maintenance"]).to be_falsey
58
+ end
59
+
60
+ # After the app is updated, there's no guarantee it will still exist
61
+ # so we cannot rely on an api call to determine maintenance mode
62
+ app_update_info = app.instance_variable_get(:"@app_update_info")
63
+ expect(app_update_info["name"]).to eq(app.name)
64
+ expect(app_update_info["maintenance"]).to be_truthy
65
+ end
66
+
67
+ it "marks itself 'finished' when done in non-block mode" do
68
+ app = Hatchet::Runner.new("default_ruby")
69
+
70
+ def app.push_with_retry!; nil; end
71
+ app.deploy
72
+ expect(app.platform_api.app.info(app.name)["maintenance"]).to be_falsey
73
+
74
+ app.teardown!
75
+
76
+ # After the app is updated, there's no guarantee it will still exist
77
+ # so we cannot rely on an api call to determine maintenance mode
78
+ app_update_info = app.instance_variable_get(:"@app_update_info")
79
+ expect(app_update_info["name"]).to eq(app.name)
80
+ expect(app_update_info["maintenance"]).to be_truthy
81
+ end
82
+
83
+ it "before deploy" do
84
+ @called = false
85
+ @dir = false
86
+ app = Hatchet::App.new("default_ruby")
87
+ def app.push_with_retry!
88
+ # do nothing
89
+ end
90
+ app.before_deploy do
91
+ @called = true
92
+ @dir = Dir.pwd
93
+ end
94
+ app.deploy do
95
+ expect(@called).to eq(true)
96
+ expect(@dir).to eq(Dir.pwd)
97
+ end
98
+ expect(@dir).to_not eq(Dir.pwd)
99
+ end
100
+
101
+ it "auto commits code" do
102
+ string = "foo#{SecureRandom.hex}"
103
+ app = Hatchet::App.new("default_ruby")
104
+ def app.push_with_retry!
105
+ # do nothing
106
+ end
107
+ app.before_deploy do |app|
108
+ expect(app.send(:needs_commit?)).to eq(false)
109
+ `echo "#{string}" > Gemfile`
110
+ expect(app.send(:needs_commit?)).to eq(true)
111
+ end
112
+ app.deploy do
113
+ expect(File.read("Gemfile").chomp).to eq(string)
114
+ expect(app.send(:needs_commit?)).to eq(false)
115
+ end
116
+ end
117
+
118
+ it "nested in directory" do
119
+ string = "foo#{SecureRandom.hex}"
120
+ app = Hatchet::App.new("default_ruby")
121
+ def app.push_with_retry!
122
+ # do nothing
123
+ end
124
+ app.in_directory do
125
+ `echo "#{string}" > Gemfile`
126
+ dir = Dir.pwd
127
+ app.deploy do
128
+ expect(File.read("Gemfile").chomp).to eq(string)
129
+ expect(dir).to eq(Dir.pwd)
130
+ end
131
+ end
132
+ end
133
+
134
+ it "run" do
135
+ skip("Must set HATCHET_EXPENSIVE_MODE") unless ENV["HATCHET_EXPENSIVE_MODE"]
136
+
137
+ app = Hatchet::GitApp.new("default_ruby", run_multi: true)
138
+ app.deploy do
139
+ expect(app.run("ls -a Gemfile 'foo bar #baz'")).to match(/ls: cannot access 'foo bar #baz': No such file or directory\s+Gemfile/)
140
+ expect((0 != $?.exitstatus)).to be_truthy
141
+
142
+ app.run("ls erpderp", heroku: ({ "exit-code" => (Hatchet::App::SkipDefaultOption) }))
143
+ expect((0 == $?.exitstatus)).to be_truthy
144
+
145
+ app.run("ls erpderp", heroku: ({ "no-tty" => nil }))
146
+ expect((0 != $?.exitstatus)).to be_truthy
147
+
148
+ expect(app.run("echo \\$HELLO \\$NAME", raw: true, heroku: ({ "env" => "HELLO=ohai;NAME=world" }))).to match(/ohai world/)
149
+
150
+ expect(app.run("echo \\$HELLO \\$NAME", raw: true, heroku: ({ "env" => "" }))).to_not match(/ohai world/)
151
+
152
+ random_name = SecureRandom.hex
153
+ expect(app.run("mkdir foo; touch foo/#{random_name}; ls foo/")).to match(/#{random_name}/)
154
+ end
155
+ end
156
+
157
+ class AtomicCount
158
+ attr_reader :value
159
+
160
+ def initialize(value)
161
+ @value = value
162
+ @mutex = Mutex.new
163
+ end
164
+
165
+ # In MRI the `+=` is not atomic, it is two seperate virtual machine
166
+ # instructions. To protect against race conditions, we can lock with a mutex
167
+ def add(val)
168
+ @mutex.synchronize do
169
+ @value += val
170
+ end
171
+ end
172
+ end
173
+
174
+ it "run multi" do
175
+ skip("Must set HATCHET_EXPENSIVE_MODE") unless ENV["HATCHET_EXPENSIVE_MODE"]
176
+
177
+ @run_count = AtomicCount.new(0)
178
+ app = Hatchet::GitApp.new("default_ruby", run_multi: true)
179
+ app.deploy do
180
+ app.run_multi("ls") { |out| expect(out).to include("Gemfile"); @run_count.add(1) }
181
+ app.run_multi("blerg -v") { |_, status| expect(status.success?).to be_falsey; @run_count.add(1) }
182
+ app.run_multi("ruby -v") do |out, status|
183
+ expect(out).to include("ruby")
184
+ expect(status.success?).to be_truthy
185
+
186
+ @run_count.add(1)
187
+ end
188
+
189
+ expect(app.platform_api.formation.list(app.name).detect {|ps| ps["type"] == "web"}["size"].downcase).to_not eq("free")
190
+ end
191
+
192
+ # After the deploy block exits `teardown!` is called
193
+ # this ensures all `run_multi` commands have exited and the dyno should be scaled down
194
+ expect(@run_count.value).to eq(3)
195
+ end
196
+
197
+ describe "running concurrent tests in different examples works" do
198
+ # This is not a great pattern if we're running tests via a parallel runner
199
+ #
200
+ # For example this will be guaranteed to be called, not just once, but at least once for every process
201
+ # that needs to run a test. In the best case it will only fire once, in the worst case it will fire N times
202
+ # if there are N tests. It is effectively the same as a `before(:each)`
203
+ #
204
+ # Documented here: https://github.com/grosser/parallel_split_test/pull/22/files
205
+ before(:all) do
206
+ skip("Must set HATCHET_EXPENSIVE_MODE") unless ENV["HATCHET_EXPENSIVE_MODE"]
207
+
208
+ @app = Hatchet::GitApp.new("default_ruby", run_multi: true)
209
+ @app.deploy
210
+ end
211
+
212
+ after(:all) do
213
+ @app.teardown! if @app
214
+ end
215
+
216
+ it "test one" do
217
+ expect(@app.run("ls")).to include("Gemfile")
218
+ expect(@app.platform_api.formation.list(@app.name).detect {|ps| ps["type"] == "web"}["size"].downcase).to_not eq("free")
219
+ end
220
+
221
+ it "test two" do
222
+ expect(@app.run("ruby -v")).to include("ruby")
223
+ expect(@app.platform_api.formation.list(@app.name).detect {|ps| ps["type"] == "web"}["size"].downcase).to_not eq("free")
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,67 @@
1
+ require "spec_helper"
2
+
3
+ describe "CIFourTest" do
4
+ it "error with bad app" do
5
+ string = SecureRandom.hex
6
+
7
+ app = Hatchet::GitApp.new("default_ruby")
8
+ app.run_ci do |test_run|
9
+ expect(test_run.output).to_not match(string)
10
+ expect(test_run.output).to match("Installing rake")
11
+
12
+ run!("echo 'puts \"#{string}\"' >> Rakefile")
13
+ test_run.run_again
14
+
15
+ expect(test_run.output).to match(string)
16
+ expect(test_run.output).to match("Using rake")
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
+ 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
+ end
28
+
29
+ it "error with bad app" do
30
+ expect {
31
+ Hatchet::GitApp.new("rails5_ci_fails_no_database").run_ci { }
32
+ }.to raise_error(/PG::ConnectionBad: could not connect to server/)
33
+ end
34
+
35
+ it "error with bad app" do
36
+ @before_deploy_called = false
37
+ @before_deploy_dir_pwd = nil
38
+
39
+ before_deploy = -> do
40
+ @before_deploy_called = true
41
+ @before_deploy_dir_pwd = Dir.pwd
42
+ end
43
+
44
+ Hatchet::GitApp.new("rails5_ci_fails_no_database", allow_failure: true, before_deploy: before_deploy).run_ci do |test_run|
45
+ expect(test_run.status).to eq(:errored)
46
+ expect(@before_deploy_dir_pwd).to eq(Dir.pwd)
47
+ expect(@before_deploy_called).to be_truthy
48
+ end
49
+
50
+ expect(@before_deploy_dir_pwd).to_not eq(Dir.pwd)
51
+ end
52
+
53
+ it "ci create app with stack" do
54
+ app = Hatchet::GitApp.new("rails5_ruby_schema_format")
55
+ app.run_ci do |test_run|
56
+ expect(test_run.output).to match("Ruby buildpack tests completed successfully")
57
+ expect(test_run.status).to eq(:succeeded)
58
+ expect(app.pipeline_id).to_not be_nil
59
+
60
+ api_rate_limit = app.api_rate_limit.call
61
+ couplings = api_rate_limit.pipeline_coupling.list_by_pipeline(app.pipeline_id)
62
+ coupled_app = api_rate_limit.app.info(couplings.first["app"]["id"])
63
+ expect(coupled_app["name"]).to eq(app.name)
64
+ end
65
+ expect(app.pipeline_id).to be_nil
66
+ end
67
+ end
@@ -0,0 +1,34 @@
1
+ require("spec_helper")
2
+ describe "ConfigTest" do
3
+ before { @config = Hatchet::Config.new }
4
+
5
+ it("config path for name") do
6
+ expect(@config.path_for_name("rails3_mri_193")).to(eq("repo_fixtures/repos/rails3/rails3_mri_193"))
7
+ end
8
+
9
+ it("config dirs") do
10
+ { "repo_fixtures/repos/bundler/no_lockfile" => "https://github.com/sharpstone/no_lockfile.git", "repo_fixtures/repos/default/default_ruby" => "https://github.com/sharpstone/default_ruby.git", "repo_fixtures/repos/rails2/rails2blog" => "https://github.com/sharpstone/rails2blog.git", "repo_fixtures/repos/rails3/rails3_mri_193" => "https://github.com/sharpstone/rails3_mri_193.git" }.each do |key, value|
11
+ assert_include(key, value, @config.dirs)
12
+ end
13
+ end
14
+
15
+ it("config repos") do
16
+ { "default_ruby" => "repo_fixtures/repos/default/default_ruby", "no_lockfile" => "repo_fixtures/repos/bundler/no_lockfile", "rails2blog" => "repo_fixtures/repos/rails2/rails2blog", "rails3_mri_193" => "repo_fixtures/repos/rails3/rails3_mri_193" }.each do |key, value|
17
+ assert_include(key, value, @config.repos)
18
+ end
19
+ end
20
+
21
+ it("no internal config raises no errors") do
22
+ @config.send(:set_internal_config!, {})
23
+ expect(@config.repo_directory_path).to(eq("./repos"))
24
+ end
25
+
26
+ it("github shortcuts") do
27
+ @config.send(:init_config!, "foo" => (["schneems/sextant"]))
28
+ expect(@config.dirs["./repos/foo/sextant"]).to(eq("https://github.com/schneems/sextant.git"))
29
+ end
30
+
31
+ private def assert_include(key, value, actual)
32
+ expect(actual[key]).to eq(value), "Expected #{actual.inspect} to include #{{ key => value }} but it did not"
33
+ end
34
+ end
@@ -0,0 +1,17 @@
1
+ require("spec_helper")
2
+ describe "EditRepoTest" do
3
+ it "can deploy git app" do
4
+ Hatchet::GitApp.new("default_ruby").in_directory do |app|
5
+ `touch foo`
6
+ expect($?.success?).to(eq(true))
7
+
8
+ `git add .; git commit -m foo`
9
+ expect($?.success?).to(eq(true))
10
+ expect(`ls`).to(match("foo"))
11
+ end
12
+
13
+ Hatchet::GitApp.new("default_ruby").in_directory do |app|
14
+ expect(`ls`).to_not match(/foo/)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ require "spec_helper"
2
+
3
+ describe "GitAppTest" do
4
+ it "can deploy git app" do
5
+ Hatchet::GitApp.new("rails5_ruby_schema_format").deploy do |app|
6
+ expect(app.run("ruby -v")).to match("2.6.6")
7
+ end
8
+ end
9
+ end