heroku_hatchet 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +178 -0
- data/Rakefile +5 -0
- data/bin/hatchet +74 -0
- data/hatchet.gemspec +28 -0
- data/hatchet.json +5 -0
- data/lib/hatchet/anvil_app.rb +57 -0
- data/lib/hatchet/app.rb +85 -0
- data/lib/hatchet/config.rb +90 -0
- data/lib/hatchet/git_app.rb +25 -0
- data/lib/hatchet/process_spawn.rb +49 -0
- data/lib/hatchet/stream_exec.rb +58 -0
- data/lib/hatchet/tasks.rb +10 -0
- data/lib/hatchet/version.rb +3 -0
- data/lib/hatchet.rb +20 -0
- data/test/fixtures/builpacks/null-buildpack/bin/compile +4 -0
- data/test/fixtures/builpacks/null-buildpack/bin/detect +5 -0
- data/test/fixtures/builpacks/null-buildpack/bin/release +3 -0
- data/test/fixtures/builpacks/null-buildpack/hatchet.json +5 -0
- data/test/fixtures/builpacks/null-buildpack/readme.md +41 -0
- data/test/hatchet/anvil_test.rb +21 -0
- data/test/hatchet/config_test.rb +35 -0
- data/test/hatchet/git_test.rb +15 -0
- data/test/test_helper.rb +11 -0
- metadata +179 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Terence Lee
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
# Hatchet
|
2
|
+
|
3
|
+
![](http://f.cl.ly/items/2M2O2Q2I2x0e1M1P2936/Screen%20Shot%202013-01-06%20at%209.59.38%20PM.png)
|
4
|
+
|
5
|
+
## What
|
6
|
+
|
7
|
+
The Hatchet is a an integration testing library for developing Heroku buildpacks.
|
8
|
+
|
9
|
+
## Install
|
10
|
+
|
11
|
+
First run:
|
12
|
+
|
13
|
+
$ bundle install
|
14
|
+
|
15
|
+
This library uses the heroku-api gem, you will need to make your API key
|
16
|
+
available to the system.
|
17
|
+
|
18
|
+
You can get your token by running:
|
19
|
+
|
20
|
+
$ heroku auth:token
|
21
|
+
alskdfju108f09uvngu172019
|
22
|
+
|
23
|
+
|
24
|
+
We need to export this token into our environment open up your `.bashrc`
|
25
|
+
|
26
|
+
export HEROKU_API_KEY="alskdfju108f09uvngu172019"
|
27
|
+
|
28
|
+
Then source the file. If you don't want to set your api key system wide,
|
29
|
+
it will be pulled automatically via shelling out, but this is slower.
|
30
|
+
|
31
|
+
## Run the Tests
|
32
|
+
|
33
|
+
$ bundle exec rake test
|
34
|
+
|
35
|
+
|
36
|
+
## Writing Tests
|
37
|
+
|
38
|
+
Hatchet is meant for running integration tests, which means we actually have to deploy a real live honest to goodness app on Heroku and see how it behaves.
|
39
|
+
|
40
|
+
First you'll need a repo to an app you know works on Heroku, add it to the proper folder in repos. Such as `repos/rails3/`. Once you've done that make a corresponding test file in the `test` dir such as `test/repos/rails3`. I've already got a project called "codetriage" and the test is "triage_test.rb".
|
41
|
+
|
42
|
+
Now that you have an app, we'll need to create a heroku instance, and deploy our code to that instance you can do that with this code:
|
43
|
+
|
44
|
+
Hatchet::App.new("repos/rails3/codetriage").deploy do |app|
|
45
|
+
##
|
46
|
+
end
|
47
|
+
|
48
|
+
The first argument to the app is the directory where you can find the code. Once your test is done, the app will automatically be destroyed.
|
49
|
+
|
50
|
+
Now that you've deployed your app you'll want to make sure that the deploy worked correctly. Since we're using `test/unit` you can use regular assertions such as `assert` and `refute`. Since we're yielding to an `app` variable we can check the `deployed?` status of the app:
|
51
|
+
|
52
|
+
Hatchet::App.new("repos/rails3/codetriage").deploy do |app|
|
53
|
+
assert app.deployed?
|
54
|
+
end
|
55
|
+
|
56
|
+
The primary purpose of the buildpack is configuring and deploying apps, so if it deployed chances are the buildpack is working correctly, but sometimes you may want more information. You can run arbitrary commands such as `heroku bash` and then check for the existence of a file.
|
57
|
+
|
58
|
+
Hatchet::App.new("repos/rails3/codetriage").deploy do |app|
|
59
|
+
app.run("bash") do |cmd|
|
60
|
+
assert cmd.run("ls public/assets").include?("application.css")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Anything you put in `cmd.run` at this point will be executed from with in the app that you are in.
|
65
|
+
|
66
|
+
cmd.run("cat")
|
67
|
+
cmd.run("cd")
|
68
|
+
cmd.run("cd .. ; ls | grep foo")
|
69
|
+
|
70
|
+
It behaves exactly as if you were in a remote shell. If you really wanted you could even run the tests:
|
71
|
+
|
72
|
+
cmd.run("rake test")
|
73
|
+
|
74
|
+
But since cmd.run doesn't return the exit status now, that wouldn't be
|
75
|
+
so useful (also there is a default timeout to all commands). If you want
|
76
|
+
you can configure the timeout by passing in a second parameter
|
77
|
+
|
78
|
+
cmd.run("rake test", 180.seconds)
|
79
|
+
|
80
|
+
|
81
|
+
## Testing A Different Buildpack
|
82
|
+
|
83
|
+
You can specify buildpack to deploy with like so:
|
84
|
+
|
85
|
+
Hatchet::App.new("repos/rails3/codetriage", buildpack: "https://github.com/schneems/heroku-buildpack-ruby.git").deploy do |app|
|
86
|
+
|
87
|
+
## Hatchet Config
|
88
|
+
|
89
|
+
Hatchet is designed to test buildpacks, and requires full repositories
|
90
|
+
to deploy to Heroku. Web application repos, especially Rails repos, aren't known for
|
91
|
+
being small, if you're testing a custom buildpack and have
|
92
|
+
`BUILDPACK_URL` set in your app config, it needs to be cloned each time
|
93
|
+
you deploy your app. If you've `git add`-ed a bunch of repos then this
|
94
|
+
clone would be pretty slow, we're not going to do this. Do not commit
|
95
|
+
your repos to git.
|
96
|
+
|
97
|
+
Instead we will keep a structured file called
|
98
|
+
inventively `hatchet.json` at the root of your project. This file will
|
99
|
+
describe the structure of your repos, have the name of the repo, and a
|
100
|
+
git url. We will use it to sync remote git repos with your local
|
101
|
+
project. It might look something like this
|
102
|
+
|
103
|
+
{
|
104
|
+
"hatchet": {},
|
105
|
+
"rails3": ["git@github.com:codetriage/codetriage.git"],
|
106
|
+
"rails2": ["git@github.com:heroku/rails2blog.git"]
|
107
|
+
}
|
108
|
+
|
109
|
+
the 'hatchet' object accessor is reserved for hatchet settings.
|
110
|
+
. To copy each repo in your `hatchet.json`
|
111
|
+
run the command:
|
112
|
+
|
113
|
+
$ hatchet install
|
114
|
+
|
115
|
+
The above `hatchet.json` will produce a directory structure like this:
|
116
|
+
|
117
|
+
repos/
|
118
|
+
rails3/
|
119
|
+
codetriage/
|
120
|
+
#...
|
121
|
+
rails2/
|
122
|
+
rails2blog/
|
123
|
+
# ...
|
124
|
+
|
125
|
+
While you are running your tests if you reference a repo that isn't
|
126
|
+
synced locally Hatchet will raise an error. Since you're using a
|
127
|
+
standard file for your repos, you can now reference the name of the git
|
128
|
+
repo, provided you don't have conflicting names:
|
129
|
+
|
130
|
+
Hatchet::App.new("codetriage").deploy do |app|
|
131
|
+
|
132
|
+
If you do have conflicting names, use full paths.
|
133
|
+
|
134
|
+
A word of warning on including rails/ruby repos inside of your test
|
135
|
+
directory, if you're using a runner that looks for patterns such as
|
136
|
+
`*_test.rb` to run your hatchet tests, it may incorrectly think you want
|
137
|
+
to run the tests inside of the rails repositories. To get rid of this
|
138
|
+
problem move your repos direcory out of `test/` or be more specific
|
139
|
+
with your tests such as moving them to a `test/hatchet` directory and
|
140
|
+
changing your pattern if you are using `Rake::TestTask` it might look like this:
|
141
|
+
|
142
|
+
t.pattern = 'test/hatchet/**/*_test.rb'
|
143
|
+
|
144
|
+
A note on external repos: since you're basing tests on these repos, it
|
145
|
+
is in your best interest to not change them or your tests may
|
146
|
+
spontaneously fail. In the future we may create a hatchet.lockfile or
|
147
|
+
something to declare the commit
|
148
|
+
|
149
|
+
## Hatchet CLI
|
150
|
+
|
151
|
+
Hatchet has a CLI for installing and maintaining external repos you're
|
152
|
+
using to test against. If you have Hatchet installed as a gem run
|
153
|
+
|
154
|
+
$ hatchet --help
|
155
|
+
|
156
|
+
For more info on commands. If you're using the source code you can run
|
157
|
+
the command by going to the source code directory and running:
|
158
|
+
|
159
|
+
$ ./bin/hatchet --help
|
160
|
+
|
161
|
+
|
162
|
+
|
163
|
+
## The Future
|
164
|
+
|
165
|
+
### Speed
|
166
|
+
|
167
|
+
Efforts may be spent optimizing / parallelizing the process, almost all of the time of the test is spent waiting for IO, so hopefully we should be able to parallelize many tests / deploys at the same time. The hardest part of this (i believe) would be splitting out the different runs into different log streams so that the output wouldn't be completely useless.
|
168
|
+
|
169
|
+
Right now running 1 deploy test takes about 3 min on my machine.
|
170
|
+
|
171
|
+
## Git Based Deploys
|
172
|
+
|
173
|
+
It would be great to allow hatchet to deploy apps off of git url, however if we do that we could open ourselves up to false negatives, if we are pointing at an external repo that gets broken.
|
174
|
+
|
175
|
+
|
176
|
+
## Features?
|
177
|
+
|
178
|
+
What else do we want to test? Config vars, addons, etc. Let's write some tests.
|
data/Rakefile
ADDED
data/bin/hatchet
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
unless File.respond_to? :realpath
|
4
|
+
class File #:nodoc:
|
5
|
+
def self.realpath path
|
6
|
+
return realpath(File.readlink(path)) if symlink?(path)
|
7
|
+
path
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
$: << File.expand_path(File.dirname(File.realpath(__FILE__)) + '/../lib')
|
12
|
+
|
13
|
+
require 'hatchet'
|
14
|
+
require 'thor'
|
15
|
+
|
16
|
+
class HatchetCLI < Thor
|
17
|
+
desc "install", "installs repos defined in 'hatchet.json'"
|
18
|
+
def install
|
19
|
+
warn_dot_ignore!
|
20
|
+
puts "Installing repos for hatchet"
|
21
|
+
dirs.each do |directory, git_repo|
|
22
|
+
if Dir[directory].present?
|
23
|
+
puts "== Detected #{git_repo} in #{directory}, pulling\n"
|
24
|
+
pull(directory, git_repo)
|
25
|
+
else
|
26
|
+
puts "== Did not find #{git_repo} in #{directory}, cloning\n"
|
27
|
+
clone(directory, git_repo)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
desc "list", "lists all repos and their destination listed in hatchet.json"
|
33
|
+
def list
|
34
|
+
repos.each do |repo, directory|
|
35
|
+
puts "#{repo}: #{directory}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def warn_dot_ignore!
|
42
|
+
gitignore = File.open('.gitignore').read
|
43
|
+
return if gitignore.include?(config.repo_directory_path)
|
44
|
+
puts "WARNING: add #{File.join(config.repo_directory_path, '*')} to your .gitignore file \n\n"
|
45
|
+
end
|
46
|
+
|
47
|
+
def config
|
48
|
+
@config ||= Hatchet::Config.new
|
49
|
+
end
|
50
|
+
|
51
|
+
def repos
|
52
|
+
config.repos
|
53
|
+
end
|
54
|
+
|
55
|
+
def dirs
|
56
|
+
config.dirs
|
57
|
+
end
|
58
|
+
|
59
|
+
def pull(path, git_repo)
|
60
|
+
Dir.chdir(path) do
|
61
|
+
`git pull --rebase #{git_repo} master`
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def clone(path, git_repo)
|
66
|
+
path = File.join(path, '..') # up one dir to prevent repos/codetriage/codetriage/#...
|
67
|
+
FileUtils.mkdir_p(path) # create directory
|
68
|
+
Dir.chdir(path) do
|
69
|
+
`git clone #{git_repo}`
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
HatchetCLI.start(ARGV)
|
data/hatchet.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'hatchet/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "heroku_hatchet"
|
8
|
+
gem.version = Hatchet::VERSION
|
9
|
+
gem.authors = ["Richard Schneeman"]
|
10
|
+
gem.email = ["richard.schneeman+rubygems@gmail.com"]
|
11
|
+
gem.description = %q{Hatchet is a an integration testing library for developing Heroku buildpacks.}
|
12
|
+
gem.summary = %q{Hatchet is a an integration testing library for developing Heroku buildpacks.}
|
13
|
+
gem.homepage = "https://github.com/heroku/hatchet"
|
14
|
+
gem.license = "MIT"
|
15
|
+
|
16
|
+
gem.files = `git ls-files`.split($/)
|
17
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
18
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
|
+
gem.require_paths = ["lib"]
|
20
|
+
|
21
|
+
gem.add_dependency "heroku-api"
|
22
|
+
gem.add_dependency "activesupport"
|
23
|
+
gem.add_development_dependency "rake"
|
24
|
+
gem.add_dependency "anvil-cli"
|
25
|
+
gem.add_dependency "excon"
|
26
|
+
gem.add_dependency "thor"
|
27
|
+
end
|
28
|
+
|
data/hatchet.json
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
module Hatchet
|
5
|
+
class AnvilApp < App
|
6
|
+
|
7
|
+
def initialize(directory, options = {})
|
8
|
+
@buildpack = options[:buildpack]
|
9
|
+
@buildpack ||= File.expand_path('.')
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def push!
|
14
|
+
slug_url = nil
|
15
|
+
|
16
|
+
begin
|
17
|
+
stderr_orig = $stderr
|
18
|
+
stdout_orig = $stdout
|
19
|
+
string_io = StringIO.new
|
20
|
+
$stderr = string_io
|
21
|
+
slug_url = Anvil::Engine.build(".", :buildpack => @buildpack, :pipeline => true)
|
22
|
+
puts "Releasing to http://#{@name}.herokuapp.com"
|
23
|
+
response = release(@name, slug_url)
|
24
|
+
while response.status == 202
|
25
|
+
response = Excon.get("#{release_host}#{response.headers["Location"]}")
|
26
|
+
end
|
27
|
+
rescue Anvil::Builder::BuildError => e
|
28
|
+
output = $stderr.dup
|
29
|
+
stdout_orig.puts output.string # print the errors to the test output
|
30
|
+
return [false, output.string]
|
31
|
+
ensure
|
32
|
+
$stderr = stderr_orig
|
33
|
+
$stdout = stdout_orig
|
34
|
+
end
|
35
|
+
|
36
|
+
[true, string_io.string]
|
37
|
+
end
|
38
|
+
|
39
|
+
def teardown!
|
40
|
+
super
|
41
|
+
FileUtils.rm_rf("#{directory}/.anvil")
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def release(name, slug_url)
|
46
|
+
headers = {"Content-Type" => "application/json", accept: :json}
|
47
|
+
release_options = {description: "Anvil Build", slug_url: slug_url }
|
48
|
+
Excon.post("#{release_host}/v1/apps/#{name}/release",
|
49
|
+
headers: headers,
|
50
|
+
body: release_options.to_json)
|
51
|
+
end
|
52
|
+
|
53
|
+
def release_host
|
54
|
+
"https://:#{api_key}@cisaurus.heroku.com"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/hatchet/app.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
module Hatchet
|
2
|
+
class App
|
3
|
+
attr_reader :name, :directory
|
4
|
+
|
5
|
+
def initialize(repo_name, options = {})
|
6
|
+
@directory = config.path_for_name(repo_name)
|
7
|
+
@name = options[:name] || "test-app-#{Time.now.to_f}".gsub('.', '-')
|
8
|
+
@debug = options[:debug] || options[:debugging]
|
9
|
+
end
|
10
|
+
|
11
|
+
# config is read only, should be threadsafe
|
12
|
+
def self.config
|
13
|
+
@config ||= Config.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def config
|
17
|
+
self.class.config
|
18
|
+
end
|
19
|
+
|
20
|
+
# runs a command on heroku similar to `$ heroku run #foo`
|
21
|
+
# but programatically and with more control
|
22
|
+
def run(command, timeout = nil, &block)
|
23
|
+
ProcessSpawn.new(command, self, timeout).run(&block)
|
24
|
+
end
|
25
|
+
|
26
|
+
# set debug: true when creating app if you don't want it to be
|
27
|
+
# automatically destroyed, useful for debugging...bad for app limits.
|
28
|
+
# turn on global debug by setting HATCHET_DEBUG=true in the env
|
29
|
+
def debug?
|
30
|
+
@debug || ENV['HATCHET_DEBUG'] || false
|
31
|
+
end
|
32
|
+
alias :debugging? :debug?
|
33
|
+
|
34
|
+
def not_debugging?
|
35
|
+
!debug?
|
36
|
+
end
|
37
|
+
alias :no_debug? :not_debugging?
|
38
|
+
|
39
|
+
def deployed?
|
40
|
+
!heroku.get_ps(name).body.detect {|ps| ps["process"].include?("web") }.nil?
|
41
|
+
end
|
42
|
+
|
43
|
+
# creates a new heroku app via the API
|
44
|
+
def setup!
|
45
|
+
heroku.post_app(name: name)
|
46
|
+
@app_is_setup = true
|
47
|
+
end
|
48
|
+
|
49
|
+
def push!
|
50
|
+
raise NotImplementedError
|
51
|
+
end
|
52
|
+
|
53
|
+
def teardown!
|
54
|
+
return false unless @app_is_setup
|
55
|
+
if debugging?
|
56
|
+
puts "Debugging App:#{name}"
|
57
|
+
return false
|
58
|
+
end
|
59
|
+
heroku.delete_app(name)
|
60
|
+
end
|
61
|
+
|
62
|
+
# creates a new app on heroku, "pushes" via anvil or git
|
63
|
+
# then yields to self so you can call self.run or
|
64
|
+
# self.deployed?
|
65
|
+
def deploy(&block)
|
66
|
+
Dir.chdir(directory) do
|
67
|
+
self.setup!
|
68
|
+
result, output = self.push!
|
69
|
+
block.call(self, heroku, output)
|
70
|
+
end
|
71
|
+
ensure
|
72
|
+
self.teardown!
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
def api_key
|
77
|
+
@api_key ||= ENV['HEROKU_API_KEY'] || `heroku auth:token`.chomp
|
78
|
+
end
|
79
|
+
|
80
|
+
def heroku
|
81
|
+
@heroku ||= Heroku::API.new(api_key: api_key)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Hatchet
|
2
|
+
class MissingConfig < Errno::ENOENT
|
3
|
+
def initialize
|
4
|
+
super("could not find a 'hatchet.json' file in root directory")
|
5
|
+
end
|
6
|
+
end
|
7
|
+
class ParserError < JSON::ParserError; end
|
8
|
+
class BadRepoName < StandardError
|
9
|
+
def initialize(name, paths)
|
10
|
+
msg = "could not find repo: '#{name}', check for spelling or " <<
|
11
|
+
"duplicate repos. Run `$ hatchet list` to see all " <<
|
12
|
+
"repo options. Checked in #{paths.inspect}. \n\n" <<
|
13
|
+
" make sure repos are installed by running `$ hatchet install`"
|
14
|
+
super(msg)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# This class is responsible for parsing hatchet.json into something
|
19
|
+
# meaninful.
|
20
|
+
class Config
|
21
|
+
REPOS_DIR_NAME = "repos" # the top level name of repos folder
|
22
|
+
REPOS_DIRECTORY_ROOT = '.' # the the root directory where your repos folder will be stored
|
23
|
+
|
24
|
+
attr_accessor :repos, :dirs
|
25
|
+
|
26
|
+
def repo_directory_path
|
27
|
+
File.join(@repo_directory_path, REPOS_DIR_NAME)
|
28
|
+
end
|
29
|
+
|
30
|
+
# creates new config object, pass in directory where `heroku.json`
|
31
|
+
# is located
|
32
|
+
def initialize(directory = '.')
|
33
|
+
self.repos = {}
|
34
|
+
self.dirs = {}
|
35
|
+
Dir.chdir(directory) do
|
36
|
+
config_file = File.open('hatchet.json').read
|
37
|
+
init_config! JSON.parse(config_file)
|
38
|
+
end
|
39
|
+
rescue Errno::ENOENT
|
40
|
+
raise MissingConfig
|
41
|
+
rescue JSON::ParserError => e
|
42
|
+
raise ParserError, "Improperly formatted json in 'hatchet.json' \n\n" + e.message
|
43
|
+
end
|
44
|
+
|
45
|
+
# use this method to turn "codetriage" into repos/rails3/codetriage
|
46
|
+
def path_for_name(name)
|
47
|
+
possible_paths = [repos[name.to_s], "repos/#{name}", name].compact
|
48
|
+
path = possible_paths.detect do |path|
|
49
|
+
Dir[path].present?
|
50
|
+
end
|
51
|
+
raise BadRepoName.new(name, possible_paths) if path.blank?
|
52
|
+
path
|
53
|
+
end
|
54
|
+
|
55
|
+
# 'git@github.com:codetriage/codetriage.git' => 'codetriage'
|
56
|
+
def name_from_git_repo(repo)
|
57
|
+
repo.split('/').last.chomp('.git')
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def set_internal_config!(config)
|
63
|
+
@internal_config = config.delete('hatchet') || {}
|
64
|
+
@repo_directory_path = @internal_config['directory'] || REPOS_DIRECTORY_ROOT
|
65
|
+
config
|
66
|
+
end
|
67
|
+
|
68
|
+
# pulls out config and makes easy to use hashes
|
69
|
+
# dirs has the repo paths as keys and the git_repos as values
|
70
|
+
# repos has repo names as keys and the paths as values
|
71
|
+
def init_config!(config)
|
72
|
+
set_internal_config!(config)
|
73
|
+
config.each do |(directory, git_repos)|
|
74
|
+
git_repos.each do |git_repo|
|
75
|
+
git_repo = git_repo.include?("github.com") ? git_repo : "git@github.com:#{git_repo}.git"
|
76
|
+
repo_name = name_from_git_repo(git_repo)
|
77
|
+
repo_path = File.join(repo_directory_path, directory, repo_name)
|
78
|
+
if repos.key? repo_name
|
79
|
+
puts " warning duplicate repo found: #{repo_name.inspect}"
|
80
|
+
repos[repo_name] = false
|
81
|
+
else
|
82
|
+
repos[repo_name] = repo_path
|
83
|
+
end
|
84
|
+
dirs[repo_path] = git_repo
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Hatchet
|
2
|
+
# used for deploying a test app to heroku via git
|
3
|
+
class GitApp < App
|
4
|
+
BUILDPACK_URL = "https://github.com/heroku/heroku-buildpack-ruby.git"
|
5
|
+
|
6
|
+
def initialize(directory, options = {})
|
7
|
+
@buildpack = options[:buildpack] || options[:buildpack_url] || BUILDPACK_URL
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def setup!
|
12
|
+
super
|
13
|
+
heroku.put_config_vars(name, 'BUILDPACK_URL' => @buildpack)
|
14
|
+
end
|
15
|
+
|
16
|
+
def git_repo
|
17
|
+
"git@heroku.com:#{name}.git"
|
18
|
+
end
|
19
|
+
|
20
|
+
def push!
|
21
|
+
output = `git push #{git_repo} master`
|
22
|
+
[$?.success?, output]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'pty'
|
2
|
+
module Hatchet
|
3
|
+
# spawns a process on Heroku, and keeps it open for writing
|
4
|
+
# like `heroku run bash`
|
5
|
+
class ProcessSpawn
|
6
|
+
attr_reader :command, :app, :timeout
|
7
|
+
|
8
|
+
TIMEOUT = 20 # seconds to bring up a heroku command like `heroku run bash`
|
9
|
+
|
10
|
+
def initialize(command, app, timeout = nil)
|
11
|
+
@command = command
|
12
|
+
@app = app
|
13
|
+
@timeout = timeout || TIMEOUT
|
14
|
+
end
|
15
|
+
|
16
|
+
def ready?
|
17
|
+
`heroku ps -a #{app.name}`.match(/^run.*up.*`#{command}`/).present?
|
18
|
+
end
|
19
|
+
|
20
|
+
def not_ready?
|
21
|
+
!ready?
|
22
|
+
end
|
23
|
+
|
24
|
+
def wait_for_spawn!
|
25
|
+
while not_ready?
|
26
|
+
sleep 1
|
27
|
+
end
|
28
|
+
return true
|
29
|
+
end
|
30
|
+
|
31
|
+
# Open up PTY (pseudo terminal) to command like `heroku run bash`
|
32
|
+
# Wait for the dyno to deploy, then allow user to run arbitrary commands
|
33
|
+
#
|
34
|
+
def run(&block)
|
35
|
+
raise "need app" unless app.present?
|
36
|
+
raise "need command" unless command.present?
|
37
|
+
output, input, pid = PTY.spawn("heroku run #{command} -a #{app.name}")
|
38
|
+
stream = StreamExec.new(input, output)
|
39
|
+
stream.timeout("waiting for spawn", timeout) do
|
40
|
+
wait_for_spawn!
|
41
|
+
end
|
42
|
+
raise "Could not run: #{command}" unless self.ready?
|
43
|
+
yield stream
|
44
|
+
ensure
|
45
|
+
stream.close if stream.present?
|
46
|
+
Process.kill('TERM', pid) if pid.present?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
module Hatchet
|
3
|
+
# runs arbitrary commands within a Heroku process
|
4
|
+
class StreamExec
|
5
|
+
attr_reader :input, :output
|
6
|
+
TIMEOUT = 1 # seconds to run an arbitrary command on a heroku process like `$ls`
|
7
|
+
|
8
|
+
def initialize(input, output)
|
9
|
+
@input = input
|
10
|
+
@output = output
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(cmd)
|
14
|
+
raise "command expected" if cmd.blank?
|
15
|
+
input.write("#{cmd}\n")
|
16
|
+
return read(cmd)
|
17
|
+
end
|
18
|
+
|
19
|
+
def close
|
20
|
+
timeout("closing stream") do
|
21
|
+
input.close
|
22
|
+
output.close
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# There be dragons - (You're playing with process deadlock)
|
27
|
+
#
|
28
|
+
# We want to read the whole output of the command
|
29
|
+
# First pull all contents from stdout (except we don't know how many there are)
|
30
|
+
# So we have to go until our process deadlocks, then we timeout and return the string
|
31
|
+
#
|
32
|
+
# Example
|
33
|
+
# result = ""
|
34
|
+
# input.write("ls\n")
|
35
|
+
# Timeout::timeout(1) {output.each {|x| result << x}}
|
36
|
+
# Timeout::Error: execution expired
|
37
|
+
# puts result
|
38
|
+
# # => "ls\r\r\napp\tconfig.ru Gemfile\t LICENSE.txt public\t script vendor\r\r\nbin\tdb\t Gemfile.lock log\t Rakefile\t test\r\r\nconfig\tdoc\t lib\t\t Procfile README.md tmp\r\r\n"
|
39
|
+
#
|
40
|
+
# Now we want to remove the original command ("ls\r\r\n") and return the remainder
|
41
|
+
def read(cmd, str = "")
|
42
|
+
timeout do
|
43
|
+
# this is guaranteed to timeout; output.each will not return
|
44
|
+
output.each { |line| str << line }
|
45
|
+
end
|
46
|
+
str.split("#{cmd}\r\r\n").last
|
47
|
+
end
|
48
|
+
|
49
|
+
def timeout(msg = nil, val = TIMEOUT, &block)
|
50
|
+
Timeout::timeout(val) do
|
51
|
+
yield
|
52
|
+
end
|
53
|
+
rescue Timeout::Error
|
54
|
+
puts "timeout #{msg}" if msg
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
data/lib/hatchet.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'heroku/api'
|
2
|
+
require 'anvil/engine'
|
3
|
+
require 'active_support/core_ext/object/blank'
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
require 'stringio'
|
7
|
+
require 'fileutils'
|
8
|
+
|
9
|
+
module Hatchet
|
10
|
+
class App
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'hatchet/version'
|
15
|
+
require 'hatchet/app'
|
16
|
+
require 'hatchet/anvil_app'
|
17
|
+
require 'hatchet/git_app'
|
18
|
+
require 'hatchet/stream_exec'
|
19
|
+
require 'hatchet/process_spawn'
|
20
|
+
require 'hatchet/config'
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# Heroku Buildpack: Ø
|
2
|
+
|
3
|
+
Use Ø if you need Heroku to execute a binary.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
Create a directory for our Heroku app:
|
8
|
+
|
9
|
+
```bash
|
10
|
+
$ mkdir -p myapp/bin
|
11
|
+
$ cd myapp
|
12
|
+
```
|
13
|
+
|
14
|
+
Here is an example of an executable that will run on 64bit linux machine:
|
15
|
+
|
16
|
+
```bash
|
17
|
+
$ echo -e "#\!/usr/bin/env bash\n echo hello world" > ./bin/program
|
18
|
+
$ echo -e "program: bin/program" > Procfile
|
19
|
+
$ chmod +x ./bin/program
|
20
|
+
$ ./bin/program
|
21
|
+
hello world
|
22
|
+
```
|
23
|
+
|
24
|
+
Push the app to Heroku and run our executable:
|
25
|
+
|
26
|
+
```bash
|
27
|
+
$ git init; git add .; git commit -am 'init'
|
28
|
+
$ heroku create --buildpack http://github.com/ryandotsmith/null-buildpack.git
|
29
|
+
$ git push heroku master
|
30
|
+
$ heroku run program
|
31
|
+
Running `program` attached to terminal... up, run.8663
|
32
|
+
hello world
|
33
|
+
```
|
34
|
+
|
35
|
+
## Motivation
|
36
|
+
|
37
|
+
I wanted to run various executables (e.g. [log-shuttle](https://github.com/ryandotsmith/log-shuttle)) on Heroku without compiling them on Heroku. Thus, I compile programs on my linux 64 machine, or fetch the binary from the project, commit them to a repo and then run them on Heroku with the Ø buildpack.
|
38
|
+
|
39
|
+
## Issues
|
40
|
+
|
41
|
+
You will need to make sure that a 64bit linux machine can execute the binary.
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class AnvilTest < Test::Unit::TestCase
|
4
|
+
def test_deploy
|
5
|
+
Dir.chdir('test/fixtures/builpacks/null-buildpack') do
|
6
|
+
Hatchet::AnvilApp.new("codetriage",debug: true).deploy do |app|
|
7
|
+
assert true
|
8
|
+
app.run("bash") do |cmd|
|
9
|
+
# cmd.run("cd public/assets")
|
10
|
+
|
11
|
+
assert cmd.run("cat Gemfile").include?("gem 'pg'")
|
12
|
+
|
13
|
+
# deploying with null buildpack, no assets should be compiled
|
14
|
+
refute cmd.run("ls public/assets").include?("application.css")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ConfigTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@config = Hatchet::Config.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_config_path_for_name
|
10
|
+
assert_equal 'test/fixtures/repos/rails3/codetriage', @config.path_for_name('codetriage')
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_config_dirs
|
14
|
+
expected_dirs = { "test/fixtures/repos/rails3/codetriage" => "git@github.com:sharpstone/codetriage.git",
|
15
|
+
"test/fixtures/repos/rails2/rails2blog" => "git@github.com:sharpstone/rails2blog.git" }
|
16
|
+
assert_equal expected_dirs, @config.dirs
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_config_repos
|
20
|
+
expected_repos = { "codetriage" => "test/fixtures/repos/rails3/codetriage",
|
21
|
+
"rails2blog" => "test/fixtures/repos/rails2/rails2blog" }
|
22
|
+
assert_equal expected_repos, @config.repos
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_no_internal_config_raises_no_errors
|
26
|
+
# assert no_raise
|
27
|
+
@config.send :set_internal_config!, {}
|
28
|
+
assert_equal './repos', @config.repo_directory_path
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_github_shortcuts
|
32
|
+
@config.send :init_config!, {"foo" => ["schneems/sextant"]}
|
33
|
+
assert_equal("git@github.com:schneems/sextant.git", @config.dirs["./repos/foo/sextant"])
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TriageTest < Test::Unit::TestCase
|
4
|
+
def test_foo
|
5
|
+
Hatchet::GitApp.new("codetriage").deploy do |app|
|
6
|
+
assert true
|
7
|
+
assert app.deployed?
|
8
|
+
app.run("bash") do |cmd|
|
9
|
+
# cmd.run("cd public/assets")
|
10
|
+
assert cmd.run("ls public/assets").include?("application.css")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: heroku_hatchet
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Richard Schneeman
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: heroku-api
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: activesupport
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rake
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: anvil-cli
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: excon
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: thor
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :runtime
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
description: Hatchet is a an integration testing library for developing Heroku buildpacks.
|
111
|
+
email:
|
112
|
+
- richard.schneeman+rubygems@gmail.com
|
113
|
+
executables:
|
114
|
+
- hatchet
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- .gitignore
|
119
|
+
- Gemfile
|
120
|
+
- LICENSE.txt
|
121
|
+
- README.md
|
122
|
+
- Rakefile
|
123
|
+
- bin/hatchet
|
124
|
+
- hatchet.gemspec
|
125
|
+
- hatchet.json
|
126
|
+
- lib/hatchet.rb
|
127
|
+
- lib/hatchet/anvil_app.rb
|
128
|
+
- lib/hatchet/app.rb
|
129
|
+
- lib/hatchet/config.rb
|
130
|
+
- lib/hatchet/git_app.rb
|
131
|
+
- lib/hatchet/process_spawn.rb
|
132
|
+
- lib/hatchet/stream_exec.rb
|
133
|
+
- lib/hatchet/tasks.rb
|
134
|
+
- lib/hatchet/version.rb
|
135
|
+
- test/fixtures/builpacks/null-buildpack/bin/compile
|
136
|
+
- test/fixtures/builpacks/null-buildpack/bin/detect
|
137
|
+
- test/fixtures/builpacks/null-buildpack/bin/release
|
138
|
+
- test/fixtures/builpacks/null-buildpack/hatchet.json
|
139
|
+
- test/fixtures/builpacks/null-buildpack/readme.md
|
140
|
+
- test/hatchet/anvil_test.rb
|
141
|
+
- test/hatchet/config_test.rb
|
142
|
+
- test/hatchet/git_test.rb
|
143
|
+
- test/test_helper.rb
|
144
|
+
homepage: https://github.com/heroku/hatchet
|
145
|
+
licenses:
|
146
|
+
- MIT
|
147
|
+
post_install_message:
|
148
|
+
rdoc_options: []
|
149
|
+
require_paths:
|
150
|
+
- lib
|
151
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
152
|
+
none: false
|
153
|
+
requirements:
|
154
|
+
- - ! '>='
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
version: '0'
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
|
+
none: false
|
159
|
+
requirements:
|
160
|
+
- - ! '>='
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
requirements: []
|
164
|
+
rubyforge_project:
|
165
|
+
rubygems_version: 1.8.24
|
166
|
+
signing_key:
|
167
|
+
specification_version: 3
|
168
|
+
summary: Hatchet is a an integration testing library for developing Heroku buildpacks.
|
169
|
+
test_files:
|
170
|
+
- test/fixtures/builpacks/null-buildpack/bin/compile
|
171
|
+
- test/fixtures/builpacks/null-buildpack/bin/detect
|
172
|
+
- test/fixtures/builpacks/null-buildpack/bin/release
|
173
|
+
- test/fixtures/builpacks/null-buildpack/hatchet.json
|
174
|
+
- test/fixtures/builpacks/null-buildpack/readme.md
|
175
|
+
- test/hatchet/anvil_test.rb
|
176
|
+
- test/hatchet/config_test.rb
|
177
|
+
- test/hatchet/git_test.rb
|
178
|
+
- test/test_helper.rb
|
179
|
+
has_rdoc:
|