heroku_hatchet 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .DS_Store
2
+ test/fixtures/repos/*
3
+
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
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
@@ -0,0 +1,5 @@
1
+ # encoding: UTF-8
2
+ require 'bundler/gem_tasks'
3
+
4
+ require 'hatchet/tasks'
5
+
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,5 @@
1
+ {
2
+ "hatchet": {"directory": "test/fixtures"},
3
+ "rails3": ["sharpstone/codetriage"],
4
+ "rails2": ["sharpstone/rails2blog"]
5
+ }
@@ -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
@@ -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
+
@@ -0,0 +1,10 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'lib'
6
+ t.libs << 'test'
7
+ t.pattern = 'test/hatchet/**/*_test.rb'
8
+ t.verbose = false
9
+ end
10
+
@@ -0,0 +1,3 @@
1
+ module Hatchet
2
+ VERSION = "0.0.1"
3
+ end
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,4 @@
1
+ #!/usr/bin/env bash
2
+ # bin/compile <build-dir> <cache-dir>
3
+
4
+ echo "-----> Nothing to do."
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ # bin/detect <build-dir>
3
+
4
+ echo "Null"
5
+ exit 0
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+
3
+ echo "--- {}"
@@ -0,0 +1,5 @@
1
+ {
2
+ "hatchet": {"directory": "../.."},
3
+ "rails3": ["sharpstone/codetriage"],
4
+ "rails2": ["sharpstone/rails2blog"]
5
+ }
@@ -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
+
@@ -0,0 +1,11 @@
1
+ Bundler.require
2
+
3
+
4
+ require 'hatchet'
5
+ require 'test/unit'
6
+
7
+
8
+ def assert_tests_run
9
+ end
10
+
11
+
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: