Permalink
Browse files

Use hatchet.json for config

The new `hatchet.json` stores info of where your external rails/ruby projects that you're testing against are located. This goes along with the new `$ hatchet` CLI.

Declare dependencies in your `hatchet.json` like this:

    {
      "rails3": ["git@github.com:codetriage/codetriage.git"],
      "rails2": ["git@github.com:heroku/rails2blog.git"]
    }

Then run `$ hatchet install` which will clone a directory structure like this:

    repos/
      rails3/
        codetriage/
          #...
      rails2/
        rails2blog/
          # …

Since `hatchet.json` manages these dependencies, you can now just use the name of the repo such as `codetriage` instead of the full path to the repo such as `repos/rails3/codetriage'

After installing repos you should add the directory where your repos live in your `.gitignore` file so they are not required when a buildpack is required in production on Heroku.
  • Loading branch information...
1 parent 8ad274d commit 40e9dabf9d210c0ad372ca103b8951f53f3b5253 @schneems schneems committed Mar 21, 2013
View
@@ -1 +1,3 @@
-.DS_Store
+.DS_Store
+test/fixtures/repos/*
+
View
@@ -0,0 +1,38 @@
+PATH
+ remote: .
+ specs:
+ hatchet (0.0.1)
+ activesupport
+ anvil-cli
+ excon
+ heroku-api
+ rake
+ thor
+
+GEM
+ remote: http://rubygems.org/
+ specs:
+ activesupport (3.2.13)
+ i18n (= 0.6.1)
+ multi_json (~> 1.0)
+ anvil-cli (0.15.0)
+ progress (~> 2.4.0)
+ rest-client (~> 1.6.7)
+ thor (~> 0.15.2)
+ excon (0.16.10)
+ heroku-api (0.3.8)
+ excon (~> 0.16.10)
+ i18n (0.6.1)
+ mime-types (1.21)
+ multi_json (1.7.1)
+ progress (2.4.0)
+ rake (10.0.3)
+ rest-client (1.6.7)
+ mime-types (>= 1.16)
+ thor (0.15.4)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ hatchet!
View
@@ -84,6 +84,80 @@ You can specify buildpack to deploy with like so:
Hatchet::App.new("repos/rails3/codetriage", buildpack: "https://github.com/schneems/heroku-buildpack-ruby.git").deploy do |app|
+## Hatchet Config
+
+Hatchet is designed to test buildpacks, and requires full repositories
+to deploy to Heroku. Web application repos, especially Rails repos, aren't known for
+being small, if you're testing a custom buildpack and have
+`BUILDPACK_URL` set in your app config, it needs to be cloned each time
+you deploy your app. If you've `git add`-ed a bunch of repos then this
+clone would be pretty slow, we're not going to do this. Do not commit
+your repos to git.
+
+Instead we will keep a structured file called
+inventively `hatchet.json` at the root of your project. This file will
+describe the structure of your repos, have the name of the repo, and a
+git url. We will use it to sync remote git repos with your local
+project. It might look something like this
+
+ {
+ "hatchet": {},
+ "rails3": ["git@github.com:codetriage/codetriage.git"],
+ "rails2": ["git@github.com:heroku/rails2blog.git"]
+ }
+
+the 'hatchet' object accessor is reserved for hatchet settings.
+. To copy each repo in your `hatchet.json`
+run the command:
+
+ $ hatchet install
+
+The above `hatchet.json` will produce a directory structure like this:
+
+ repos/
+ rails3/
+ codetriage/
+ #...
+ rails2/
+ rails2blog/
+ # ...
+
+While you are running your tests if you reference a repo that isn't
+synced locally Hatchet will raise an error. Since you're using a
+standard file for your repos, you can now reference the name of the git
+repo, provided you don't have conflicting names:
+
+ Hatchet::App.new("codetriage").deploy do |app|
+
+If you do have conflicting names, use full paths.
+
+A word of warning on including rails/ruby repos inside of your test
+directory, if you're using a runner that looks for patterns such as
+`*_test.rb` to run your hatchet tests, it may incorrectly think you want
+to run the tests inside of the rails repositories. To get rid of this
+problem move your repos direcory out of `test/` or be more specific
+with your tests such as moving them to a `test/hatchet` directory and
+changing your pattern if you are using `Rake::TestTask` it might look like this:
+
+ t.pattern = 'test/hatchet/**/*_test.rb'
+
+A note on external repos: since you're basing tests on these repos, it
+is in your best interest to not change them or your tests may
+spontaneously fail. In the future we may create a hatchet.lockfile or
+something to declare the commit
+
+## Hatchet CLI
+
+Hatchet has a CLI for installing and maintaining external repos you're
+using to test against. If you have Hatchet installed as a gem run
+
+ $ hatchet --help
+
+For more info on commands. If you're using the source code you can run
+the command by going to the source code directory and running:
+
+ $ ./bin/hatchet --help
+
## The Future
View
@@ -1,2 +1,5 @@
# encoding: UTF-8
require 'bundler/gem_tasks'
+
+require 'hatchet/tasks'
+
View
@@ -0,0 +1,74 @@
+#!/usr/bin/env ruby
+
+unless File.respond_to? :realpath
+ class File #:nodoc:
+ def self.realpath path
+ return realpath(File.readlink(path)) if symlink?(path)
+ path
+ end
+ end
+end
+$: << File.expand_path(File.dirname(File.realpath(__FILE__)) + '/../lib')
+
+require 'hatchet'
+require 'thor'
+
+class HatchetCLI < Thor
+ desc "install", "installs repos defined in 'hatchet.json'"
+ def install
+ warn_dot_ignore!
+ puts "Installing repos for hatchet"
+ dirs.each do |directory, git_repo|
+ if Dir[directory].present?
+ puts "== Detected #{git_repo} in #{directory}, pulling\n"
+ pull(directory, git_repo)
+ else
+ puts "== Did not find #{git_repo} in #{directory}, cloning\n"
+ clone(directory, git_repo)
+ end
+ end
+ end
+
+ desc "list", "lists all repos and their destination listed in hatchet.json"
+ def list
+ repos.each do |repo, directory|
+ puts "#{repo}: #{directory}"
+ end
+ end
+
+ private
+
+ def warn_dot_ignore!
+ gitignore = File.open('.gitignore').read
+ return if gitignore.include?(config.repo_directory_path)
+ puts "WARNING: add #{File.join(config.repo_directory_path, '*')} to your .gitignore file \n\n"
+ end
+
+ def config
+ @config ||= Hatchet::Config.new
+ end
+
+ def repos
+ config.repos
+ end
+
+ def dirs
+ config.dirs
+ end
+
+ def pull(path, git_repo)
+ Dir.chdir(path) do
+ `git pull --rebase #{git_repo} master`
+ end
+ end
+
+ def clone(path, git_repo)
+ path = File.join(path, '..') # up one dir to prevent repos/codetriage/codetriage/#...
+ FileUtils.mkdir_p(path) # create directory
+ Dir.chdir(path) do
+ `git clone #{git_repo}`
+ end
+ end
+end
+
+HatchetCLI.start(ARGV)
View
@@ -8,9 +8,9 @@ Gem::Specification.new do |gem|
gem.version = Hatchet::VERSION
gem.authors = ["Richard Schneeman"]
gem.email = ["schneems@gmail.com"]
- gem.description = %q{The Hatchet is a an integration testing library for developing Heroku buildpacks.}
- gem.summary = %q{The Hatchet is a an integration testing library for developing Heroku buildpacks.}
- gem.homepage = ""
+ gem.description = %q{Hatchet is a an integration testing library for developing Heroku buildpacks.}
+ gem.summary = %q{Hatchet is a an integration testing library for developing Heroku buildpacks.}
+ gem.homepage = "https://github.com/heroku/hatchet"
gem.license = "MIT"
gem.files = `git ls-files`.split($/)
@@ -23,4 +23,5 @@ Gem::Specification.new do |gem|
gem.add_dependency "rake"
gem.add_dependency "anvil-cli"
gem.add_dependency "excon"
+ gem.add_dependency "thor"
end
View
@@ -0,0 +1,5 @@
+{
+ "hatchet": {"directory": "test/fixtures"},
+ "rails3": ["git@github.com:codetriage/codetriage.git"],
+ "rails2": ["git@github.com:heroku/rails2blog.git"]
+}
View
@@ -2,6 +2,10 @@
require 'anvil/engine'
require 'active_support/core_ext/object/blank'
+require 'json'
+require 'stringio'
+require 'fileutils'
+
module Hatchet
class App
end
@@ -13,3 +17,4 @@ class App
require 'hatchet/git_app'
require 'hatchet/stream_exec'
require 'hatchet/process_spawn'
+require 'hatchet/config'
View
@@ -2,14 +2,18 @@ module Hatchet
class App
attr_reader :name, :directory
- def initialize(directory, options = {})
- @directory = directory
- @name = options[:name] || "test-app-#{Time.now.to_f}".gsub('.', '-')
- @debug = options[:debug]
+ def initialize(repo_name, options = {})
+ @directory = config.path_for_name(repo_name)
+ @name = options[:name] || "test-app-#{Time.now.to_f}".gsub('.', '-')
end
- def git_repo
- "git@heroku.com:#{name}.git"
+ # config is read only, should be threadsafe
+ def self.config
+ @config ||= Config.new
+ end
+
+ def config
+ self.class.config
end
# runs a command on heroku similar to `$ heroku run #foo`
@@ -33,6 +37,7 @@ def deployed?
!heroku.get_ps(name).body.detect {|ps| ps["process"].include?("web") }.nil?
end
+ # creates a new heroku app via the API
def setup!
heroku.post_app(name: name)
@app_is_setup = true
@@ -46,6 +51,9 @@ def teardown!
heroku.delete_app(name)
end
+ # creates a new app on heroku, "pushes" via anvil or git
+ # then yields to self so you can call self.run or
+ # self.deployed?
def deploy(&block)
Dir.chdir(directory) do
self.setup!
View
@@ -0,0 +1,88 @@
+module Hatchet
+ class MissingConfig < Errno::ENOENT
+ def initialize
+ super("could not find a 'hatchet.json' file in root directory")
+ end
+ end
+ class ParserError < JSON::ParserError; end
+ class BadRepoName < StandardError
+ def initialize(name, paths)
+ msg = "could not find repo: '#{name}', check for spelling or " <<
+ "duplicate repos. Run `$ hatchet list` to see all " <<
+ "repo options. Checked in #{paths.inspect}. \n\n" <<
+ " make sure repos are installed by running `$ hatchet install`"
+ super(msg)
+ end
+ end
+
+ # This class is responsible for parsing hatchet.json into something
+ # meaninful.
+ class Config
+ REPOS_DIR_NAME = "repos" # the top level name of repos folder
+ REPOS_DIRECTORY_ROOT = '.' # the the root directory where your repos folder will be stored
+
+ attr_accessor :repos, :dirs
+
+ def repo_directory_path
+ File.join(@repo_directory_path, REPOS_DIR_NAME)
+ end
+
+ # creates new config object, pass in directory where `heroku.json`
+ # is located
+ def initialize(directory = '.')
+ self.repos = {}
+ self.dirs = {}
+ Dir.chdir(directory) do
+ config_file = File.open('hatchet.json').read
+ init_config! JSON.parse(config_file)
+ end
+ rescue Errno::ENOENT
+ raise MissingConfig
+ rescue JSON::ParserError => e
+ raise ParserError, "Improperly formatted json in 'hatchet.json' \n\n" + e.message
+ end
+
+ # use this method to turn "codetriage" into repos/rails3/codetriage
+ def path_for_name(name)
+ possible_paths = [repos[name.to_s], "repos/#{name}", name].compact
+ path = possible_paths.detect do |path|
+ Dir[path].present?
+ end
+ raise BadRepoName.new(name, possible_paths) if path.blank?
+ path
+ end
+
+ # 'git@github.com:codetriage/codetriage.git' => 'codetriage'
+ def name_from_git_repo(repo)
+ repo.split('/').last.chomp('.git')
+ end
+
+ private
+
+ def set_internal_config!(config)
+ @internal_config = config.delete('hatchet')
+ @repo_directory_path = @internal_config['directory'] || REPOS_DIRECTORY_ROOT
+ end
+
+ # pulls out config and makes easy to use hashes
+ # dirs has the repo paths as keys and the git_repos as values
+ # repos has repo names as keys and the paths as values
+ def init_config!(config)
+ set_internal_config!(config)
+ config.each do |(directory, git_repos)|
+ git_repos.each do |git_repo|
+ repo_name = name_from_git_repo(git_repo)
+ repo_path = File.join(repo_directory_path, directory, repo_name)
+ if repos.key? repo_name
+ puts " warning duplicate repo found: #{repo_name.inspect}"
+ repos[repo_name] = false
+ else
+ repos[repo_name] = repo_path
+ end
+ dirs[repo_path] = git_repo
+ end
+ end
+ end
+ end
+end
+
View
@@ -13,6 +13,10 @@ def setup!
heroku.put_config_vars(name, 'BUILDPACK_URL' => @buildpack)
end
+ def git_repo
+ "git@heroku.com:#{name}.git"
+ end
+
def push!
output = `git push #{git_repo} master`
[$?.success?, output]
Oops, something went wrong.

0 comments on commit 40e9dab

Please sign in to comment.