heroku_hatchet 7.2.0 → 7.3.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -44,9 +44,9 @@ module Hatchet
44
44
 
45
45
  # use this method to turn "codetriage" into repos/rails3/codetriage
46
46
  def path_for_name(name)
47
- possible_paths = [repos[name.to_s], "repos/#{name}", name].compact
47
+ possible_paths = [repos[name.to_s], "#{repo_directory_path}/#{name}", name].compact
48
48
  path = possible_paths.detect do |path|
49
- !Dir[path]&.empty?
49
+ !(Dir[path] && Dir[path].empty?)
50
50
  end
51
51
  raise BadRepoName.new(name, possible_paths) if path.nil? || path.empty?
52
52
  path
@@ -5,25 +5,30 @@ 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
- output = git_push_heroku_yall
14
- rescue FailedDeploy => e
15
- if e.output.match?(/reached the API rate limit/)
16
- throw(:throttle)
17
- elsif @allow_failure
18
- output = e.output
19
- else
20
- raise e
12
+ begin
13
+ output = git_push_heroku_yall
14
+ rescue FailedDeploy => e
15
+ case e.output
16
+ when /reached the API rate limit/, /429 Too Many Requests/, /HTTP 429/, /HTTP code = 429/
17
+ throw(:throttle)
18
+ else
19
+ raise e unless @allow_failure
20
+ output = e.output
21
+ end
21
22
  end
22
23
  end
23
24
 
24
25
  return output
25
26
  end
26
27
 
28
+ def releases
29
+ platform_api.release.list(name)
30
+ end
31
+
27
32
  private def git_push_heroku_yall
28
33
  output = `git push #{git_repo} HEAD:main 2>&1`
29
34
 
@@ -31,7 +36,6 @@ module Hatchet
31
36
  raise FailedDeployError.new(self, "Buildpack: #{@buildpack.inspect}\nRepo: #{git_repo}", output: output)
32
37
  end
33
38
 
34
- releases = platform_api.release.list(name)
35
39
  if releases.last["status"] == "failed"
36
40
  commit! # An empty commit allows us to deploy again
37
41
  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
@@ -55,7 +55,7 @@ module Hatchet
55
55
  # To be safe try to delete an app even if we're not over the limit
56
56
  # since the exception may have been caused by going over the maximum account limit
57
57
  if app_exception_message
58
- io.puts <<~EOM
58
+ io.puts <<-EOM.strip_heredoc
59
59
  WARNING: Running reaper due to exception on app
60
60
  #{stats_string}
61
61
  Exception: #{app_exception_message}
@@ -122,7 +122,7 @@ module Hatchet
122
122
 
123
123
  # Sleep, try again later
124
124
  @reaper_throttle.call(max_sleep: age.sleep_for_ttl) do |sleep_for|
125
- io.puts <<~EOM
125
+ io.puts <<-EOM.strip_heredoc
126
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
127
  This can happen if App#teardown! is not called on an application, which will leave it in an 'unfinished' state
128
128
  This can also happen if you're trying to run more tests concurrently than your currently set value for HATCHET_APP_COUNT
@@ -170,14 +170,14 @@ module Hatchet
170
170
  body = e.response.body
171
171
  request_id = e.response.headers["Request-Id"]
172
172
  if body =~ /Couldn\'t find that app./
173
- io.puts "Duplicate destoy attempted #{name.inspect}: #{id}, status: 404, request_id: #{request_id}"
173
+ io.puts "Duplicate destroy attempted #{name.inspect}: #{id}, status: 404, request_id: #{request_id}"
174
174
  raise AlreadyDeletedError.new
175
175
  else
176
176
  raise e
177
177
  end
178
178
  rescue Excon::Error::Forbidden => e
179
179
  request_id = e.response.headers["Request-Id"]
180
- io.puts "Duplicate destoy attempted #{name.inspect}: #{id}, status: 403, request_id: #{request_id}"
180
+ io.puts "Duplicate destroy attempted #{name.inspect}: #{id}, status: 403, request_id: #{request_id}"
181
181
  raise AlreadyDeletedError.new
