heroku_hatchet 7.1.1 → 7.3.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: 9dda03e482a8634af97061a88f4b4ed424697928742d1e0b1c654b2837fbe4e0
4
- data.tar.gz: c5958a54971f4470faee1007a94ee4902622d21ebca28ae8c5fbc5dbfff181f9
3
+ metadata.gz: 6c1b161f5f43f135f7eb4e6c803b6228b15e51904001a744572cca572e26a799
4
+ data.tar.gz: e804ea689f9934308978e4804799fee00a0d252d3e64cb62eb29a20c4d05659f
5
5
  SHA512:
6
- metadata.gz: a4c7794985aa05f0206de153a9f1dc3e01d5eac6b870e35990bd53a8c8ae0ed7c5ba67c9db9fb2b76fe8c4cc6f8a934ad173ba0140135356738f91a3bac54b6b
7
- data.tar.gz: 542c31aa8fea0633175f2654a8ac21ffa53525a03c6127f19ce930a71883f5eaed8749915c9e971d574e5feace1eaab82765473b6d04430ac2161b2410603fb9
6
+ metadata.gz: b2b9364b374e384b3e5d16f159774d4b1da90d3a79c5c3afad6092d05209a13bd8d52c2d34c97feb4da0b989339d120a4562c65b121e7ec6b3d843ae3657cefe
7
+ data.tar.gz: '026904fd62500a2874d57f9c8462aee651f62dd1467c6281bc7061312d2a683203f8e1237577d2bb66ab3f4d5174bdf2fe408e3c165a950a1cf4a386673f4a29'
@@ -29,9 +29,9 @@ references:
29
29
  bundle clean
30
30
 
31
31
  jobs:
32
- "ruby-2.5":
32
+ "ruby-2.2":
33
33
  docker:
34
- - image: circleci/ruby:2.5
34
+ - image: circleci/ruby:2.2
35
35
  steps:
36
36
  - checkout
37
37
  - <<: *restore
@@ -39,9 +39,9 @@ jobs:
39
39
  - <<: *hatchet_setup
40
40
  - <<: *unit
41
41
  - <<: *save
42
- "ruby-2.6":
42
+ "ruby-2.5":
43
43
  docker:
44
- - image: circleci/ruby:2.6
44
+ - image: circleci/ruby:2.5
45
45
  steps:
46
46
  - checkout
47
47
  - <<: *restore
@@ -64,6 +64,6 @@ workflows:
64
64
  version: 2
65
65
  build:
66
66
  jobs:
67
+ - "ruby-2.2"
67
68
  - "ruby-2.5"
68
- - "ruby-2.6"
69
69
  - "ruby-2.7"
@@ -1,7 +1,8 @@
1
1
  name: Check Changelog
2
2
 
3
- on: [pull_request]
4
-
3
+ on:
4
+ pull_request:
5
+ types: [opened, reopened, edited, synchronize]
5
6
  jobs:
6
7
  build:
7
8
  runs-on: ubuntu-latest
@@ -1,5 +1,44 @@
1
1
  ## HEAD
2
2
 
