heroku_hatchet 1.4.3 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/CHANGELOG.md +5 -0
- data/README.md +67 -53
- data/hatchet.gemspec +1 -0
- data/hatchet.json +5 -1
- data/lib/hatchet.rb +12 -24
- data/lib/hatchet/anvil_app.rb +1 -58
- data/lib/hatchet/app.rb +62 -0
- data/lib/hatchet/git_app.rb +0 -9
- data/lib/hatchet/reaper.rb +1 -1
- data/lib/hatchet/test_run.rb +192 -0
- data/lib/hatchet/version.rb +1 -1
- data/test/hatchet/ci_test.rb +24 -0
- data/test/hatchet/config_test.rb +23 -12
- data/test/test_helper.rb +3 -3
- metadata +20 -7
- data/test/hatchet/anvil_test.rb +0 -7
- data/test/hatchet/runner_test.rb +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a9f803b82d8440304dca2283cdcb3f1f9d689094
|
4
|
+
data.tar.gz: bfd8d1b84f7fdd80f53535905f474bfdc82d5e59
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7f7cfcda96ee5f0222afc0cb70b2119007a8efadd6f72dd389b25df6754a46c7bb4336e88591e6678066f681ae4381af3f124221fb3b3ebdff24ec1435d2176a
|
7
|
+
data.tar.gz: cf49871c076c769bf1bcd5f3cc24f4c1607de040ef684b3dd493fb5073dde7af70e3baff264406801aade89c423ce0378de8f7e43877af969798be29856abfa1
|
data/.travis.yml
CHANGED
@@ -9,4 +9,4 @@ env:
|
|
9
9
|
- HATCHET_BUILDPACK_BRANCH=master
|
10
10
|
- HATCHET_RETRIES=3
|
11
11
|
- HATCHET_APP_LIMIT=80
|
12
|
-
- secure:
|
12
|
+
- secure: TvpZ0CrIe0FqjyTUOAtVqjHHrtF1esMroa00bYYRBas050/y7ygVpAn9utFZZChgt1PUbM48I01UaQglGxtmXVl3ahQXtPpXlzlwJOlDS09dlZFfLenkv530/pxIlpRtqk4q18gCoLBblXX7RZu3TGt0qds+o8dQrBzL6QifAHs=
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -38,13 +38,25 @@ To prevent regressions and to make pushing out new features faster and easier.
|
|
38
38
|
|
39
39
|
## What can Hatchet Test?
|
40
40
|
|
41
|
-
Hatchet can easily test deployment of buildpacks, getting the build output, and running arbitrary interactive processes such as `heroku run bash`.
|
41
|
+
Hatchet can easily test deployment of buildpacks, getting the build output, and running arbitrary interactive processes such as `heroku run bash`. It can also test running CI against an app.
|
42
|
+
|
43
|
+
## Writing Tests
|
44
|
+
|
45
|
+
Hatchet is test framework agnostic. [This project](https://github.com/heroku/hatchet) uses `Test::Unit` to run it's own tests. While the [heroku-ruby-buildpack](https://github.com/heroku/heroku-buildpack-ruby) uses rspec.
|
46
|
+
|
47
|
+
Rspec has a number of advantages, the ability to run `focused: true` to only run the exact test you want as well as the ability to tag tests. Rspec also has a number of useful plugins, one especialy useful one is `gem 'rspec-retry'` which will re-run any failed tests a given number of times (I recommend setting this to at least 2) this decrease the number of false negatives your tests will have.
|
48
|
+
|
49
|
+
Whatever testing framework you chose, we recommend using a parallel test runner when running the full suite [parallel_tests](https://github.com/grosser/parallel_tests) works with rspec and test::unit and is amazing.
|
50
|
+
|
51
|
+
If you're unfamiliar with the ruby testing eco-system or want some help, looking at existing projects is a good place to get started.
|
52
|
+
|
53
|
+
There is a section below on getting Hatchet to work on Travis.
|
42
54
|
|
43
55
|
## Testing a Buildpack
|
44
56
|
|
45
57
|
Hatchet was built for testing the Ruby buildpack, but you can use it to test any buildpack you desire provided you don't mind writing your tests written in Ruby.
|
46
58
|
|
47
|
-
You will need copies of applications that can be deployed by your buildpack. You can see the ones for the Hatchet unit tests (and the Ruby buildpack) https://github.com/sharpstone. Hatchet does not require that you keep these apps checked into your git repo which would make fetching your buildpack slow instead declare them in a `hatchet.json` file (see below).
|
59
|
+
You will need copies of applications that can be deployed by your buildpack. You can see the ones for the Hatchet unit tests (and the Ruby buildpack) https://github.com/sharpstone. Hatchet does not require that you keep these apps checked into your git repo which would make fetching your buildpack slow, instead declare them in a `hatchet.json` file (see below).
|
48
60
|
|
49
61
|
Hatchet will automate retrieving these files `$ hatchet install`, as well as deploying them using your local copy of the buildpack, retrieving the build output and running commands against deploying applications.
|
50
62
|
|
@@ -100,38 +112,19 @@ spontaneously fail. In the future we may create a hatchet.lockfile or
|
|
100
112
|
something to declare the commit
|
101
113
|
|
102
114
|
|
103
|
-
## Deployments: Anvil vs. Git
|
104
|
-
|
105
|
-
Before you start testing a buildpack, understand that there are two different ways to deploy to Heroku. The first you are likely familiar with Git, requires a `git push heroku master`. You can configure the buildpack of an app being deployed in this way through the `BUILDPACK_URL` environment variable of the app. The buildpack url must be publicaly available.
|
106
|
-
|
107
|
-
The second method is by [Anvil](https://github.com/ddollar/anvil-cli) . In this method the build is performed as a service. This service takes an app to be built as well as a buildpack. When using Anvil, you do not need to put your buildpack anywhere publicly available.
|
108
|
-
|
109
|
-
When developing local features you will likely wish to use Anvil since it does not require a publicly available URL and you can iterate faster. When testing for regression you will almost always want to use Git, since it is the closest approximation to real world deployment. For this reason Hatchet provides a globally configurable way to toggle between the two deployment modes `Hatchet::Runner`
|
110
|
-
|
111
|
-
|
112
115
|
## Deploying apps
|
113
116
|
|
114
|
-
Now that you've got your apps locally you can have hatchet deploy them for you. Hatchet can deploy using one of two ways Anvil and Git. To specify one or the other set your `HATCHET_DEPLOY_STRATEGY` environment variable to `anvil` or `git`. The default is `anvil`. In production, you should always test against `git`
|
115
|
-
|
116
|
-
|
117
|
-
A `Hatchet::GitApp` will deploy using the standard `git push heroku master` and is not configurable, if you use this option you need to have a publicly accessible copy of your buildpack. Using Git to test your buildpack may be slow and require you to frequently push your buildpack to a public git repo. For this reason we recommend using Anvil to run your tests locally:
|
118
|
-
|
119
|
-
```ruby
|
120
|
-
Hatchet::Runner.new("rails3_mri_193").deploy do |app|
|
121
|
-
|
122
|
-
end
|
123
|
-
```
|
124
117
|
|
125
|
-
|
118
|
+
You can specify the location of your public buildpack url in an environment variable:
|
126
119
|
|
127
120
|
```sh
|
128
121
|
HATCHET_BUILDPACK_BASE=https://github.com/heroku/heroku-buildpack-ruby.git
|
129
122
|
HATCHET_BUILDPACK_BRANCH=master
|
130
123
|
```
|
131
124
|
|
132
|
-
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.
|
125
|
+
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 travis (by leaving `HATCHET_BUILDPACK_BRANCH` blank).
|
133
126
|
|
134
|
-
Deploys are expected to work, if the `ENV['HATCHET_RETRIES']` is set, then deploys will be automatically retried that number of times. Due to testing using a network and random
|
127
|
+
Deploys are expected to work, if the `ENV['HATCHET_RETRIES']` is set, then deploys will be automatically retried that number of times. Due to testing using a network and random failures, setting this value to `3` retries seems to work well. If an app cannot be deployed within its allotted number of retries an error will be raised.
|
135
128
|
|
136
129
|
If you are testing an app that is supposed to fail deployment you can set the `allow_failure: true` flag when creating the app:
|
137
130
|
|
@@ -139,7 +132,7 @@ If you are testing an app that is supposed to fail deployment you can set the `a
|
|
139
132
|
Hatchet::Runner.new("no_lockfile", allow_failure: true).deploy do |app|
|
140
133
|
```
|
141
134
|
|
142
|
-
After the block finishes your app will be removed from heroku. If you are investigating a deploy, you can add the `debug: true` flag to your app:
|
135
|
+
After the block finishes your app will be queued to be removed from heroku. If you are investigating a deploy, you can add the `debug: true` flag to your app:
|
143
136
|
|
144
137
|
```ruby
|
145
138
|
Hatchet::Runner.new("rails3_mri_193", debug: true).deploy do |app|
|
@@ -155,21 +148,15 @@ app = Hatchet::Runner.new("rails3_mri_193", name: "testapp")
|
|
155
148
|
|
156
149
|
Deploying the app takes a few minutes, so you may want to skip that part to make debugging a problem easier since you're iterating much faster.
|
157
150
|
|
158
|
-
|
159
|
-
If you need to deploy using a buildpack that is not in the root of your directory you can specify a path in the `buildpack` option:
|
160
|
-
|
151
|
+
If you need to deploy using a different buildpack you can specify one manually:
|
161
152
|
|
162
153
|
```ruby
|
163
|
-
buildpack_path = File.expand_path 'test/fixtures/buildpacks/heroku-buildpack-ruby'
|
164
154
|
|
165
155
|
def test_deploy
|
166
|
-
Hatchet::
|
156
|
+
Hatchet::Runner.new("rails3_mri_193", buildpack: "https://github.com/heroku/heroku-buildpack-ruby.git").deploy do |app|
|
167
157
|
# ...
|
168
158
|
```
|
169
159
|
|
170
|
-
If you are using a `Hatchet::GitApp` this is where you specify the publicly avaialble location of your buildpack, such as `https://github.com/heroku/heroku-buildpack-ruby.git#mybranch`
|
171
|
-
|
172
|
-
|
173
160
|
## Getting Deploy Output
|
174
161
|
|
175
162
|
After Hatchet deploys your app you can get the output by using `app.output`
|
@@ -201,6 +188,15 @@ Hatchet::Runner.new("rails3_mri_193").deploy do |app|
|
|
201
188
|
|
202
189
|
This is useful for checking the existence of generated files such as assets. If you need to run an interactive session such as `heroku run bash` or `heroku run rails console` you can use the run command and pass a block:
|
203
190
|
|
191
|
+
```
|
192
|
+
Hatchet::Runner.new("rails3_mri_193").deploy do |app|
|
193
|
+
app.run("cat Procfile")
|
194
|
+
end
|
195
|
+
```
|
196
|
+
|
197
|
+
This is the prefered way to run commands against the app. You can also string together commands in a session, however due to difficulties in driving a REPL programatically via [repl_runner](http://github.com/schneems/repl_runner) it's less deterministic.
|
198
|
+
|
199
|
+
|
204
200
|
```ruby
|
205
201
|
Hatchet::Runner.new("rails3_mri_193").deploy do |app|
|
206
202
|
app.run("bash") do |bash|
|
@@ -210,38 +206,56 @@ Hatchet::Runner.new("rails3_mri_193").deploy do |app|
|
|
210
206
|
end
|
211
207
|
```
|
212
208
|
|
213
|
-
|
209
|
+
Please read the docs on [repl_runner](http://github.com/schneems/repl_runner) for more info. The only interactive commands that are supported out of the box are `rails console`, `bash`, and `irb` it is fairly easy to add your own though:
|
210
|
+
|
211
|
+
## Heroku CI
|
212
|
+
|
213
|
+
Hatchet supports testing Heroku CI.
|
214
214
|
|
215
215
|
```ruby
|
216
|
-
Hatchet::Runner.new("
|
217
|
-
|
218
|
-
console.run("a = 1 + 2") {|result| assert_match "3", result }
|
219
|
-
console.run("'foo' * a") {|result| assert_match "foofoofoo", result }
|
220
|
-
end
|
216
|
+
Hatchet::Runner.new("rails5_ruby_schema_format").run_ci do |test_run|
|
217
|
+
assert_match "Ruby buildpack tests completed successfully", test_run.output
|
221
218
|
end
|
222
219
|
```
|
223
220
|
|
224
|
-
|
221
|
+
Call the `run_ci` method on the hatchet `Runner`. The object passed to the block is a `Hatchet::TestRun` object. You can call:
|
225
222
|
|
226
|
-
|
227
|
-
|
228
|
-
config.terminate_command "exit()" # the command you use to end the 'python' console
|
229
|
-
config.startup_timeout 60 # seconds to boot
|
230
|
-
config.return_char "\n" # the character that submits the command
|
231
|
-
end
|
232
|
-
```
|
223
|
+
- `test_run.output` this will have the setup and test output of your tests.
|
224
|
+
- `test_run.app` this has a reference to the "app" you're testing against, however currently no `heroku create` is run (as it's not needed to run tests, only a pipeline and a blob of code).
|
233
225
|
|
234
|
-
If you
|
226
|
+
An exception will be raised if either the test times out or a status of `:errored` or `:failed` is returned. If you expect your test to fail, you can pass in `allow_failure: true` when creating your hatchet runner. If you do that, you'll also get access to different statuses:
|
235
227
|
|
236
|
-
|
228
|
+
- `test_run.status` this will return a symbol of the status of your test. Statuses include, but are not limited to `:pending`, `:building`, `:errored`, `:creating`, `:succeeded`, and `:failed`
|
237
229
|
|
238
|
-
|
230
|
+
You can pass in a different timeout to the `run_ci` method `run_ci(timeout: 300)`.
|
239
231
|
|
240
|
-
|
232
|
+
You will likely need an `app.json` in the root directory of the app you're deploying. For example:
|
241
233
|
|
242
|
-
|
234
|
+
```json
|
235
|
+
{
|
236
|
+
"environments": {
|
237
|
+
"test": {
|
238
|
+
"addons":[
|
239
|
+
"heroku-postgresql"
|
240
|
+
]
|
241
|
+
}
|
242
|
+
}
|
243
|
+
}
|
244
|
+
```
|
245
|
+
|
246
|
+
This is on [a Rails5 test app](https://github.com/sharpstone/rails5_ruby_schema_format/blob/master/app.json) that needs the database to run.
|
243
247
|
|
244
|
-
|
248
|
+
Do **NOT** specify a `buildpacks` key in the `app.json` as Hatchet will do this for you automatically. If you need to set buildpacks you can pass them into the `buildpacks:` keword argument:
|
249
|
+
|
250
|
+
```
|
251
|
+
buildpacks = []
|
252
|
+
buildpacks << "https://github.com/heroku/heroku-buildpack-pgbouncer.git"
|
253
|
+
buildpacks << [HATCHET_BUILDPACK_BASE, HATCHET_BUILDPACK_BRANCH.call].join("#")
|
254
|
+
|
255
|
+
Hatchet::Runner.new("rails5_ruby_schema_format", buildpacks: buildpacks).run_ci do |test_run|
|
256
|
+
# ...
|
257
|
+
end
|
258
|
+
```
|
245
259
|
|
246
260
|
## Testing on Travis
|
247
261
|
|
data/hatchet.gemspec
CHANGED
@@ -18,6 +18,7 @@ Gem::Specification.new do |gem|
|
|
18
18
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
19
|
gem.require_paths = ["lib"]
|
20
20
|
|
21
|
+
gem.add_dependency "platform-api", "~> 1"
|
21
22
|
gem.add_dependency "heroku-api", "~> 0"
|
22
23
|
gem.add_dependency "activesupport", "~> 4"
|
23
24
|
gem.add_dependency "rrrretry", "~> 1"
|
data/hatchet.json
CHANGED
@@ -3,5 +3,9 @@
|
|
3
3
|
"rails3": ["sharpstone/rails3_mri_193"],
|
4
4
|
"rails2": ["sharpstone/rails2blog"],
|
5
5
|
"bundler": ["sharpstone/no_lockfile"],
|
6
|
-
"default": ["sharpstone/default_ruby"]
|
6
|
+
"default": ["sharpstone/default_ruby"],
|
7
|
+
"ci": [
|
8
|
+
"sharpstone/rails5_ruby_schema_format",
|
9
|
+
"sharpstone/rails5_ci_fails_no_database"
|
10
|
+
]
|
7
11
|
}
|
data/lib/hatchet.rb
CHANGED
@@ -9,39 +9,27 @@ require 'stringio'
|
|
9
9
|
require 'fileutils'
|
10
10
|
require 'stringio'
|
11
11
|
|
12
|
-
|
13
12
|
module Hatchet
|
14
|
-
|
13
|
+
end
|
15
14
|
|
16
|
-
class App
|
17
|
-
end
|
18
15
|
|
19
|
-
def self.git_branch
|
20
|
-
return ENV['TRAVIS_BRANCH'] if ENV['TRAVIS_BRANCH']
|
21
|
-
out = `git describe --contains --all HEAD`.strip
|
22
|
-
raise "Attempting to find current branch name. Error: Cannot describe git: #{out}" unless $?.success?
|
23
|
-
out
|
24
|
-
end
|
25
|
-
|
26
|
-
def self.set_deploy_strategy!
|
27
|
-
deploy_strat = (ENV['HATCHET_DEPLOY_STRATEGY'] || :git).to_sym
|
28
|
-
case Hatchet::const_set("DEPLOY_STRATEGY", deploy_strat)
|
29
|
-
when :anvil
|
30
|
-
Hatchet.const_set("Runner", Hatchet::AnvilApp)
|
31
|
-
when :git
|
32
|
-
Hatchet.const_set("Runner", Hatchet::GitApp)
|
33
|
-
else
|
34
|
-
raise "unknown deploy strategy #{Hatchet::DEPLOY_STRATEGY}, expected 'anvil', 'git'"
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
16
|
|
39
17
|
require 'hatchet/version'
|
40
18
|
require 'hatchet/reaper'
|
19
|
+
require 'hatchet/test_run'
|
41
20
|
require 'hatchet/app'
|
42
21
|
require 'hatchet/anvil_app'
|
43
22
|
require 'hatchet/git_app'
|
44
23
|
require 'hatchet/config'
|
45
24
|
|
25
|
+
module Hatchet
|
26
|
+
RETRIES = Integer(ENV['HATCHET_RETRIES'] || 1)
|
27
|
+
Runner = Hatchet::GitApp
|
46
28
|
|
47
|
-
|
29
|
+
def self.git_branch
|
30
|
+
return ENV['TRAVIS_BRANCH'] if ENV['TRAVIS_BRANCH']
|
31
|
+
out = `git describe --contains --all HEAD`.strip
|
32
|
+
raise "Attempting to find current branch name. Error: Cannot describe git: #{out}" unless $?.success?
|
33
|
+
out
|
34
|
+
end
|
35
|
+
end
|
data/lib/hatchet/anvil_app.rb
CHANGED
@@ -3,65 +3,8 @@ require 'stringio'
|
|
3
3
|
|
4
4
|
module Hatchet
|
5
5
|
class AnvilApp < App
|
6
|
-
|
7
6
|
def initialize(directory, options = {})
|
8
|
-
|
9
|
-
@buildpack ||= File.expand_path('.')
|
10
|
-
super
|
11
|
-
end
|
12
|
-
|
13
|
-
def push_without_retry!
|
14
|
-
out, err = wrap_stdout_and_rescue(Anvil::Builder::BuildError) do
|
15
|
-
slug_url = Anvil::Engine.build(".", :buildpack => @buildpack, :pipeline => true)
|
16
|
-
puts "Releasing to http://#{@name}.herokuapp.com"
|
17
|
-
response = release(@name, slug_url)
|
18
|
-
while response.status == 202
|
19
|
-
response = Excon.get("#{release_host}#{response.headers["Location"]}")
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
err.string
|
24
|
-
end
|
25
|
-
|
26
|
-
def wrap_stdout_and_rescue(error, &block)
|
27
|
-
wrap_stdout do |orig_out, orig_err|
|
28
|
-
begin
|
29
|
-
yield orig_out, orig_err
|
30
|
-
rescue error => e
|
31
|
-
return [$stdout.dup, $stderr.dup] if @allow_failure
|
32
|
-
orig_out.puts $stderr.dup.string # print the errors to the test output
|
33
|
-
raise e
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def wrap_stdout(orig_out = $stdout, orig_err = $stderr, &block)
|
39
|
-
$stderr = StringIO.new
|
40
|
-
$stdout = StringIO.new
|
41
|
-
yield orig_out, orig_err
|
42
|
-
puts [$stdout.dup, $stderr.dup].inspect
|
43
|
-
return $stdout.dup, $stderr.dup
|
44
|
-
ensure
|
45
|
-
$stdout = orig_out
|
46
|
-
$stderr = orig_err
|
47
|
-
end
|
48
|
-
|
49
|
-
def teardown!
|
50
|
-
super
|
51
|
-
FileUtils.rm_rf("#{directory}/.anvil")
|
52
|
-
end
|
53
|
-
|
54
|
-
private
|
55
|
-
def release(name, slug_url)
|
56
|
-
headers = {"Content-Type" => "application/json", accept: :json}
|
57
|
-
release_options = {description: "Anvil Build", slug_url: slug_url }
|
58
|
-
Excon.post("#{release_host}/v1/apps/#{name}/release",
|
59
|
-
headers: headers,
|
60
|
-
body: release_options.to_json)
|
61
|
-
end
|
62
|
-
|
63
|
-
def release_host
|
64
|
-
"https://:#{api_key}@cisaurus.heroku.com"
|
7
|
+
raise "Anvil no longer works"
|
65
8
|
end
|
66
9
|
end
|
67
10
|
end
|
data/lib/hatchet/app.rb
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
require 'shellwords'
|
2
|
+
require 'platform-api'
|
2
3
|
|
3
4
|
module Hatchet
|
4
5
|
class App
|
6
|
+
HATCHET_BUILDPACK_BASE = (ENV['HATCHET_BUILDPACK_BASE'] || "https://github.com/heroku/heroku-buildpack-ruby.git")
|
7
|
+
HATCHET_BUILDPACK_BRANCH = -> { ENV['HATCHET_BUILDPACK_BRANCH'] || Hatchet.git_branch }
|
8
|
+
BUILDPACK_URL = "https://github.com/heroku/heroku-buildpack-ruby.git"
|
9
|
+
|
5
10
|
attr_reader :name, :stack, :directory, :repo_name
|
6
11
|
|
7
12
|
class FailedDeploy < StandardError
|
@@ -22,9 +27,14 @@ module Hatchet
|
|
22
27
|
@debug = options[:debug] || options[:debugging]
|
23
28
|
@allow_failure = options[:allow_failure] || false
|
24
29
|
@labs = ([] << options[:labs]).flatten.compact
|
30
|
+
@buildpack = options[:buildpack] || options[:buildpack_url] || [HATCHET_BUILDPACK_BASE, HATCHET_BUILDPACK_BRANCH.call].join("#")
|
25
31
|
@reaper = Reaper.new(heroku)
|
26
32
|
end
|
27
33
|
|
34
|
+
def allow_failure?
|
35
|
+
@allow_failure
|
36
|
+
end
|
37
|
+
|
28
38
|
# config is read only, should be threadsafe
|
29
39
|
def self.config
|
30
40
|
@config ||= Config.new
|
@@ -194,6 +204,58 @@ module Hatchet
|
|
194
204
|
@heroku ||= Heroku::API.new(api_key: api_key)
|
195
205
|
end
|
196
206
|
|
207
|
+
def run_ci(timeout: 300, &block)
|
208
|
+
Hatchet::RETRIES.times.retry do
|
209
|
+
result = create_pipeline
|
210
|
+
@pipeline_id = result["id"]
|
211
|
+
end
|
212
|
+
|
213
|
+
# create_app
|
214
|
+
# platform_api.pipeline_coupling.create(app: name, pipeline: @pipeline_id, stage: "development")
|
215
|
+
test_run = TestRun.new(token: api_key,
|
216
|
+
buildpacks: @buildpack,
|
217
|
+
timeout: timeout,
|
218
|
+
app: self,
|
219
|
+
pipeline: @pipeline_id)
|
220
|
+
|
221
|
+
Hatchet::RETRIES.times.retry do
|
222
|
+
test_run.create_test_run
|
223
|
+
end
|
224
|
+
test_run.wait!(&block)
|
225
|
+
ensure
|
226
|
+
delete_pipeline(@pipeline_id) if @pipeline_id
|
227
|
+
end
|
228
|
+
|
229
|
+
def pipeline_id
|
230
|
+
@pipeline_id
|
231
|
+
end
|
232
|
+
|
233
|
+
def create_pipeline
|
234
|
+
platform_api.pipeline.create(name: @name)
|
235
|
+
end
|
236
|
+
|
237
|
+
def source_get_url
|
238
|
+
create_source
|
239
|
+
@source_get_url
|
240
|
+
end
|
241
|
+
|
242
|
+
def create_source
|
243
|
+
@create_source ||= begin
|
244
|
+
result = platform_api.source.create
|
245
|
+
@source_get_url = result["source_blob"]["get_url"]
|
246
|
+
@source_put_url = result["source_blob"]["put_url"]
|
247
|
+
@source_put_url
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def delete_pipeline(pipeline_id)
|
252
|
+
platform_api.pipeline.delete(pipeline_id)
|
253
|
+
end
|
254
|
+
|
255
|
+
def platform_api
|
256
|
+
@platform_api ||= PlatformAPI.connect_oauth(api_key)
|
257
|
+
end
|
258
|
+
|
197
259
|
private
|
198
260
|
# if someone uses bundle exec
|
199
261
|
def bundle_exec
|
data/lib/hatchet/git_app.rb
CHANGED
@@ -1,15 +1,6 @@
|
|
1
1
|
module Hatchet
|
2
2
|
# used for deploying a test app to heroku via git
|
3
3
|
class GitApp < App
|
4
|
-
HATCHET_BUILDPACK_BASE = (ENV['HATCHET_BUILDPACK_BASE'] || "https://github.com/heroku/heroku-buildpack-ruby.git")
|
5
|
-
HATCHET_BUILDPACK_BRANCH = -> { ENV['HATCHET_BUILDPACK_BRANCH'] || Hatchet.git_branch }
|
6
|
-
BUILDPACK_URL = "https://github.com/heroku/heroku-buildpack-ruby.git"
|
7
|
-
|
8
|
-
def initialize(directory, options = {})
|
9
|
-
@buildpack = options[:buildpack] || options[:buildpack_url] || [HATCHET_BUILDPACK_BASE, HATCHET_BUILDPACK_BRANCH.call].join("#")
|
10
|
-
super
|
11
|
-
end
|
12
|
-
|
13
4
|
def setup!
|
14
5
|
super
|
15
6
|
heroku.put_config_vars(name, 'BUILDPACK_URL' => @buildpack)
|
data/lib/hatchet/reaper.rb
CHANGED
@@ -0,0 +1,192 @@
|
|
1
|
+
module Hatchet
|
2
|
+
class FailedTestError < StandardError
|
3
|
+
def initialize(app, output)
|
4
|
+
msg = "Could run tests on pipeline id: '#{app.pipeline_id}' (#{app.repo_name}) at path: '#{app.directory}'\n" <<
|
5
|
+
" if this was expected add `allow_failure: true` to your hatchet initialization hash.\n" <<
|
6
|
+
"output:\n" <<
|
7
|
+
"#{output}"
|
8
|
+
super(msg)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class TestRun
|
13
|
+
|
14
|
+
# Hatchet::GitApp.new("rails3_mri_193").run_ci do |test_run|
|
15
|
+
# assert :succeeded, test_run.status
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# TestRun.new(token: , buildpacks: , test_dir: )
|
19
|
+
#
|
20
|
+
def initialize(
|
21
|
+
token:,
|
22
|
+
buildpacks:,
|
23
|
+
app:,
|
24
|
+
pipeline:,
|
25
|
+
timeout: 10,
|
26
|
+
pause: 5,
|
27
|
+
commit_sha: "sha",
|
28
|
+
commit_branch: "master",
|
29
|
+
commit_message: "commit",
|
30
|
+
organization: nil
|
31
|
+
)
|
32
|
+
@pipeline = pipeline || "hatchet-t-#{SecureRandom.hex(10)}"
|
33
|
+
@timeout = timeout
|
34
|
+
@pause = pause
|
35
|
+
@organization = organization
|
36
|
+
@token = token
|
37
|
+
@commit_sha = commit_sha
|
38
|
+
@commit_branch = commit_branch
|
39
|
+
@commit_message = commit_message
|
40
|
+
@buildpacks = Array(buildpacks)
|
41
|
+
@app = app
|
42
|
+
@mutex = Mutex.new
|
43
|
+
@status = false
|
44
|
+
end
|
45
|
+
attr_reader :app
|
46
|
+
|
47
|
+
def create_test_run
|
48
|
+
@mutex.synchronize do
|
49
|
+
raise "Test is already running" if @status
|
50
|
+
@status = :building
|
51
|
+
|
52
|
+
body = {
|
53
|
+
source_blob_url: source_blob_url,
|
54
|
+
pipeline: @pipeline,
|
55
|
+
organization: @organization,
|
56
|
+
commit_sha: @commit_sha,
|
57
|
+
commit_branch: @commit_branch,
|
58
|
+
commit_message: @commit_message,
|
59
|
+
}
|
60
|
+
|
61
|
+
# https://github.com/heroku/api/blob/master/schema/variants/3.ci/platform-api-reference.md#test-run-create
|
62
|
+
attributes = excon_request(
|
63
|
+
method: :post,
|
64
|
+
path: "/test-runs",
|
65
|
+
version: "3.ci",
|
66
|
+
body: body,
|
67
|
+
expects: [201]
|
68
|
+
)
|
69
|
+
@test_run_id = attributes["id"]
|
70
|
+
end
|
71
|
+
info
|
72
|
+
end
|
73
|
+
|
74
|
+
def info
|
75
|
+
# GET /test-runs/{test_run_id}
|
76
|
+
response = excon_request(
|
77
|
+
method: :get,
|
78
|
+
path: "/test-runs/#{@test_run_id}",
|
79
|
+
version: "3.ci",
|
80
|
+
expects: [201, 200]
|
81
|
+
)
|
82
|
+
|
83
|
+
@setup_stream_url = response["setup_stream_url"]
|
84
|
+
@output_stream_url = response["output_stream_url"]
|
85
|
+
|
86
|
+
@status = response["status"].to_sym
|
87
|
+
end
|
88
|
+
|
89
|
+
def status
|
90
|
+
@status # :pending, :building, :creating, :succeeded, :failed, :errored
|
91
|
+
end
|
92
|
+
|
93
|
+
def output
|
94
|
+
get_contents_or_whatever(@setup_stream_url) +
|
95
|
+
get_contents_or_whatever(@output_stream_url)
|
96
|
+
end
|
97
|
+
|
98
|
+
def wait!
|
99
|
+
Timeout::timeout(@timeout) do
|
100
|
+
while true do
|
101
|
+
info
|
102
|
+
case @status
|
103
|
+
when :succeeded
|
104
|
+
yield self
|
105
|
+
return self
|
106
|
+
when :failed, :errored
|
107
|
+
raise FailedTestError.new(self.app, self.output) unless app.allow_failure?
|
108
|
+
yield self
|
109
|
+
return self
|
110
|
+
else
|
111
|
+
# keep looping
|
112
|
+
end
|
113
|
+
sleep @pause
|
114
|
+
end
|
115
|
+
end
|
116
|
+
rescue Timeout::Error
|
117
|
+
puts "Timed out status: #{@status}, timeout: #{@timeout}"
|
118
|
+
raise FailedTestError.new(self.app, self.output) unless app.allow_failure?
|
119
|
+
yield self
|
120
|
+
return self
|
121
|
+
end
|
122
|
+
|
123
|
+
# Here's where the magic happens folks
|
124
|
+
#
|
125
|
+
# == Set the buildpack
|
126
|
+
#
|
127
|
+
# We take the current directory structure and see if it has an `app.json`
|
128
|
+
# This is how Heroku CI knows what buildpacks to use to run your tests
|
129
|
+
# Hatchet will inject whatever buildpack you pass to it, by default
|
130
|
+
# it uses the same buildpack you have specified in your Hatchet constants
|
131
|
+
# and uses the same branch your tests are using
|
132
|
+
#
|
133
|
+
# == Generate source blob url
|
134
|
+
#
|
135
|
+
# The CI endpoint takes a url that has a `.tgz` file to execute your tests.
|
136
|
+
# We pull down the app you're testing against, inject an `app.json` (or modify
|
137
|
+
# if it already exists). We the use the heroku "source" api to generate a
|
138
|
+
# url that we can put our newly generated `.tgz` file. It also returns a "get"
|
139
|
+
# url where those contents can be downloaded. We pass this url back to CI
|
140
|
+
#
|
141
|
+
def source_blob_url
|
142
|
+
@app.in_directory do
|
143
|
+
app_json = JSON.parse(File.read("app.json")) if File.exist?("app.json")
|
144
|
+
app_json ||= {}
|
145
|
+
app_json["environments"] ||= {}
|
146
|
+
app_json["environments"]["test"] ||= {}
|
147
|
+
app_json["environments"]["test"]["buildpacks"] = @buildpacks.map {|b| { url: b } }
|
148
|
+
File.open("app.json", "w") {|f| f.write(JSON.generate(app_json)) }
|
149
|
+
|
150
|
+
`tar c . | gzip -9 > slug.tgz`
|
151
|
+
|
152
|
+
source_put_url = @app.create_source
|
153
|
+
Hatchet::RETRIES.times.retry do
|
154
|
+
Excon.put(source_put_url,
|
155
|
+
expects: [200],
|
156
|
+
body: File.read('slug.tgz'))
|
157
|
+
end
|
158
|
+
end
|
159
|
+
return @app.source_get_url
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
def get_contents_or_whatever(url)
|
164
|
+
Excon.get(url, read_timeout: @pause).body
|
165
|
+
rescue Excon::Error::Timeout
|
166
|
+
""
|
167
|
+
end
|
168
|
+
|
169
|
+
def excon_request(options)
|
170
|
+
JSON.parse(raw_excon_request(options).body)
|
171
|
+
end
|
172
|
+
|
173
|
+
def version
|
174
|
+
"3"
|
175
|
+
end
|
176
|
+
|
177
|
+
def raw_excon_request(options)
|
178
|
+
version = options[:version] || 3
|
179
|
+
options[:headers] = {
|
180
|
+
"Authorization" => "Bearer #{@token}",
|
181
|
+
"Accept" => "application/vnd.heroku+json; version=#{version}",
|
182
|
+
"Content-Type" => "application/json"
|
183
|
+
}.merge(options[:headers] || {})
|
184
|
+
options[:body] = JSON.generate(options[:body]) if options[:body]
|
185
|
+
|
186
|
+
Hatchet::RETRIES.times.retry do
|
187
|
+
connection = Excon.new("https://api.heroku.com")
|
188
|
+
return connection.request(options)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
data/lib/hatchet/version.rb
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
|
4
|
+
class CITest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def test_ci_create_app_with_stack
|
7
|
+
Hatchet::GitApp.new("rails5_ruby_schema_format").run_ci do |test_run|
|
8
|
+
assert_match "Ruby buildpack tests completed successfully", test_run.output
|
9
|
+
assert_equal :succeeded, test_run.status
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_error_with_bad_app
|
14
|
+
error = assert_raise(Hatchet::FailedTestError) do
|
15
|
+
Hatchet::GitApp.new("rails5_ci_fails_no_database").run_ci {}
|
16
|
+
end
|
17
|
+
|
18
|
+
assert_match "PG::ConnectionBad: could not connect to server" ,error.message
|
19
|
+
|
20
|
+
Hatchet::GitApp.new("rails5_ci_fails_no_database", allow_failure: true).run_ci do |test_run|
|
21
|
+
assert_equal :errored, test_run.status
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/test/hatchet/config_test.rb
CHANGED
@@ -11,20 +11,26 @@ class ConfigTest < Test::Unit::TestCase
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def test_config_dirs
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
14
|
+
{
|
15
|
+
"test/fixtures/repos/bundler/no_lockfile" => "git://github.com/sharpstone/no_lockfile.git",
|
16
|
+
"test/fixtures/repos/default/default_ruby" => "git://github.com/sharpstone/default_ruby.git",
|
17
|
+
"test/fixtures/repos/default/default_ruby" => "git://github.com/sharpstone/default_ruby.git",
|
18
|
+
"test/fixtures/repos/rails2/rails2blog" => "git://github.com/sharpstone/rails2blog.git",
|
19
|
+
"test/fixtures/repos/rails3/rails3_mri_193" => "git://github.com/sharpstone/rails3_mri_193.git"
|
20
|
+
}.each do |key, value|
|
21
|
+
assert_include(key, value, @config.dirs)
|
22
|
+
end
|
23
|
+
end
|
21
24
|
|
22
25
|
def test_config_repos
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
26
|
+
{
|
27
|
+
"default_ruby" => "test/fixtures/repos/default/default_ruby",
|
28
|
+
"no_lockfile" => "test/fixtures/repos/bundler/no_lockfile",
|
29
|
+
"rails2blog" => "test/fixtures/repos/rails2/rails2blog",
|
30
|
+
"rails3_mri_193" => "test/fixtures/repos/rails3/rails3_mri_193"
|
31
|
+
}.each do |key, value|
|
32
|
+
assert_include(key, value, @config.repos)
|
33
|
+
end
|
28
34
|
end
|
29
35
|
|
30
36
|
def test_no_internal_config_raises_no_errors
|
@@ -37,5 +43,10 @@ class ConfigTest < Test::Unit::TestCase
|
|
37
43
|
@config.send :init_config!, {"foo" => ["schneems/sextant"]}
|
38
44
|
assert_equal("git://github.com/schneems/sextant.git", @config.dirs["./repos/foo/sextant"])
|
39
45
|
end
|
46
|
+
private
|
47
|
+
|
48
|
+
def assert_include(key, value, actual)
|
49
|
+
assert_equal value, actual[key], "Expected #{actual.inspect} to include #{ {key => value } } but it did not"
|
50
|
+
end
|
40
51
|
end
|
41
52
|
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: heroku_hatchet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.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:
|
11
|
+
date: 2017-03-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: platform-api
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: heroku-api
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -231,6 +245,7 @@ files:
|
|
231
245
|
- lib/hatchet/git_app.rb
|
232
246
|
- lib/hatchet/reaper.rb
|
233
247
|
- lib/hatchet/tasks.rb
|
248
|
+
- lib/hatchet/test_run.rb
|
234
249
|
- lib/hatchet/version.rb
|
235
250
|
- test/fixtures/buildpacks/null-buildpack/bin/compile
|
236
251
|
- test/fixtures/buildpacks/null-buildpack/bin/detect
|
@@ -239,8 +254,8 @@ files:
|
|
239
254
|
- test/fixtures/buildpacks/null-buildpack/readme.md
|
240
255
|
- test/hatchet/allow_failure_anvil_test.rb
|
241
256
|
- test/hatchet/allow_failure_git_test.rb
|
242
|
-
- test/hatchet/anvil_test.rb
|
243
257
|
- test/hatchet/app_test.rb
|
258
|
+
- test/hatchet/ci_test.rb
|
244
259
|
- test/hatchet/config_test.rb
|
245
260
|
- test/hatchet/edit_repo_test.rb
|
246
261
|
- test/hatchet/git_test.rb
|
@@ -248,7 +263,6 @@ files:
|
|
248
263
|
- test/hatchet/labs_test.rb
|
249
264
|
- test/hatchet/multi_cmd_runner_test.rb
|
250
265
|
- test/hatchet/reaper_test.rb
|
251
|
-
- test/hatchet/runner_test.rb
|
252
266
|
- test/test_helper.rb
|
253
267
|
homepage: https://github.com/heroku/hatchet
|
254
268
|
licenses:
|
@@ -270,7 +284,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
270
284
|
version: '0'
|
271
285
|
requirements: []
|
272
286
|
rubyforge_project:
|
273
|
-
rubygems_version: 2.
|
287
|
+
rubygems_version: 2.6.11
|
274
288
|
signing_key:
|
275
289
|
specification_version: 4
|
276
290
|
summary: Hatchet is a an integration testing library for developing Heroku buildpacks.
|
@@ -282,8 +296,8 @@ test_files:
|
|
282
296
|
- test/fixtures/buildpacks/null-buildpack/readme.md
|
283
297
|
- test/hatchet/allow_failure_anvil_test.rb
|
284
298
|
- test/hatchet/allow_failure_git_test.rb
|
285
|
-
- test/hatchet/anvil_test.rb
|
286
299
|
- test/hatchet/app_test.rb
|
300
|
+
- test/hatchet/ci_test.rb
|
287
301
|
- test/hatchet/config_test.rb
|
288
302
|
- test/hatchet/edit_repo_test.rb
|
289
303
|
- test/hatchet/git_test.rb
|
@@ -291,5 +305,4 @@ test_files:
|
|
291
305
|
- test/hatchet/labs_test.rb
|
292
306
|
- test/hatchet/multi_cmd_runner_test.rb
|
293
307
|
- test/hatchet/reaper_test.rb
|
294
|
-
- test/hatchet/runner_test.rb
|
295
308
|
- test/test_helper.rb
|
data/test/hatchet/anvil_test.rb
DELETED
data/test/hatchet/runner_test.rb
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
require 'test_helper'
|
2
|
-
|
3
|
-
class HatchetRunnerTest < Test::Unit::TestCase
|
4
|
-
|
5
|
-
def setup
|
6
|
-
@default = ENV['HATCHET_DEPLOY_STRATEGY']
|
7
|
-
end
|
8
|
-
|
9
|
-
def teardown
|
10
|
-
ENV['HATCHET_DEPLOY_STRATEGY'] = @default
|
11
|
-
Hatchet.set_deploy_strategy!
|
12
|
-
end
|
13
|
-
|
14
|
-
def test_defaults
|
15
|
-
assert_equal nil, ENV['HATCHET_DEPLOY_STRATEGY']
|
16
|
-
assert_equal :git, Hatchet::DEPLOY_STRATEGY
|
17
|
-
assert_equal Hatchet::GitApp, Hatchet::Runner
|
18
|
-
end
|
19
|
-
|
20
|
-
def test_change_deploy_strat
|
21
|
-
ENV['HATCHET_DEPLOY_STRATEGY'] = "git"
|
22
|
-
Hatchet.set_deploy_strategy!
|
23
|
-
assert_equal :git, Hatchet::DEPLOY_STRATEGY
|
24
|
-
assert_equal Hatchet::GitApp, Hatchet::Runner
|
25
|
-
|
26
|
-
ENV['HATCHET_DEPLOY_STRATEGY'] = "anvil"
|
27
|
-
Hatchet.set_deploy_strategy!
|
28
|
-
assert_equal :anvil, Hatchet::DEPLOY_STRATEGY
|
29
|
-
assert_equal Hatchet::AnvilApp, Hatchet::Runner
|
30
|
-
end
|
31
|
-
end
|