182
182
  end
183
183
 
@@ -1,4 +1,4 @@
1
- version: 1
1
+ version: 2
2
2
  updates:
3
3
  - package-ecosystem: "bundler"
4
4
  directory: "/"
@@ -1,7 +1,9 @@
1
+ require "tempfile"
2
+
1
3
  module Hatchet
2
4
  class FailedTestError < StandardError
3
5
  def initialize(app, output)
4
- msg = "Could not run tests on pipeline id: '#{app.pipeline_id}' (#{app.repo_name}) at path: '#{app.directory}'\n" <<
6
+ msg = "Could not run tests on pipeline id: '#{app.pipeline_id}' (#{app.repo_name}) at path: '#{app.original_source_code_directory}'\n" <<
5
7
  " if this was expected add `allow_failure: true` to your hatchet initialization hash.\n" <<
6
8
  "output:\n" <<
7
9
  "#{output}"
@@ -140,8 +142,9 @@ module Hatchet
140
142
  end
141
143
  end
142
144
  rescue Timeout::Error
143
- puts "Timed out status: #{@status}, timeout: #{@timeout}"
144
- raise FailedTestError.new(self.app, self.output) unless app.allow_failure?
145
+ message = "Timed out status: #{@status}, timeout: #{@timeout}, app: #{app.name}"
146
+ puts message
147
+ raise FailedTestError.new(self.app, "#{message}, output:\n#{self.output}") unless app.allow_failure?
145
148
  yield self
146
149
  return self
147
150
  end
@@ -176,15 +179,17 @@ module Hatchet
176
179
  app_json["stack"] ||= @app.stack if @app.stack && !@app.stack.empty?
177
180
  File.open("app.json", "w") {|f| f.write(JSON.generate(app_json)) }
178
181
 
179
- out = `tar c . | gzip -9 > slug.tgz`
180
- raise "Tar command failed: #{out}" unless $?.success?
181
-
182
- source_put_url = @app.create_source
183
- Hatchet::RETRIES.times.retry do
184
- PlatformAPI.rate_throttle.call do
185
- Excon.put(source_put_url,
186
- expects: [200],
187
- body: File.read('slug.tgz'))
182
+ Tempfile.create("slug.tgz") do |slug|
183
+ out = `tar c . | gzip -9 > #{slug.path}`
184
+ raise "Tar command failed: #{out}" unless $?.success?
185
+
186
+ source_put_url = @app.create_source
187
+ Hatchet::RETRIES.times.retry do
188
+ PlatformAPI.rate_throttle.call do
189
+ Excon.put(source_put_url,
190
+ expects: [200],
191
+ body: slug.read)
192
+ end
188
193
  end
189
194
  end
190
195
  end
@@ -1,3 +1,3 @@
1
1
  module Hatchet
2
- VERSION = "7.2.0"
2
+ VERSION = "7.3.4"
3
3
  end