3
+ ## 7.3.1
4
+
5
+ - Fix Ruby incompatibility introduced by using `&.` and `rescue` without
6
+ `begin`/`end` without a `required_ruby_version` in hatchet.gemspec.
7
+ (https://github.com/heroku/hatchet/pull/139)
8
+
9
+ ## 7.3.0
10
+
11
+ - Deprecations
12
+ - Deprecation: Calling `App#before_deploy` as a way to clear/replace the existing block should now be done with `App#before_deploy(:replace)` (https://github.com/heroku/hatchet/pull/126)
13
+ - Deprecation: HATCHET_BUILDPACK_BASE default (https://github.com/heroku/hatchet/pull/133)
14
+ - Deprecation: App#directory (https://github.com/heroku/hatchet/pull/135)
15
+
16
+ - Flappy test improvements
17
+ - Increase CI timeout limit to 900 seconds (15 minutes) (https://github.com/heroku/hatchet/pull/137)
18
+ - Empty string returns from App#run now trigger retries (https://github.com/heroku/hatchet/pull/132)
19
+ - Rescue 403 on pipeline delete (https://github.com/heroku/hatchet/pull/130)
20
+ - Additional rate throttle cases handled (https://github.com/heroku/hatchet/pull/128)
21
+
22
+ - Usability
23
+ - Annotate rspec expectation failures inside of deploy blocks with hatchet debug information (https://github.com/heroku/hatchet/pull/136)
24
+ - Hatchet#new raises a helpful error when no source code location is provided (https://github.com/heroku/hatchet/pull/134)
25
+ - Lazy evaluation of HATCHET_BUILDPACK_BASE env var (https://github.com/heroku/hatchet/pull/133)
26
+ - Allow multiple `App#before_deploy` blocks to be set and called (https://github.com/heroku/hatchet/pull/126)
27
+ - Performance improvement when running without an explicit HEROKU_API_KEY set (https://github.com/heroku/hatchet/pull/128)
28
+
29
+ ## 7.2.0
30
+
31
+ - App#setup! no longer modifies files on disk. (https://github.com/heroku/hatchet/pull/125)
32
+ - Add `$ hatchet init` command for bootstrapping new projects (https://github.com/heroku/hatchet/pull/123)
33
+
34
+ ## 7.1.3
35
+
36
+ - Important!! Fix branch name detection on CircleCI (https://github.com/heroku/hatchet/pull/124)
37
+
38
+ ## 7.1.2
39
+
40
+ - Fix support for Hatchet deploying the 'main' branch (https://github.com/heroku/hatchet/pull/122)
41
+
3
42
  ## 7.1.1
4
43
 
5
44
  - Fix destroy_all functionality (https://github.com/heroku/hatchet/pull/121)
data/README.md CHANGED
@@ -57,6 +57,8 @@ In addition to speed, Hatchet provides isolation. Suppose you're executing `bin/
57
57
 
58
58
  ## Quicklinks
59
59
 
60
+ - Getting started
61
+ - [Add hatchet tests to a existing buildpack](#hatchet-init)
60
62
  - Concepts
61
63
  - [Tell Hatchet how to find your buildpack](#specify-buildpack)
62
64
  - [Give Hatchet some example apps to deploy](#example-apps)
@@ -78,6 +80,45 @@ In addition to speed, Hatchet provides isolation. Suppose you're executing `bin/
78
80
  - [Introduction to the Rspec testing framework for non-rubyists](#basic-rspec)
79
81
  - [Introduction to Ruby for non-rubyists](#basic-ruby)
80
82
 
83
+ ## Getting Started
84
+
85
+ ### Hatchet Init
86
+
87
+ If you're working in a project that does not already have hatchet tests you can run this command to get started quickly:
88
+
89
+ Make sure you're in directory that contains your buildpack and run:
90
+
91
+ ```
92
+ $ gem install heroku_hatchet
93
+ $ hatchet init
94
+ ```
95
+
96
+ This will bootstrap your project with the necessarry files to test your buildpack. Including but not limited to:
97
+
98
+ - Gemfile
99
+ - hatchet.json
100
+ - spec/spec_helper.rb
101
+ - spec/hatchet/buildpack_spec.rb
102
+ - .circleci/config.yml
103
+ - .github/dependabot.yml
104
+ - .gitignore
105
+
106
+ Once this executes successfully then you can run your tests with:
107
+
108
+ ```
109
+ $ bundle exec rspec
110
+ ```
111
+
112
+ > Note: You'll need to update the `buildpack_spec.rb` file to remove the exception
113
+
114
+ You can also focus a specific file or test by providing a path and line number:
115
+
116
+ ```
117
+ $ bundle exec rspec spec/hatchet/buildpack_spec:5
118
+ ```
119
+
120
+ Keep reading to find out more about how hatchet works.
121
+
81
122
  ## Concepts
82
123
 
83
124
  ### Specify buildpack
@@ -89,7 +130,7 @@ ENV["HATCHET_BUILDPACK_BASE"] = "https://github.com/path-to-your/buildpack"
89
130
  require 'hatchet'`
90
131
  ```
91
132
 
92
- If you do not specify `HATCHET_BUILDPACK_URL` the default Ruby buildpack will be used. If you do not specify a `HATCHET_BUILDPACK_BRANCH` the current branch you are on will be used. This is how the Ruby buildpack runs tests on branches on CI (by leaving `HATCHET_BUILDPACK_BRANCH` blank).
133
+ If you do not specify `HATCHET_BUILDPACK_BASE` the default Ruby buildpack will be used. If you do not specify a `HATCHET_BUILDPACK_BRANCH` the current branch you are on will be used. This is how the Ruby buildpack runs tests on branches on CI (by leaving `HATCHET_BUILDPACK_BRANCH` blank).
93
134
 
94
135
  The workflow generally looks like this:
95
136
 
@@ -156,7 +197,7 @@ You can reference one of these applications in your test by using it's git name:
156
197
  Hatchet::Runner.new('no_lockfile')
157
198
  ```
158
199
 
159
- If you have conflicting names, use full paths like `Hatchet::RUnner.new("sharpstone/no_lockfile")`.
200
+ If you have conflicting names, use full paths like `Hatchet::Runner.new("sharpstone/no_lockfile")`.
160
201
 
161
202
  When you run `hatchet install` it will lock all the Repos to a specific commit. This is done so that if a repo changes upstream that introduces an error the test suite won't automatically pick it up. For example in https://github.com/sharpstone/lock_fail/commit/e61ba47043fbae131abb74fd74added7e6e504df an error is added, but this will only cause a failure if your project intentionally locks to commit `e61ba47043fbae131abb74fd74added7e6e504df` or later.
162
203
 
@@ -484,7 +525,7 @@ end
484
525
 
485
526
  In this example, the app would use the nodejs buildpack, and then `:default` gets replaced by your Git url and branch name.
486
527
 
487
- - before_deploy (Block): Instead of using the `tap` syntax you can provide a block directly to hatchet app initialization:
528
+ - before_deploy (Block): Instead of using the `tap` syntax you can provide a block directly to hatchet app initialization. Example:
488
529
 
489
530
  ```ruby
490
531
  Hatchet::Runner.new("default_ruby", before_deploy: ->{ FileUtils.touch("foo.txt")}).deploy do
@@ -589,6 +630,23 @@ Hatchet::Runner.new("default_ruby", before_deploy: before_deploy_proc).deploy do
589
630
  end
590
631
  ```
591
632
 
633
+ You can call multiple blocks by specifying (`:prepend` or `:append`):
634
+
635
+ ```ruby
636
+ Hatchet::Runner.new("default_ruby").tap do |app|
637
+ app.before_deploy do
638
+ FileUtils.touch("foo.txt")
639
+ end
640
+
641
+ app.before_deploy(:append) do
642
+ FileUtils.touch("bar.txt")
643
+ end
644
+ app.deploy do
645
+ end
646
+ end
647
+ ```
648
+
649
+
592
650
  - `app.commit!`: Will updates the contents of your local git dir if you've modified files on disk
593
651
 
594
652
  ```ruby
@@ -619,7 +677,7 @@ end
619
677
  > Note: If you want to execute tests in this temp directory, you likely want to use `in_directory_fork` otherwise, you might accidentally contaminate the current environment's variables if you modify them.
620
678
 
621
679
  - `app.in_directory_fork`: Runs the given block in a temp directory and inside of a forked process, an example given above.
622
- - `app.directory`: Returns the directory of the example application on disk, this is NOT the temp directory that you're currently executing against. It's probably not what you want.
680
+ - `app.original_source_code_directory`: Returns the directory of the example application on disk, this is NOT the temp directory that you're currently executing against. It's probably not what you want.
623
681
  - `app.deploy`: Your main method takes a block to execute after the deploy is successful. If no block is provided, you must manually call `app.teardown!` (see below for an example).
624
682
  - `app.output`: The output contents of the deploy
625
683
  - `app.platform_api`: Returns an instance of the [platform-api Heroku client](https://github.com/heroku/platform-api). If Hatchet doesn't give you access to a part of Heroku that you need, you can likely do it with the platform-api client.
@@ -15,8 +15,14 @@ require 'thor'
15
15
  require 'threaded'
16
16
  require 'date'
17
17
  require 'yaml'
18
+ require 'pathname'
18
19
 
19
20
  class HatchetCLI < Thor
21
+ desc "init", "bootstraps a project with minimal files required to add hatchet tests"
22
+ define_method("init") do
23
+ Hatchet::InitProject.new.call
24
+ end
25
+
20
26
  desc "ci:install_heroku", "installs the `heroku` cli"
21
27
  define_method("ci:install_heroku") do
22
28
  if `which heroku` && $?.success?
@@ -46,7 +52,7 @@ class HatchetCLI < Thor
46
52
  Threaded.later do
47
53
  commit = lock_hash[directory]
48
54
  directory = File.expand_path(directory)
49
- if !Dir[directory]&.empty?
55
+ if !(Dir[directory] && Dir[directory].empty?)
50
56
  puts "== pulling '#{git_repo}' into '#{directory}'\n"
51
57
  pull(directory, git_repo)
52
58
  else
@@ -17,17 +17,17 @@ Gem::Specification.new do |gem|
17
17
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
+ gem.required_ruby_version = '>= 2.2.0'
20
21
 
21
22
  gem.add_dependency "platform-api", "~> 3"
22
23
  gem.add_dependency "rrrretry", "~> 1"
23
24
  gem.add_dependency "excon", "~> 0"
24
- gem.add_dependency "thor", "~> 0"
25
+ gem.add_dependency "thor", "~> 1"
25
26
  gem.add_dependency "threaded", "~> 0"
26
27
 
27
28
  gem.add_development_dependency "rspec"
28
29
  gem.add_development_dependency "rake", ">= 10"
29
30
  gem.add_development_dependency "mocha", ">= 1"
30
31
  gem.add_development_dependency "parallel_split_test"
31
- gem.add_development_dependency "travis", ">= 1"
32
32
  gem.add_development_dependency "rspec-retry"
33
33
  end
@@ -18,6 +18,8 @@ require 'hatchet/anvil_app'
18
18
  require 'hatchet/git_app'
19
19
  require 'hatchet/config'
20
20
  require 'hatchet/api_rate_limit'
21
+ require 'hatchet/init_project'
22
+ require 'hatchet/heroku_run'
21
23
 
22
24
  module Hatchet
23
25
  RETRIES = Integer(ENV['HATCHET_RETRIES'] || 1)
@@ -30,7 +32,7 @@ module Hatchet
30
32
  return ENV['TRAVIS_PULL_REQUEST_BRANCH'] if ENV['TRAVIS_PULL_REQUEST_BRANCH'] && !ENV['TRAVIS_PULL_REQUEST_BRANCH'].empty?
31
33
  return ENV['TRAVIS_BRANCH'] if ENV['TRAVIS_BRANCH']
32
34
 
33
- out = `git describe --contains --all HEAD`.strip
35
+ out = `git rev-parse --abbrev-ref HEAD`.strip
34
36
  raise "Attempting to find current branch name. Error: Cannot describe git: #{out}" unless $?.success?
35
37
  out
36
38
  end
@@ -44,3 +46,22 @@ module Hatchet
44
46
  end
45
47
  end
46
48
  end
49
+
50
+ unless ::String.instance_methods.include?(:strip_heredoc)
51
+ # We can get rid of this when all rubies can support <<~ syntax
52
+ class ::String
53
+ def strip_heredoc
54
+ gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, "".freeze)
55
+ end
56
+ end
57
+ end
58
+
59
+ unless ::String.instance_methods.include?(:match?)
60
+ # We can get rid of this when all rubies can support String#match? method
61
+ class ::String
62
+ def match?(value)
63
+ self =~ value
64
+ end
65
+ end
66
+ end
67
+
@@ -5,11 +5,15 @@ require 'tmpdir'
5
5
 
6
6
  module Hatchet
7
7
  class App
8
- HATCHET_BUILDPACK_BASE = (ENV['HATCHET_BUILDPACK_BASE'] || "https://github.com/heroku/heroku-buildpack-ruby.git")
8
+ HATCHET_BUILDPACK_BASE = -> {
9
+ ENV.fetch('HATCHET_BUILDPACK_BASE') {
10
+ warn "ENV HATCHET_BUILDPACK_BASE is not set. It currently defaults to the ruby buildpack. In the future this env var will be required"
11
+ "https://github.com/heroku/heroku-buildpack-ruby.git"
12
+ }
13
+ }
9
14
  HATCHET_BUILDPACK_BRANCH = -> { ENV['HATCHET_BUILDPACK_BRANCH'] || ENV['HEROKU_TEST_RUN_BRANCH'] || Hatchet.git_branch }
10
- BUILDPACK_URL = "https://github.com/heroku/heroku-buildpack-ruby.git"
11
15
 
12
- attr_reader :name, :stack, :directory, :repo_name, :app_config, :buildpacks, :reaper, :max_retries_count
16
+ attr_reader :name, :stack, :repo_name, :app_config, :buildpacks, :reaper, :max_retries_count
13
17
 
14
18
  class FailedDeploy < StandardError; end
15
19
 
@@ -18,7 +22,7 @@ module Hatchet
18
22
 
19
23
  def initialize(app, message, output: )
20
24
  @output = output
21
- msg = "Could not deploy '#{app.name}' (#{app.repo_name}) using '#{app.class}' at path: '#{app.directory}'\n"
25
+ msg = "Could not deploy '#{app.name}' (#{app.repo_name}) using '#{app.class}' at path: '#{app.original_source_code_directory}'\n"
22
26
  msg << "if this was expected add `allow_failure: true` to your deploy hash.\n"
23
27
  msg << "#{message}\n"
24
28
  msg << "output:\n"
@@ -32,7 +36,7 @@ module Hatchet
32
36
 
33
37
  def initialize(app, message, output: )
34
38
  @output = output
35
- msg = "Could not release '#{app.name}' (#{app.repo_name}) using '#{app.class}' at path: '#{app.directory}'\n"
39
+ msg = "Could not release '#{app.name}' (#{app.repo_name}) using '#{app.class}' at path: '#{app.original_source_code_directory}'\n"
36
40
  msg << "if this was expected add `allow_failure: true` to your deploy hash.\n"
37
41
  msg << "#{message}\n"
38
42
  msg << "output:\n"
@@ -42,8 +46,9 @@ module Hatchet
42
46
  end
43
47
 
44
48
  SkipDefaultOption = Object.new
49
+ DEFAULT_REPO_NAME = Object.new
45
50
 
46
- def initialize(repo_name,
51
+ def initialize(repo_name = DEFAULT_REPO_NAME,
47
52
  stack: "",
48
53
  name: default_name,
49
54
  debug: nil,
@@ -58,6 +63,7 @@ module Hatchet
58
63
  retries: RETRIES,
59
64
  config: {}
60
65
  )
66
+ raise "You tried creating a Hatchet::App instance without source code, pass in a path to an app to deploy or the name of an app in your hatchet.json" if repo_name == DEFAULT_REPO_NAME
61
67
  @repo_name = repo_name
62
68
  @directory = self.config.path_for_name(@repo_name)
63
69
  @name = name
@@ -78,13 +84,36 @@ module Hatchet
78
84
  @already_in_dir = nil
79
85
  @app_is_setup = nil
80
86
 
81
- @before_deploy = before_deploy
87
+ @before_deploy_array = []
88
+ @before_deploy_array << before_deploy if before_deploy
82
89
  @app_config = config
83
90
  @reaper = Reaper.new(api_rate_limit: api_rate_limit)
84
91
  end
85
92
 
93
+ private def test_failure_classes
94
+ class_array = []
95
+ class_array << RSpec::Expectations::ExpectationNotMetError if defined?(RSpec::Expectations::ExpectationNotMetError)
96
+ class_array
97
+ end
98
+
99
+ def annotate_failures
100
+ yield
101
+ rescue *test_failure_classes => e
102
+ raise e, "App: #{name} (#{@repo_name})\n#{e.message}"
103
+ end
104
+
105
+ def directory
106
+ warn "Calling App#directory returns the original location of the app's source code that should not be modified, if this is really what you want use `original_source_code_directory` instead."
107
+ warn caller
108
+ @directory
109
+ end
110
+
111
+ def original_source_code_directory
112
+ @directory
113
+ end
114
+
86
115
  def self.default_buildpack
87
- [HATCHET_BUILDPACK_BASE, HATCHET_BUILDPACK_BRANCH.call].join("#")
116
+ [HATCHET_BUILDPACK_BASE.call, HATCHET_BUILDPACK_BRANCH.call].join("#")
88
117
  end
89
118
 
90
119
  def allow_failure?
@@ -157,32 +186,17 @@ module Hatchet
157
186
  command = command.to_s
158
187
  end
159
188
 
160
- heroku_command = build_heroku_command(command, options)
161
-
162
189
  allow_run_multi! if @run_multi
163
190
 
164
- output = ""
165
-
166
- ShellThrottle.new(platform_api: @platform_api).call do |throttle|
167
- output = `#{heroku_command}`
168
- throw(:throttle) if output.match?(/reached the API rate limit/)
169
- end
170
-
171
- return output
172
- end
173
-
174
- private def build_heroku_command(command, options = {})
175
- command = command.shellescape unless options.delete(:raw)
191
+ run_obj = Hatchet::HerokuRun.new(
192
+ command,
193
+ app: self,
194
+ retry_on_empty: options.fetch(:retry_on_empty, !ENV["HATCHET_DISABLE_EMPTY_RUN_RETRY"]),
195
+ heroku: options[:heroku],
196
+ raw: options[:raw]
197
+ ).call
176
198
 
177
- default_options = { "app" => name, "exit-code" => nil }
178
- heroku_options = (default_options.merge(options.delete(:heroku) || {})).map do |k,v|
179
- next if v == Hatchet::App::SkipDefaultOption # for forcefully removing e.g. --exit-code, a user can pass this
180
- arg = "--#{k.to_s.shellescape}"
181
- arg << "=#{v.to_s.shellescape}" unless v.nil? # nil means we include the option without an argument
182
- arg
183
- end.join(" ")
184
-
185
- "heroku run #{heroku_options} -- #{command}"
199
+ return run_obj.output
186
200
  end
187
201
 
188
202
  private def allow_run_multi!
@@ -191,7 +205,6 @@ module Hatchet
191
205
  @run_multi_is_setup ||= platform_api.formation.update(name, "web", {"size" => "Standard-1X"})
192
206
  end
193
207
 
194
-
195
208
  # Allows multiple commands to be run concurrently in the background.
196
209
  #
197
210
  # WARNING! Using the feature requres that the underlying app is not on the "free" Heroku
@@ -229,23 +242,15 @@ module Hatchet
229
242
  allow_run_multi!
230
243
 
231
244
  run_thread = Thread.new do
232
- heroku_command = build_heroku_command(command, options)
233
-
234
- out = nil
235
- status = nil
236
- ShellThrottle.new(platform_api: @platform_api).call do |throttle|
237
- out = `#{heroku_command}`
238
- throw(:throttle) if output.match?(/reached the API rate limit/)
239
- status = $?
240
- end
241
-
242
- yield out, status
243
-
244
- # if block.arity == 1
245
- # block.call(out)
246
- # else
247
- # block.call(out, status)
248
- # end
245
+ run_obj = Hatchet::HerokuRun.new(
246
+ command,
247
+ app: self,
248
+ retry_on_empty: options.fetch(:retry_on_empty, !ENV["HATCHET_DISABLE_EMPTY_RUN_RETRY"]),
249
+ heroku: options[:heroku],
250
+ raw: options[:raw]
251
+ ).call
252
+
253
+ yield run_obj.output, run_obj.status
249
254
  end
250
255
  run_thread.abort_on_exception = true
251
256
 
@@ -297,22 +302,46 @@ module Hatchet
297
302
  def setup!
298
303
  return self if @app_is_setup
299
304
  puts "Hatchet setup: #{name.inspect} for #{repo_name.inspect}"
300
- create_git_repo! unless is_git_repo?
301
305
  create_app
302
306
  set_labs!
303
307
  buildpack_list = @buildpacks.map { |pack| { buildpack: pack } }
304
308
  api_rate_limit.call.buildpack_installation.update(name, updates: buildpack_list)
305
309
  set_config @app_config
306
310
 
307
- call_before_deploy
308
311
  @app_is_setup = true
309
312
  self
310
313
  end
311
314
  alias :setup :setup!
312
315
 
313
- def before_deploy(&block)
316
+ private def in_dir_setup!
317
+ setup!
318
+ raise "Error you're in #{Dir.pwd} and might accidentally modify your disk contents" unless @already_in_dir
319
+ @in_dir_setup ||= begin
320
+ create_git_repo! unless is_git_repo?
321
+ call_before_deploy
322
+ true
323
+ end
324
+ end
325
+
326
+ def before_deploy(behavior = :default, &block)
314
327
  raise "block required" unless block
315
- @before_deploy = block
328
+
329
+ case behavior
330
+ when :default, :replace
331
+ if @before_deploy_array.any? && behavior == :default
332
+ STDERR.puts "Calling App#before_deploy multiple times will overwrite the contents. If you intended this: use `App#before_deploy(:replace)`"
333
+ STDERR.puts "In the future, calling this method with no arguements will default to `App#before_deploy(:append)` behavior.\n#{caller.join("\n")}"
334
+ end
335
+
336
+ @before_deploy_array.clear
337
+ @before_deploy_array << block
338
+ when :prepend
339
+ @before_deploy_array = [block] + @before_deploy_array
340
+ when :append
341
+ @before_deploy_array << block
342
+ else
343
+ raise "Unrecognized behavior: #{behavior.inspect}, valid inputs are :append, :prepend, and :replace"
344
+ end
316
345
 
317
346
  self
318
347
  end
@@ -338,14 +367,14 @@ module Hatchet
338
367
  @reaper.cycle if @app_is_setup
339
368
  end
340
369
 
341
- def in_directory(directory = self.directory)
342
- yield directory and return if @already_in_dir
370
+ def in_directory
371
+ yield and return if @already_in_dir
343
372
 
344
373
  Dir.mktmpdir do |tmpdir|
345
- FileUtils.cp_r("#{directory}/.", "#{tmpdir}/.")
374
+ FileUtils.cp_r("#{original_source_code_directory}/.", "#{tmpdir}/.")
346
375
  Dir.chdir(tmpdir) do
347
376
  @already_in_dir = true
348
- yield directory
377
+ yield
349
378
  @already_in_dir = false
350
379
  end
351
380
  end
@@ -379,9 +408,11 @@ module Hatchet
379
408
 
380
409
  def deploy(&block)
381
410
  in_directory do
382
- self.setup!
383
- self.push_with_retry!
384
- block.call(self, api_rate_limit.call, output) if block_given?
411
+ annotate_failures do
412
+ in_dir_setup!
413
+ self.push_with_retry!
414
+ block.call(self, api_rate_limit.call, output) if block_given?
415
+ end
385
416
  end
386
417
  ensure
387
418
  self.teardown! if block_given?
@@ -415,26 +446,26 @@ module Hatchet
415
446
  end
416
447
 
417
448
  def api_key
418
- @api_key ||= ENV['HEROKU_API_KEY'] || `heroku auth:token`.chomp
449
+ @api_key ||= ENV['HEROKU_API_KEY'] ||= `heroku auth:token`.chomp
419
450
  end
420
451
 
421
452
  def heroku
422
453
  raise "Not supported, use `platform_api` instead."
423
454
  end
424
455
 
425
- def run_ci(timeout: 300, &block)
456
+ def run_ci(timeout: 900, &block)
426
457
  in_directory do
427
458
  max_retries_count.times.retry do
428
459
  result = create_pipeline
429
460
  @pipeline_id = result["id"]
430
461
  end
431
462
 
432
- # when the CI run finishes, the associated ephemeral app created for the test run internally gets removed almost immediately
463
+ # When the CI run finishes, the associated ephemeral app created for the test run internally gets removed almost immediately
433
464
  # the system then sees a pipeline with no apps, and deletes it, also almost immediately
434
465
  # that would, with bad timing, mean our test run info poll in wait! would 403, and/or the delete_pipeline at the end
435
466
  # that's why we create an app explictly (or maybe it already exists), and then associate it with with the pipeline
436
467
  # the app will be auto cleaned up later
437
- self.setup!
468
+ in_dir_setup!
438
469
  max_retries_count.times.retry do
439
470
  couple_pipeline(@name, @pipeline_id)
440
471
  end
@@ -487,6 +518,9 @@ module Hatchet
487
518
 
488
519
  def delete_pipeline(pipeline_id)
489
520
  api_rate_limit.call.pipeline.delete(pipeline_id)
521
+ rescue Excon::Error::Forbidden
522
+ warn "Error deleting pipeline id: #{pipeline_id.inspect}, status: 403"
523
+ # Means the pipeline likely doesn't exist, not sure why though
490
524
  end
491
525
 
492
526
  def platform_api
@@ -526,14 +560,17 @@ module Hatchet
526
560
  end
527
561
 
528
562
  private def call_before_deploy
529
- return unless @before_deploy
530
- raise "before_deploy: #{@before_deploy.inspect} must respond to :call" unless @before_deploy.respond_to?(:call)
531
- raise "before_deploy: #{@before_deploy.inspect} must respond to :arity" unless @before_deploy.respond_to?(:arity)
563
+ return unless @before_deploy_array.any?
532
564
 
533
- if @before_deploy.arity == 1
534
- @before_deploy.call(self)
535
- else
536
- @before_deploy.call
565
+ @before_deploy_array.each do |block|
566
+ raise "before_deploy: #{block.inspect} must respond to :call" unless block.respond_to?(:call)
567
+ raise "before_deploy: #{block.inspect} must respond to :arity" unless block.respond_to?(:arity)
568
+
569
+ if block.arity == 1
570
+ block.call(self)
571
+ else
572
+ block.call
573
+ end
537
574
  end
538
575
 
539
576
  commit! if needs_commit?