heroku_hatchet 7.0.0 → 7.2.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.
- checksums.yaml +4 -4
- data/.github/workflows/check_changelog.yml +3 -2
- data/CHANGELOG.md +25 -0
- data/README.md +45 -2
- data/bin/hatchet +15 -4
- data/lib/hatchet.rb +2 -1
- data/lib/hatchet/app.rb +26 -16
- data/lib/hatchet/git_app.rb +2 -1
- data/lib/hatchet/init_project.rb +86 -0
- data/lib/hatchet/reaper.rb +1 -1
- data/lib/hatchet/templates/Gemfile.erb +5 -0
- data/lib/hatchet/templates/buildpack_spec.erb +23 -0
- data/lib/hatchet/templates/check_changelog.erb +13 -0
- data/lib/hatchet/templates/circleci_template.erb +45 -0
- data/lib/hatchet/templates/dependabot.erb +9 -0
- data/lib/hatchet/templates/hatchet_json.erb +11 -0
- data/lib/hatchet/templates/spec_helper.erb +30 -0
- data/lib/hatchet/version.rb +1 -1
- data/spec/hatchet/allow_failure_git_spec.rb +19 -4
- data/spec/hatchet/app_spec.rb +22 -0
- data/spec/hatchet/git_spec.rb +9 -3
- data/spec/hatchet/local_repo_spec.rb +8 -7
- data/spec/hatchet/lock_spec.rb +51 -0
- data/spec/unit/init_spec.rb +52 -0
- data/spec/unit/reaper_spec.rb +16 -0
- metadata +12 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2dd4a304828fca43576fed5a71b7be305142c3220c7dd020bff44b17a4a070a0
|
4
|
+
data.tar.gz: c0dee0a8ffb794f10a3dbb96beeafa26dd5f95b489eb9638a40bf408dce44c11
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f6fa0cef6167e9b4985b08f01e42f33410632d4dfb083551d2641e7009745a079044f606cc4456ed8b8f0dfb0ec087b9b3197d1dabb9a0c41cd7f96ee978e22c
|
7
|
+
data.tar.gz: 50689c87372d4df2fc7d354ccc0f6f870285283869a190f3272f76d0d238e2d3cd13ca485ca6436916a6976c847fbd2be057fbe4e99ed1aa8ac97abdfef9eeb9
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,30 @@
|
|
1
1
|
## HEAD
|
2
2
|
|
3
|
+
## 7.1.4
|
4
|
+
|
5
|
+
- App#setup! no longer modifies files on disk. (https://github.com/heroku/hatchet/pull/125)
|
6
|
+
- Add `$ hatchet init` command for bootstrapping new projects (https://github.com/heroku/hatchet/pull/123)
|
7
|
+
|
8
|
+
## 7.1.3
|
9
|
+
|
10
|
+
- Important!! Fix branch name detection on CircleCI (https://github.com/heroku/hatchet/pull/124)
|
11
|
+
|
12
|
+
## 7.1.2
|
13
|
+
|
14
|
+
- Fix support for Hatchet deploying the 'main' branch (https://github.com/heroku/hatchet/pull/122)
|
15
|
+
|
16
|
+
## 7.1.1
|
17
|
+
|
18
|
+
- Fix destroy_all functionality (https://github.com/heroku/hatchet/pull/121)
|
19
|
+
|
20
|
+
## 7.1.0
|
21
|
+
|
22
|
+
- Initializing an `App` can now take a `retries` key to overload the global hatchet env var (https://github.com/heroku/hatchet/pull/119)
|
23
|
+
- Calling `App#commit!` now adds an empty commit if there is no changes on disk (https://github.com/heroku/hatchet/pull/119)
|
24
|
+
- Bugfix: Failed release phase in deploys can now be re-run (https://github.com/heroku/hatchet/pull/119)
|
25
|
+
- Bugfix: Allow `hatchet lock` to be run against new projects (https://github.com/heroku/hatchet/pull/118)
|
26
|
+
- Bugfix: Allow `hatchet.lock` file to lock a repo to a different branch than what is specified as default for GitHub (https://github.com/heroku/hatchet/pull/118)
|
27
|
+
|
3
28
|
## 7.0.0
|
4
29
|
|
5
30
|
- ActiveSupport's Object#blank? and Object#present? are no longer provided by default (https://github.com/heroku/hatchet/pull/107)
|
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 `
|
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::
|
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
|
|
@@ -531,6 +572,8 @@ WARNING: Enabling `run_multi` setting on an app will charge your Heroku account
|
|
531
572
|
WARNING: Do not use `run_multi` if you're not using the `deploy` block syntax or manually call `teardown!` inside the text context [more info about how behavior does not work with the `after` block syntax in rspec](https://github.com/heroku/hatchet/issues/110).
|
532
573
|
WARNING: To work, `run_multi` requires your application to have a `web` process associated with it.
|
533
574
|
|
575
|
+
- `retries` (Integer): When passed in, this value will be used insead of the global `HATCHET_RETRIES` set via environment variable. When `allow_failures: true` is set as well as a retries value, then the application will not retry pushing to Heroku.
|
576
|
+
|
534
577
|
### App methods:
|
535
578
|
|
536
579
|
- `app.set_config()`: Updates the configuration on your app taking in a hash
|
data/bin/hatchet
CHANGED
@@ -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?
|
@@ -67,7 +73,7 @@ class HatchetCLI < Thor
|
|
67
73
|
desc "locks to specific git commits", "updates hatchet.lock"
|
68
74
|
def lock
|
69
75
|
lock_hash = {}
|
70
|
-
lockfile_hash = load_lockfile
|
76
|
+
lockfile_hash = load_lockfile(create_if_does_not_exist: true)
|
71
77
|
dirs.map do |directory, git_repo|
|
72
78
|
Threaded.later do
|
73
79
|
puts "== locking #{directory}"
|
@@ -118,10 +124,15 @@ class HatchetCLI < Thor
|
|
118
124
|
end
|
119
125
|
|
120
126
|
private
|
121
|
-
def load_lockfile
|
127
|
+
def load_lockfile(create_if_does_not_exist: false)
|
122
128
|
return YAML.safe_load(File.read('hatchet.lock')).to_h
|
123
129
|
rescue Errno::ENOENT
|
124
|
-
|
130
|
+
if create_if_does_not_exist
|
131
|
+
FileUtils.touch('hatchet.lock')
|
132
|
+
{}
|
133
|
+
else
|
134
|
+
raise "No such file found `hatchet.lock` please run `$ bundle exec hatchet lock`"
|
135
|
+
end
|
125
136
|
end
|
126
137
|
|
127
138
|
def bad_repo?(url)
|
@@ -151,7 +162,7 @@ class HatchetCLI < Thor
|
|
151
162
|
end
|
152
163
|
|
153
164
|
def checkout_commit(directory, commit)
|
154
|
-
cmd("cd #{directory} && git reset --hard #{commit}")
|
165
|
+
cmd("cd #{directory} && git fetch origin #{commit} && git checkout #{commit} && git checkout - && git reset --hard #{commit}")
|
155
166
|
end
|
156
167
|
|
157
168
|
def commit_at_directory(directory)
|
data/lib/hatchet.rb
CHANGED
@@ -18,6 +18,7 @@ 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'
|
21
22
|
|
22
23
|
module Hatchet
|
23
24
|
RETRIES = Integer(ENV['HATCHET_RETRIES'] || 1)
|
@@ -30,7 +31,7 @@ module Hatchet
|
|
30
31
|
return ENV['TRAVIS_PULL_REQUEST_BRANCH'] if ENV['TRAVIS_PULL_REQUEST_BRANCH'] && !ENV['TRAVIS_PULL_REQUEST_BRANCH'].empty?
|
31
32
|
return ENV['TRAVIS_BRANCH'] if ENV['TRAVIS_BRANCH']
|
32
33
|
|
33
|
-
out = `git
|
34
|
+
out = `git rev-parse --abbrev-ref HEAD`.strip
|
34
35
|
raise "Attempting to find current branch name. Error: Cannot describe git: #{out}" unless $?.success?
|
35
36
|
out
|
36
37
|
end
|
data/lib/hatchet/app.rb
CHANGED
@@ -9,7 +9,7 @@ module Hatchet
|
|
9
9
|
HATCHET_BUILDPACK_BRANCH = -> { ENV['HATCHET_BUILDPACK_BRANCH'] || ENV['HEROKU_TEST_RUN_BRANCH'] || Hatchet.git_branch }
|
10
10
|
BUILDPACK_URL = "https://github.com/heroku/heroku-buildpack-ruby.git"
|
11
11
|
|
12
|
-
attr_reader :name, :stack, :directory, :repo_name, :app_config, :buildpacks, :reaper
|
12
|
+
attr_reader :name, :stack, :directory, :repo_name, :app_config, :buildpacks, :reaper, :max_retries_count
|
13
13
|
|
14
14
|
class FailedDeploy < StandardError; end
|
15
15
|
|
@@ -55,6 +55,7 @@ module Hatchet
|
|
55
55
|
buildpack_url: nil,
|
56
56
|
before_deploy: nil,
|
57
57
|
run_multi: ENV["HATCHET_RUN_MULTI"],
|
58
|
+
retries: RETRIES,
|
58
59
|
config: {}
|
59
60
|
)
|
60
61
|
@repo_name = repo_name
|
@@ -68,6 +69,7 @@ module Hatchet
|
|
68
69
|
@buildpacks = Array(@buildpacks)
|
69
70
|
@buildpacks.map! {|b| b == :default ? self.class.default_buildpack : b}
|
70
71
|
@run_multi = run_multi
|
72
|
+
@max_retries_count = retries
|
71
73
|
|
72
74
|
if run_multi && !ENV["HATCHET_EXPENSIVE_MODE"]
|
73
75
|
raise "You're attempting to enable `run_multi: true` mode, but have not enabled `HATCHET_EXPENSIVE_MODE=1` env var to verify you understand the risks"
|
@@ -129,7 +131,7 @@ module Hatchet
|
|
129
131
|
end
|
130
132
|
|
131
133
|
def add_database(plan_name = 'heroku-postgresql:dev', match_val = "HEROKU_POSTGRESQL_[A-Z]+_URL")
|
132
|
-
|
134
|
+
max_retries_count.times.retry do
|
133
135
|
# heroku.post_addon(name, plan_name)
|
134
136
|
api_rate_limit.call.addon.create(name, plan: plan_name )
|
135
137
|
_, value = get_config.detect {|k, v| k.match(/#{match_val}/) }
|
@@ -295,19 +297,27 @@ module Hatchet
|
|
295
297
|
def setup!
|
296
298
|
return self if @app_is_setup
|
297
299
|
puts "Hatchet setup: #{name.inspect} for #{repo_name.inspect}"
|
298
|
-
create_git_repo! unless is_git_repo?
|
299
300
|
create_app
|
300
301
|
set_labs!
|
301
302
|
buildpack_list = @buildpacks.map { |pack| { buildpack: pack } }
|
302
303
|
api_rate_limit.call.buildpack_installation.update(name, updates: buildpack_list)
|
303
304
|
set_config @app_config
|
304
305
|
|
305
|
-
call_before_deploy
|
306
306
|
@app_is_setup = true
|
307
307
|
self
|
308
308
|
end
|
309
309
|
alias :setup :setup!
|
310
310
|
|
311
|
+
private def in_dir_setup!
|
312
|
+
setup!
|
313
|
+
raise "Error you're in #{Dir.pwd} and might accidentally modify your disk contents" unless @already_in_dir
|
314
|
+
@in_dir_setup ||= begin
|
315
|
+
create_git_repo! unless is_git_repo?
|
316
|
+
call_before_deploy
|
317
|
+
true
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
311
321
|
def before_deploy(&block)
|
312
322
|
raise "block required" unless block
|
313
323
|
@before_deploy = block
|
@@ -316,7 +326,7 @@ module Hatchet
|
|
316
326
|
end
|
317
327
|
|
318
328
|
def commit!
|
319
|
-
local_cmd_exec!('git add .; git commit -m next')
|
329
|
+
local_cmd_exec!('git add .; git commit --allow-empty -m next')
|
320
330
|
end
|
321
331
|
|
322
332
|
def push_without_retry!
|
@@ -377,7 +387,7 @@ module Hatchet
|
|
377
387
|
|
378
388
|
def deploy(&block)
|
379
389
|
in_directory do
|
380
|
-
|
390
|
+
in_dir_setup!
|
381
391
|
self.push_with_retry!
|
382
392
|
block.call(self, api_rate_limit.call, output) if block_given?
|
383
393
|
end
|
@@ -386,12 +396,12 @@ module Hatchet
|
|
386
396
|
end
|
387
397
|
|
388
398
|
def push
|
389
|
-
|
390
|
-
|
399
|
+
retry_count = allow_failure? ? 1 : max_retries_count
|
400
|
+
retry_count.times.retry do |attempt|
|
391
401
|
begin
|
392
402
|
@output = self.push_without_retry!
|
393
403
|
rescue StandardError => error
|
394
|
-
puts retry_error_message(error, attempt
|
404
|
+
puts retry_error_message(error, attempt) unless retry_count == 1
|
395
405
|
raise error
|
396
406
|
end
|
397
407
|
end
|
@@ -400,10 +410,10 @@ module Hatchet
|
|
400
410
|
alias :push_with_retry :push
|
401
411
|
alias :push_with_retry! :push_with_retry
|
402
412
|
|
403
|
-
def retry_error_message(error, attempt
|
413
|
+
def retry_error_message(error, attempt)
|
404
414
|
attempt += 1
|
405
|
-
return "" if attempt ==
|
406
|
-
msg = "\nRetrying failed Attempt ##{attempt}/#{
|
415
|
+
return "" if attempt == max_retries_count
|
416
|
+
msg = "\nRetrying failed Attempt ##{attempt}/#{max_retries_count} to push for '#{name}' due to error: \n"<<
|
407
417
|
"#{error.class} #{error.message}\n #{error.backtrace.join("\n ")}"
|
408
418
|
return msg
|
409
419
|
end
|
@@ -422,7 +432,7 @@ module Hatchet
|
|
422
432
|
|
423
433
|
def run_ci(timeout: 300, &block)
|
424
434
|
in_directory do
|
425
|
-
|
435
|
+
max_retries_count.times.retry do
|
426
436
|
result = create_pipeline
|
427
437
|
@pipeline_id = result["id"]
|
428
438
|
end
|
@@ -432,8 +442,8 @@ module Hatchet
|
|
432
442
|
# that would, with bad timing, mean our test run info poll in wait! would 403, and/or the delete_pipeline at the end
|
433
443
|
# that's why we create an app explictly (or maybe it already exists), and then associate it with with the pipeline
|
434
444
|
# the app will be auto cleaned up later
|
435
|
-
|
436
|
-
|
445
|
+
in_dir_setup!
|
446
|
+
max_retries_count.times.retry do
|
437
447
|
couple_pipeline(@name, @pipeline_id)
|
438
448
|
end
|
439
449
|
|
@@ -446,7 +456,7 @@ module Hatchet
|
|
446
456
|
api_rate_limit: api_rate_limit
|
447
457
|
)
|
448
458
|
|
449
|
-
|
459
|
+
max_retries_count.times.retry do
|
450
460
|
test_run.create_test_run
|
451
461
|
end
|
452
462
|
test_run.wait!(&block)
|
data/lib/hatchet/git_app.rb
CHANGED
@@ -25,7 +25,7 @@ module Hatchet
|
|
25
25
|
end
|
26
26
|
|
27
27
|
private def git_push_heroku_yall
|
28
|
-
output = `git push #{git_repo}
|
28
|
+
output = `git push #{git_repo} HEAD:main 2>&1`
|
29
29
|
|
30
30
|
if !$?.success?
|
31
31
|
raise FailedDeployError.new(self, "Buildpack: #{@buildpack.inspect}\nRepo: #{git_repo}", output: output)
|
@@ -33,6 +33,7 @@ module Hatchet
|
|
33
33
|
|
34
34
|
releases = platform_api.release.list(name)
|
35
35
|
if releases.last["status"] == "failed"
|
36
|
+
commit! # An empty commit allows us to deploy again
|
36
37
|
raise FailedReleaseError.new(self, "Buildpack: #{@buildpack.inspect}\nRepo: #{git_repo}", output: output)
|
37
38
|
end
|
38
39
|
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Hatchet
|
5
|
+
# Bootstraps a project with files for running hatchet tests
|
6
|
+
#
|
7
|
+
# Hatchet::InitProject.new.call
|
8
|
+
#
|
9
|
+
# puts File.exist?("spec/spec_helper.rb") # => true
|
10
|
+
# puts File.exist?("") # => true
|
11
|
+
class InitProject
|
12
|
+
def initialize(dir: ".", io: STDOUT)
|
13
|
+
|
14
|
+
@target_dir = Pathname.new(dir)
|
15
|
+
raise "Must run in a directory with a buildpack, #{@target_dir} has no bin/ directory" unless @target_dir.join("bin").directory?
|
16
|
+
|
17
|
+
@template_dir = Pathname.new(__dir__).join("templates")
|
18
|
+
@thor_shell = ::Thor::Shell::Basic.new
|
19
|
+
@io = io
|
20
|
+
@git_ignore = @target_dir.join(".gitignore")
|
21
|
+
|
22
|
+
FileUtils.touch(@git_ignore)
|
23
|
+
FileUtils.touch(@target_dir.join("hatchet.lock"))
|
24
|
+
end
|
25
|
+
|
26
|
+
def call
|
27
|
+
write_target(target: ".circleci/config.yml", template: "circleci_template.erb")
|
28
|
+
write_target(target: "Gemfile", template: "Gemfile.erb")
|
29
|
+
write_target(target: "hatchet.json", template: "hatchet_json.erb")
|
30
|
+
write_target(target: "spec/spec_helper.rb", template: "spec_helper.erb")
|
31
|
+
write_target(target: "spec/hatchet/buildpack_spec.rb", template: "buildpack_spec.erb")
|
32
|
+
write_target(target: ".github/dependabot.yml", template: "dependabot.erb")
|
33
|
+
write_target(target: ".github/workflows/check_changelog.yml", template: "check_changelog.erb")
|
34
|
+
|
35
|
+
add_gitignore(".rspec_status")
|
36
|
+
add_gitignore("repos/*")
|
37
|
+
|
38
|
+
stream("cd #{@target_dir} && bundle install")
|
39
|
+
stream("cd #{@target_dir} && hatchet install")
|
40
|
+
|
41
|
+
@io.puts
|
42
|
+
@io.puts "Done, run `bundle exec rspec` to execute your tests"
|
43
|
+
@io.puts
|
44
|
+
end
|
45
|
+
|
46
|
+
private def add_gitignore(statement)
|
47
|
+
@git_ignore.open("a") {|f| f.puts statement } unless @git_ignore.read.include?(statement)
|
48
|
+
end
|
49
|
+
|
50
|
+
private def stream(command)
|
51
|
+
output = ""
|
52
|
+
IO.popen(command) do |io|
|
53
|
+
until io.eof?
|
54
|
+
buffer = io.gets
|
55
|
+
output << buffer
|
56
|
+
@io.puts(buffer)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
raise "Error running #{command}. Output:\n#{output}" unless $?.success?
|
60
|
+
output
|
61
|
+
end
|
62
|
+
|
63
|
+
private def write_target(template: nil, target:, contents: nil)
|
64
|
+
if template
|
65
|
+
template = @template_dir.join(template)
|
66
|
+
contents = ERB.new(template.read).result(binding)
|
67
|
+
end
|
68
|
+
|
69
|
+
target = @target_dir.join(target)
|
70
|
+
target.dirname.mkpath # Create directory if it doesn't exist already
|
71
|
+
|
72
|
+
if target.exist?
|
73
|
+
return if contents === target.read # identical
|
74
|
+
target.write(contents) if @thor_shell.file_collision(target) { contents }
|
75
|
+
else
|
76
|
+
target.write(contents)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private def cmd(command)
|
81
|
+
result = `#{command}`.chomp
|
82
|
+
raise "Command #{command} failed:\n#{result}" unless $?.success?
|
83
|
+
result
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/hatchet/reaper.rb
CHANGED
@@ -0,0 +1,23 @@
|
|
1
|
+
require_relative "../spec_helper.rb"
|
2
|
+
|
3
|
+
RSpec.describe "This buildpack" do
|
4
|
+
it "has its own tests" do
|
5
|
+
raise "delete this and replace it with your own logic"
|
6
|
+
|
7
|
+
# Specify where you want your buildpack to go using :default
|
8
|
+
buildpacks = [:default, "heroku/ruby"]
|
9
|
+
|
10
|
+
# To deploy a different app modify the hatchet.json or
|
11
|
+
# commit an app to your source control and use a path
|
12
|
+
# instead of "default_ruby" here
|
13
|
+
Hatchet::Runner.new("default_ruby", buildpacks: buildpacks).tap do |app|
|
14
|
+
app.before_deploy do
|
15
|
+
# Modfiy the app here if you need
|
16
|
+
end
|
17
|
+
app.deploy do
|
18
|
+
# Assert the behavior you desire here
|
19
|
+
expect(app.output).to match("deployed to Heroku")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
name: Check Changelog
|
2
|
+
|
3
|
+
on:
|
4
|
+
pull_request:
|
5
|
+
types: [opened, reopened, edited, synchronize]
|
6
|
+
jobs:
|
7
|
+
build:
|
8
|
+
runs-on: ubuntu-latest
|
9
|
+
steps:
|
10
|
+
- uses: actions/checkout@v1
|
11
|
+
- name: Check that CHANGELOG is touched
|
12
|
+
run: |
|
13
|
+
cat $GITHUB_EVENT_PATH | jq .pull_request.title | grep -i '\[\(\(changelog skip\)\|\(ci skip\)\)\]' || git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md
|
@@ -0,0 +1,45 @@
|
|
1
|
+
version: 2
|
2
|
+
references:
|
3
|
+
unit: &unit
|
4
|
+
run:
|
5
|
+
name: Run test suite
|
6
|
+
command: PARALLEL_SPLIT_TEST_PROCESSES=25 IS_RUNNING_ON_CI=1 bundle exec parallel_split_test spec/
|
7
|
+
restore: &restore
|
8
|
+
restore_cache:
|
9
|
+
keys:
|
10
|
+
- v1_bundler_deps-{{ .Environment.CIRCLE_JOB }}
|
11
|
+
save: &save
|
12
|
+
save_cache:
|
13
|
+
paths:
|
14
|
+
- ./vendor/bundle
|
15
|
+
key: v1_bundler_deps-{{ .Environment.CIRCLE_JOB }} # CIRCLE_JOB e.g. "ruby-2.5"
|
16
|
+
hatchet_setup: &hatchet_setup
|
17
|
+
run:
|
18
|
+
name: Hatchet setup
|
19
|
+
command: |
|
20
|
+
bundle exec hatchet ci:setup
|
21
|
+
bundle: &bundle
|
22
|
+
run:
|
23
|
+
name: install dependencies
|
24
|
+
command: |
|
25
|
+
bundle install --jobs=4 --retry=3 --path vendor/bundle
|
26
|
+
bundle update
|
27
|
+
bundle clean
|
28
|
+
jobs:
|
29
|
+
"ruby-2.7":
|
30
|
+
docker:
|
31
|
+
- image: circleci/ruby:2.7
|
32
|
+
steps:
|
33
|
+
- checkout
|
34
|
+
- <<: *restore
|
35
|
+
- <<: *bundle
|
36
|
+
- <<: *hatchet_setup
|
37
|
+
- <<: *unit
|
38
|
+
- <<: *save
|
39
|
+
|
40
|
+
workflows:
|
41
|
+
version: 2
|
42
|
+
build:
|
43
|
+
jobs:
|
44
|
+
- "ruby-2.7"
|
45
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
|
3
|
+
require 'rspec/retry'
|
4
|
+
|
5
|
+
ENV["HATCHET_BUILDPACK_BASE"] = "<%= cmd("git config --get remote.origin.url") %>"
|
6
|
+
|
7
|
+
require 'hatchet'
|
8
|
+
require 'pathname'
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
# Enable flags like --only-failures and --next-failure
|
12
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
13
|
+
config.verbose_retry = true # show retry status in spec process
|
14
|
+
config.default_retry_count = 2 if ENV['IS_RUNNING_ON_CI'] # retry all tests that fail again
|
15
|
+
|
16
|
+
config.expect_with :rspec do |c|
|
17
|
+
c.syntax = :expect
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def run!(cmd)
|
22
|
+
out = `#{cmd}`
|
23
|
+
raise "Error running #{cmd}, output: #{out}" unless $?.success?
|
24
|
+
out
|
25
|
+
end
|
26
|
+
|
27
|
+
def spec_dir
|
28
|
+
Pathname.new(__dir__)
|
29
|
+
end
|
30
|
+
|
data/lib/hatchet/version.rb
CHANGED
@@ -13,14 +13,29 @@ describe "AllowFailureGitTest" do
|
|
13
13
|
}
|
14
14
|
|
15
15
|
it "is marked as a failure if the release fails" do
|
16
|
+
app = Hatchet::GitApp.new("default_ruby", before_deploy: release_fail_proc, retries: 2)
|
17
|
+
def app.retry_error_message(*args); @test_attempts_count ||= 0; @test_attempts_count += 1; "" end
|
18
|
+
def app.test_attempts_count; @test_attempts_count ; end
|
19
|
+
|
16
20
|
expect {
|
17
|
-
|
18
|
-
}.to
|
21
|
+
app.deploy {}
|
22
|
+
}.to raise_error { |error|
|
23
|
+
expect(error).to be_a(Hatchet::App::FailedReleaseError)
|
24
|
+
expect(error.message).to_not match("Everything up-to-date")
|
25
|
+
}
|
26
|
+
|
27
|
+
expect(app.test_attempts_count).to eq(2)
|
19
28
|
end
|
20
29
|
|
21
30
|
it "works when failure is allowed" do
|
22
|
-
Hatchet::GitApp.new("default_ruby", before_deploy: release_fail_proc, allow_failure: true).
|
23
|
-
|
31
|
+
Hatchet::GitApp.new("default_ruby", before_deploy: release_fail_proc, allow_failure: true, retries: 3).tap do |app|
|
32
|
+
def app.retry_error_message(*args); @test_attempts_count ||= 0; @test_attempts_count += 1; "" end
|
33
|
+
def app.test_attempts_count; @test_attempts_count ; end
|
34
|
+
|
35
|
+
app.deploy do
|
36
|
+
expect(app.output).to match("failing on release")
|
37
|
+
expect(app.test_attempts_count).to eq(nil)
|
38
|
+
end
|
24
39
|
end
|
25
40
|
end
|
26
41
|
end
|
data/spec/hatchet/app_spec.rb
CHANGED
@@ -1,6 +1,28 @@
|
|
1
1
|
require("spec_helper")
|
2
2
|
|
3
3
|
describe "AppTest" do
|
4
|
+
it "does not modify local files by mistake" do
|
5
|
+
Dir.mktmpdir do |dir_1|
|
6
|
+
app = Hatchet::Runner.new(dir_1)
|
7
|
+
Dir.mktmpdir do |dir_2|
|
8
|
+
Dir.chdir(dir_2) do
|
9
|
+
FileUtils.touch("foo.txt")
|
10
|
+
|
11
|
+
app.setup!
|
12
|
+
end
|
13
|
+
|
14
|
+
entries_array = Dir.entries(dir_2)
|
15
|
+
entries_array -= ["..", ".", "foo.txt"]
|
16
|
+
expect(entries_array).to be_empty
|
17
|
+
|
18
|
+
|
19
|
+
entries_array = Dir.entries(dir_1)
|
20
|
+
entries_array -= ["..", ".", "foo.txt"]
|
21
|
+
expect(entries_array).to be_empty
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
4
26
|
it "rate throttles `git push` " do
|
5
27
|
app = Hatchet::GitApp.new("default_ruby")
|
6
28
|
def app.git_push_heroku_yall
|
data/spec/hatchet/git_spec.rb
CHANGED
@@ -1,9 +1,15 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
3
|
describe "GitAppTest" do
|
4
|
-
it "can deploy git app" do
|
5
|
-
Hatchet::GitApp.new("
|
6
|
-
expect(app.
|
4
|
+
it "can deploy git app to the main branch" do
|
5
|
+
Hatchet::GitApp.new("lock_fail_main", allow_failure: true).deploy do |app|
|
6
|
+
expect(app.output).to match("INTENTIONAL ERROR")
|
7
7
|
end
|
8
8
|
end
|
9
|
+
|
10
|
+
it "returns the correct branch name on circle CI" do
|
11
|
+
skip("only runs on circle") unless ENV["CIRCLE_BRANCH"]
|
12
|
+
|
13
|
+
expect(Hatchet.git_branch).to eq(ENV["CIRCLE_BRANCH"])
|
14
|
+
end
|
9
15
|
end
|
@@ -13,14 +13,15 @@ describe "LocalRepoTest" do
|
|
13
13
|
|
14
14
|
it "repos checked into git" do
|
15
15
|
begin
|
16
|
-
|
17
|
-
app.
|
18
|
-
|
19
|
-
|
20
|
-
|
16
|
+
fixture_dir = "repo_fixtures/different-folder-for-checked-in-repos/default_ruby"
|
17
|
+
app = Hatchet::App.new(fixture_dir)
|
18
|
+
def app.push_with_retry!; end
|
19
|
+
|
20
|
+
expect(Dir.exist?("#{fixture_dir}/.git")).to be_falsey
|
21
|
+
|
22
|
+
app.deploy do
|
23
|
+
expect(Dir.exist?(".git")).to be_truthy
|
21
24
|
end
|
22
|
-
ensure
|
23
|
-
app.teardown! if app
|
24
25
|
end
|
25
26
|
end
|
26
27
|
end
|
data/spec/hatchet/lock_spec.rb
CHANGED
@@ -28,3 +28,54 @@ describe "LockTest" do
|
|
28
28
|
expect(branch).to eq("main")
|
29
29
|
end
|
30
30
|
end
|
31
|
+
|
32
|
+
describe "isolated lock tests" do
|
33
|
+
it "works when there's no hatchet.lock" do
|
34
|
+
Dir.mktmpdir do |dir|
|
35
|
+
dir = Pathname.new(dir)
|
36
|
+
|
37
|
+
dir.join("hatchet.json").open("w+") do |f|
|
38
|
+
f.puts %Q{{ "foo": ["sharpstone/lock_fail_main_default_is_master"] }}
|
39
|
+
end
|
40
|
+
|
41
|
+
output = `cd #{dir} && hatchet lock 2>&1`
|
42
|
+
|
43
|
+
raise "Expected cmd `hatchet lock` to succeed, but it did not: #{output}" unless $?.success?
|
44
|
+
expect(output).to include("locking")
|
45
|
+
|
46
|
+
lockfile_contents = dir.join('hatchet.lock').read
|
47
|
+
expect(lockfile_contents).to include("repos/foo/lock_fail_main_default_is_master")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it "works when a project is locked to main but the default branch is master" do
|
52
|
+
Dir.mktmpdir do |dir|
|
53
|
+
dir = Pathname.new(dir)
|
54
|
+
|
55
|
+
dir.join("hatchet.json").open("w+") do |f|
|
56
|
+
f.puts %Q{{ "foo": ["sharpstone/lock_fail_main_default_is_master"] }}
|
57
|
+
end
|
58
|
+
|
59
|
+
dir.join("hatchet.lock").open("w+") do |f|
|
60
|
+
f.puts <<~EOM
|
61
|
+
---
|
62
|
+
- - "./repos/foo/lock_fail_main_default_is_master"
|
63
|
+
- main
|
64
|
+
EOM
|
65
|
+
end
|
66
|
+
|
67
|
+
output = `cd #{dir} && hatchet install 2>&1`
|
68
|
+
|
69
|
+
raise "Expected cmd `hatchet install` to succeed, but it did not:\n#{output}" unless $?.success?
|
70
|
+
expect(output).to include("Installing")
|
71
|
+
|
72
|
+
lockfile_contents = dir.join('hatchet.lock').read
|
73
|
+
contents = YAML.safe_load(lockfile_contents).to_h
|
74
|
+
expect(contents).to eq({"./repos/foo/lock_fail_main_default_is_master" => "main"})
|
75
|
+
|
76
|
+
contents.each do |repo_dir, commit_or_branch|
|
77
|
+
expect(`cd #{dir.join(repo_dir)} && git describe --contains --all HEAD`).to match("main")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "Hatchet::Init" do
|
4
|
+
def fake_buildpack_dir
|
5
|
+
Dir.mktmpdir do |dir|
|
6
|
+
FileUtils.mkdir_p("#{dir}/bin")
|
7
|
+
yield dir
|
8
|
+
end
|
9
|
+
end
|
10
|
+
it "raises an error when not pointing at the right directory" do
|
11
|
+
Dir.mktmpdir do |dir|
|
12
|
+
expect {
|
13
|
+
Hatchet::InitProject.new(dir: dir)
|
14
|
+
}.to raise_error(/Must run in a directory with a buildpack/)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# write_target(target: ".circleci/config.yml", template: "circleci_template.erb")
|
19
|
+
# write_target(target: "Gemfile", template: "Gemfile.erb")
|
20
|
+
# write_target(target: "hatchet.json", contents: "{}")
|
21
|
+
# write_target(target: "hatchet.lock", contents: YAML.dump({}))
|
22
|
+
# write_target(target: "spec/spec_helper.rb", template: "spec_helper.erb")
|
23
|
+
# write_target(target: "spec/hatchet/buildpack_spec.rb", template: "buildpack_spec.erb")
|
24
|
+
# write_target(target: ".github/dependabot.yml", template: "dependabot.erb")
|
25
|
+
|
26
|
+
it "generates files" do
|
27
|
+
fake_buildpack_dir do |dir|
|
28
|
+
fake_stdout = StringIO.new
|
29
|
+
init = Hatchet::InitProject.new(dir: dir, io: fake_stdout)
|
30
|
+
init.call
|
31
|
+
|
32
|
+
circle_ci_file = Pathname.new(dir).join(".circleci/config.yml")
|
33
|
+
expect(circle_ci_file.read).to match("parallel_split_test")
|
34
|
+
|
35
|
+
%W{
|
36
|
+
.circleci/config.yml
|
37
|
+
Gemfile
|
38
|
+
hatchet.json
|
39
|
+
hatchet.lock
|
40
|
+
spec/spec_helper.rb
|
41
|
+
spec/hatchet/buildpack_spec.rb
|
42
|
+
.github/dependabot.yml
|
43
|
+
.github/workflows/check_changelog.yml
|
44
|
+
.gitignore
|
45
|
+
}.each do |path|
|
46
|
+
expect(Pathname.new(dir).join(path)).to exist
|
47
|
+
end
|
48
|
+
|
49
|
+
expect(fake_stdout.string).to match("Bundle complete")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/spec/unit/reaper_spec.rb
CHANGED
@@ -1,6 +1,22 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
3
|
describe "Reaper" do
|
4
|
+
it "destroy all" do
|
5
|
+
reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 1, io: StringIO.new)
|
6
|
+
|
7
|
+
def reaper.get_heroku_apps
|
8
|
+
@mock_apps ||= [
|
9
|
+
{"name" => "hatchet-t-unfinished", "id" => 2, "maintenance" => false, "created_at" => Time.now.to_s},
|
10
|
+
{"name" => "hatchet-t-foo", "id" => 1, "maintenance" => true, "created_at" => Time.now.to_s}
|
11
|
+
]
|
12
|
+
end
|
13
|
+
def reaper.destroy_with_log(*args); @destroy_with_log_count ||= 0; @destroy_with_log_count += 1; end
|
14
|
+
|
15
|
+
reaper.destroy_all
|
16
|
+
|
17
|
+
expect(reaper.instance_variable_get("@destroy_with_log_count")).to eq(1)
|
18
|
+
end
|
19
|
+
|
4
20
|
describe "cycle" do
|
5
21
|
it "does not delete anything if under the limit" do
|
6
22
|
reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 1, io: StringIO.new)
|
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
|
+
version: 7.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Richard Schneeman
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-09-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: platform-api
|
@@ -192,11 +192,19 @@ files:
|
|
192
192
|
- lib/hatchet/app.rb
|
193
193
|
- lib/hatchet/config.rb
|
194
194
|
- lib/hatchet/git_app.rb
|
195
|
+
- lib/hatchet/init_project.rb
|
195
196
|
- lib/hatchet/reaper.rb
|
196
197
|
- lib/hatchet/reaper/app_age.rb
|
197
198
|
- lib/hatchet/reaper/reaper_throttle.rb
|
198
199
|
- lib/hatchet/shell_throttle.rb
|
199
200
|
- lib/hatchet/tasks.rb
|
201
|
+
- lib/hatchet/templates/Gemfile.erb
|
202
|
+
- lib/hatchet/templates/buildpack_spec.erb
|
203
|
+
- lib/hatchet/templates/check_changelog.erb
|
204
|
+
- lib/hatchet/templates/circleci_template.erb
|
205
|
+
- lib/hatchet/templates/dependabot.erb
|
206
|
+
- lib/hatchet/templates/hatchet_json.erb
|
207
|
+
- lib/hatchet/templates/spec_helper.erb
|
200
208
|
- lib/hatchet/test_run.rb
|
201
209
|
- lib/hatchet/version.rb
|
202
210
|
- repo_fixtures/different-folder-for-checked-in-repos/default_ruby/Gemfile
|
@@ -211,6 +219,7 @@ files:
|
|
211
219
|
- spec/hatchet/local_repo_spec.rb
|
212
220
|
- spec/hatchet/lock_spec.rb
|
213
221
|
- spec/spec_helper.rb
|
222
|
+
- spec/unit/init_spec.rb
|
214
223
|
- spec/unit/reaper_spec.rb
|
215
224
|
- spec/unit/shell_throttle.rb
|
216
225
|
- tmp/parallel_runtime_test.log
|
@@ -248,5 +257,6 @@ test_files:
|
|
248
257
|
- spec/hatchet/local_repo_spec.rb
|
249
258
|
- spec/hatchet/lock_spec.rb
|
250
259
|
- spec/spec_helper.rb
|
260
|
+
- spec/unit/init_spec.rb
|
251
261
|
- spec/unit/reaper_spec.rb
|
252
262
|
- spec/unit/shell_throttle.rb
|