@@ -5,7 +5,7 @@ describe "AllowFailureGitTest" do
5
5
  let(:release_fail_proc) {
6
6
  Proc.new do
7
7
  File.open("Procfile", "w+") do |f|
8
- f.write <<~EOM
8
+ f.write <<-EOM.strip_heredoc
9
9
  release: echo "failing on release" && exit 1
10
10
  EOM
11
11
  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
@@ -64,6 +58,10 @@ describe "AppTest" do
64
58
  expect(app.buildpacks.first).to match("https://github.com/heroku/heroku-buildpack-ruby")
65
59
  end
66
60
 
61
+ it "default_buildpack is only computed once" do
62
+ expect(Hatchet::App.default_buildpack.object_id).to eq(Hatchet::App.default_buildpack.object_id)
63
+ end
64
+
67
65
  it "create app with stack" do
68
66
  stack = "heroku-16"
69
67
  app = Hatchet::App.new("default_ruby", stack: stack)
@@ -71,6 +69,19 @@ describe "AppTest" do
71
69
  expect(app.platform_api.app.info(app.name)["build_stack"]["name"]).to eq(stack)
72
70
  end
73
71
 
72
+ it "create app with HATCHET_DEFAULT_STACK set" do
73
+ begin
74
+ original_default_stack = ENV["HATCHET_DEFAULT_STACK"]
75
+ default_stack = "heroku-18"
76
+ ENV["HATCHET_DEFAULT_STACK"] = default_stack
77
+ app = Hatchet::App.new("default_ruby")
78
+ app.create_app
79
+ expect(app.platform_api.app.info(app.name)["build_stack"]["name"]).to eq(default_stack)
80
+ ensure
81
+ ENV["HATCHET_DEFAULT_STACK"] = original_default_stack
82
+ end
83
+ end
84
+
74
85
  it "marks itself 'finished' when done in block mode" do
75
86
  app = Hatchet::Runner.new("default_ruby")
76
87
 
@@ -102,22 +113,75 @@ describe "AppTest" do
102
113
  expect(app_update_info["maintenance"]).to be_truthy
103
114
  end
104
115
 
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
116
+ describe "before deploy" do
117
+ it "dir" do
118
+ @called = false
119
+ @dir = false
120
+ app = Hatchet::App.new("default_ruby")
121
+ def app.push_with_retry!
122
+ # do nothing
123
+ end
124
+ app.before_deploy do
125
+ @called = true
126
+ @dir = Dir.pwd
127
+ end
128
+ app.deploy do
129
+ expect(@called).to eq(true)
130
+ expect(@dir).to eq(Dir.pwd)
131
+ end
132
+ expect(@dir).to_not eq(Dir.pwd)
111
133
  end
112
- app.before_deploy do
113
- @called = true
114
- @dir = Dir.pwd
134
+
135
+ it "prepend" 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(:prepend) do
144
+ @value << "hello "
145
+ end
146
+ app.deploy do
147
+ end
148
+
149
+ expect(@value).to eq("hello there")
115
150
  end
116
- app.deploy do
117
- expect(@called).to eq(true)
118
- expect(@dir).to eq(Dir.pwd)
151
+
152
+ it "append" 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(:append) do
161
+ @value << " hello"
162
+ end
163
+ app.deploy do
164
+ end
165
+
166
+ expect(@value).to eq("there hello")
167
+ end
168
+
169
+ it "replace" do
170
+ @value = ""
171
+ app = Hatchet::App.new("default_ruby")
172
+ def app.push_with_retry!; end
173
+ app.before_deploy do
174
+ @value << "there"
175
+ end
176
+
177
+ app.before_deploy(:replace) do
178
+ @value << "hello"
179
+ end
180
+ app.deploy do
181
+ end
182
+
183
+ expect(@value).to eq("hello")
119
184
  end
120
- expect(@dir).to_not eq(Dir.pwd)
121
185
  end
122
186
 
123
187
  it "auto commits code" do
@@ -28,7 +28,7 @@ describe "CIFourTest" do
28
28
 
29
29
  it "error with bad app" do
30
30
  expect {
31
- Hatchet::GitApp.new("rails5_ci_fails_no_database").run_ci { }
31
+ Hatchet::GitApp.new("rails5_ci_fails_no_database", stack: "heroku-18").run_ci { }
32
32
  }.to raise_error(/PG::ConnectionBad: could not connect to server/)
33
33
  end
34
34
 
@@ -41,7 +41,7 @@ describe "CIFourTest" do
41
41
  @before_deploy_dir_pwd = Dir.pwd
42
42
  end
43
43
 
44
- Hatchet::GitApp.new("rails5_ci_fails_no_database", allow_failure: true, before_deploy: before_deploy).run_ci do |test_run|
44
+ Hatchet::GitApp.new("rails5_ci_fails_no_database", stack: "heroku-18", allow_failure: true, before_deploy: before_deploy).run_ci do |test_run|
45
45
  expect(test_run.status).to eq(:errored)
46
46
  expect(@before_deploy_dir_pwd).to eq(Dir.pwd)
47
47
  expect(@before_deploy_called).to be_truthy