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