heroku_hatchet 5.0.0 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
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