fourchette 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4a982a6a3feff7322d9d0ae4aac49bcbcaf9f4b2
4
- data.tar.gz: 71313a343027783c2dc9e57ebb1030eb25f5451b
3
+ metadata.gz: a81c926b6fccb25d10111bbd826e6f12ac1b4925
4
+ data.tar.gz: b99d9faac8fc580772d1c5e45517343b9ae07427
5
5
  SHA512:
6
- metadata.gz: fd909b27ceaaa68a09ce47d1b15054c31418aaddddd4f559587e7801f1bee6c83feaced2a62f7ed8ab6d034436b230e9c671ce09aa3d0748d06385eefe7f4176
7
- data.tar.gz: 13e6b839fc87727fb7a3e7dd5a9cac45531ff5d621887db81e6896b3b6b5c1d4e8be92f5368a225f011a19b7bc0efd1f4e0302465ff4c283dffe88eac0c5d586
6
+ metadata.gz: 486ac1c0d082e82aab0483acb3acfa34abbbc339c916962fa0bb38c1ef8a01299a9ece417bf3b526bf4f52d9abd06b780415f002e60fd83da721fb39f314f965
7
+ data.tar.gz: e833fb94778d18cf6621d9e96524be64779119bb554796d7a50c3531dac281ae889f9c6b7e3da5dbf4632c5203039758d91c9a8f7facfc89a49a1b907483ae3d
data/.gitignore CHANGED
@@ -15,3 +15,11 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .ruby-version
19
+
20
+ # Mac specific
21
+ .DS_Store
22
+
23
+ # ngrok is for having tunnels to localhost, for local dev
24
+ ngrok
25
+ tags
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0-p353
5
+ - 2.1.0
6
+ - 2.1.1
data/Gemfile CHANGED
@@ -1,4 +1,3 @@
1
- source 'https://rubygems.org'
1
+ source 'http://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in fourchette.gemspec
4
- gemspec
3
+ gemspec
data/Procfile ADDED
@@ -0,0 +1 @@
1
+ web: bundle exec rackup -s puma -p $PORT
data/README.md CHANGED
@@ -1,29 +1,115 @@
1
- # Fourchette
1
+ <p align="center">
2
+ <a href="https://github.com/jipiboily/fourchette">
3
+ <img src="http://i.imgur.com/967yX36.png" alt="Fourchette" />
4
+ </a>
5
+ <br />
6
+ <b>Your new best friend for isolated testing environments on Heroku</b>
7
+ <br />
8
+ <a href="https://codeclimate.com/github/jipiboily/fourchette"><img src="https://codeclimate.com/github/jipiboily/fourchette.png" /></a>
9
+ <a href="https://github.com/jipiboily/fourchette"><img src="https://travis-ci.org/jipiboily/fourchette.png?branch=master" /></a>
10
+ <a href='https://coveralls.io/r/jipiboily/fourchette'><img src='https://coveralls.io/repos/jipiboily/fourchette/badge.png' alt='Coverage Status' /></a>
11
+ </p>
2
12
 
3
- TODO: Write a gem description
13
+ **IMPORTANT: this is a work in progress, use at your own risk.**
14
+
15
+ Fourchette is your new best friend for having isolated testing environements. It will help you test your GitHub PRs against a fork of one your Heroku apps. You will have one Heroku app per PR now. Isn't that amazing? It will make testing way easier and you won't have the (maybe) broken code from other PRs on staging but only the code that requires testing.
16
+
17
+ **IMPORTANT: Please note that forking your Heroku app means it will copy the same addon plans and that you will pay for multiple apps and their addons. Watch out!**
18
+
19
+ ## Flow
20
+
21
+ - a PR is created against your GitHub project
22
+ - Fourchette receives an event via GitHub Hooks
23
+ - it [forks](https://devcenter.heroku.com/articles/fork-app) an environement making it available to you
24
+ - any new commit against that PR will update the code
25
+ - closing the PR will delete the forked app
26
+ - re-opening the PR will re-create a fork
27
+
28
+ ## Diagram
29
+
30
+ Seriously? You need a diagram for that? Nope. Not going to do this. PRs accepted...I guess.
31
+
32
+ ## Features
33
+ - single project
34
+ - configuration is made via environement variables
35
+ - async processing
36
+ - it works, but that's about it for now
4
37
 
5
38
  ## Installation
6
39
 
7
- Add this line to your application's Gemfile:
40
+ Those steps could be made way easier, but this is a really minimal implementation.
41
+
42
+ 1. Add `gem 'fourchette'` to your `Gemfile`
43
+ 2. Run `bundle install`
44
+ 3. Add `require 'fourchette/rake_tasks'` to your `Rakefile`
45
+ 4. Create a `Procfile` and a `config.ru` (using the ones from this repo as example)
46
+ 5. push to Heroku
47
+ 6. configure the right environement variables (see [#configuration](#configuration))
48
+ 7. Enable your Fourchette instance
49
+
50
+ ### Configuration
51
+
52
+ - `export FOURCHETTE_GITHUB_PROJECT="jipiboily/fourchette"`
53
+ - `export FOURCHETTE_GITHUB_USERNAME="jipiboily"`
54
+ - `export FOURCHETTE_GITHUB_PERSONAL_TOKEN='a token here...'` # You can create one here: https://github.com/settings/applications
55
+ - `export FOURCHETTE_HEROKU_USERNAME='me@domain'`
56
+ - `export FOURCHETTE_HEROKU_API_KEY='API key here'`
57
+ - `export FOURCHETTE_HEROKU_APP_TO_FORK='the name of the app to fork from'`
58
+ - `export FOURCHETTE_APP_URL="http://fourchette-app.herokuapp.com"`
59
+ - `export FOURCHETTE_HEROKU_APP_PREFIX="fourchette"` # This is basically to namespace your forks. In that example, they would be named "fourchette-pr-1234" where "1234" is the PR number. Beware, the name can't be more than 30 characters total! It will be changed to be lowercase only, so you should probably just use lowercase characters anyways.
60
+
61
+ **IMPORTANT**: the GitHub user needs to be an admin of the repo to be able to add, enable or disable the web hook used by Fourchette. You could create it by hand if you prefer.
62
+
63
+ ### Enable your Fourchette instance
64
+
65
+ run `bundle exec rake fourchette:enable`
66
+
67
+ ### Enable, disable, update or delete the hook
68
+
69
+ `bundle exec rake -T` will tell you the rake tasks available. There are tasks to enable, disable or delete the GitHub hook to your Fourchette instance. There is also one to update the hook. That last one is mostly for development, if your local tunnel URl changed and you want to update the hook's URL.
70
+
71
+ ### Before & after steps, aka, callbacks
72
+
73
+ You need to run steps before and/or after the creation of your new Heroku app? Let's say you want to run mirgations after deploying new code. There is a simple (and primitive) way of doing it. It might not be perfect but can work until there is a cleaner and more flexible way of doing so, if required.
74
+
75
+ Create a file in your project to override the `Fourchette::Callbacks` class and include it after Fourchette.
76
+
77
+ You just want to override the `before` or `after` methods of `Fourchette::Callbacks` (`lib/fourchette/callbacks.rb`) to suit your needs. In those methods, you have access to GitHub's hook data via the `@param` instance variable.
78
+
79
+ ## Rake tasks
80
+
81
+ ```
82
+ rake fourchette:console # Brings up a REPL with the code loaded
83
+ rake fourchette:delete # This deletes the Fourchette hook
84
+ rake fourchette:disable # This disables Fourchette hook
85
+ rake fourchette:enable # This enables Fourchette hook
86
+ rake fourchette:update # This updates the Fourchette hook with the current URL of the app
87
+ ```
88
+
89
+ ## Async processing note
8
90
 
9
- gem 'fourchette'
91
+ Fourchette uses [Sucker Punch](https://github.com/brandonhilkert/sucker_punch), "a single-process Ruby asynchronous processing library". No need for redis or extra processes. It also mean it can run for free on Heroku, if this is what you want.
10
92
 
11
- And then execute:
93
+ ## Contribute
12
94
 
13
- $ bundle
95
+ - fork & clone
96
+ - `bundle install`
97
+ - `foreman start`
98
+ - You now have the app running on port 9292
14
99
 
15
- Or install it yourself as:
100
+ Bonus: if you need a tunnel to your local dev machine to work with GitHub hooks, you might want to look at https://ngrok.com/.
16
101
 
17
- $ gem install fourchette
102
+ ## It needs some love...
18
103
 
19
- ## Usage
104
+ What needs to be improved?
20
105
 
21
- TODO: Write usage instructions here
106
+ - currently, it is assuming everything goes well, very little to no error management. This needs to improved.
107
+ - make it simpler to bootstrap a Fourchette app (possibily a rake task to generate the required files and callback overrides)
108
+ - it is not serious until there are specs for it, so add specs for that once we have a solid direction
109
+ - security improvements (we should not accept hooks from anyone else than GitHub)
110
+ - oAuth instead of GitHub token?
111
+ - multi project would be great
22
112
 
23
- ## Contributing
113
+ # Contributors
24
114
 
25
- 1. Fork it
26
- 2. Create your feature branch (`git checkout -b my-new-feature`)
27
- 3. Commit your changes (`git commit -am 'Add some feature'`)
28
- 4. Push to the branch (`git push origin my-new-feature`)
29
- 5. Create new Pull Request
115
+ Thanks to [@jpsirois](https://github.com/jpsirois/) for the logo!
data/Rakefile CHANGED
@@ -1 +1,16 @@
1
- require "bundler/gem_tasks"
1
+ require "fourchette/rake_tasks"
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+ # Set default Rake task to spec
6
+ RSpec::Core::RakeTask.new(:spec)
7
+ task :default => :spec
8
+ rescue LoadError => ex
9
+ # That's ok, it just means we don't have RSpec loaded
10
+ end
11
+
12
+ desc 'Brings up a REPL with the code loaded'
13
+ task :console do
14
+ require './lib/fourchette'
15
+ Pry.start
16
+ end
data/config.ru ADDED
@@ -0,0 +1,3 @@
1
+ require './lib/fourchette'
2
+
3
+ run Sinatra::Application
data/fourchette.gemspec CHANGED
@@ -8,16 +8,28 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Fourchette::VERSION
9
9
  spec.authors = ["Jean-Philippe Boily"]
10
10
  spec.email = ["j@jipi.ca"]
11
- spec.description = %q{Fourchette is your new best friend for having isolated testing environements.}
12
- spec.summary = %q{Fourchette is your new best friend for having isolated testing environements.}
13
- spec.homepage = ""
11
+ spec.summary = %q{Your new best friend for isolated testing environments on Heroku.}
12
+ spec.description = %q{Fourchette is your new best friend for having isolated testing environements. It will help you test your GitHub PRs against a fork of one your Heroku apps. You will have one Heroku app per PR now. Isn't that amazing? It will make testing way easier and you won't have the (maybe) broken code from other PRs on staging but only the code that requires testing.}
13
+ spec.homepage = "https://github.com/jipiboily/fourchette"
14
14
  spec.license = "MIT"
15
15
 
16
- spec.files = `git ls-files`.split($/)
16
+ spec.files = `git ls-files -z`.split("\x0")
17
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_development_dependency "bundler", "~> 1.3"
22
- spec.add_development_dependency "rake"
21
+ spec.add_dependency "bundler", "~> 1.5"
22
+ spec.add_dependency "rake"
23
+ spec.add_dependency "sinatra", "~> 1.4.4"
24
+ spec.add_dependency "sinatra-contrib", "~> 1.4.2"
25
+ spec.add_dependency "octokit", "~> 3.0.0"
26
+ spec.add_dependency "git", "~> 1.2.6"
27
+ spec.add_dependency "heroics", "0.0.2"
28
+ spec.add_dependency "heroku", "~> 3.3.0" # Deprecated, but best/easiest solution for the pgbackups...
29
+ spec.add_dependency "sucker_punch", "~> 1.0.2"
30
+
31
+ spec.add_development_dependency 'foreman', '~> 0.63.0'
32
+ spec.add_development_dependency 'pry-debugger'
33
+ spec.add_development_dependency 'rspec', '~> 2.14.1'
34
+ spec.add_development_dependency 'coveralls'
23
35
  end
data/lib/fourchette.rb CHANGED
@@ -1,5 +1,41 @@
1
1
  require "fourchette/version"
2
+ require 'sinatra'
3
+ require 'json'
4
+ require 'cgi' # Required for Heroics
5
+ require 'heroics'
6
+ require 'octokit'
7
+ require 'git'
8
+ require 'sucker_punch'
9
+
10
+ # TODO: Extract this to development.rb and production.rb
11
+ if development?
12
+ require "sinatra/reloader"
13
+
14
+ begin
15
+ require "pry"
16
+ rescue LoadError => ex
17
+ # That's ok, we don't care...it was probably loaded from another project
18
+ # and not to hack on Fourchette anyways!
19
+ end
20
+
21
+ FOURCHETTE_CONFIG = {
22
+ env_name: 'fourchette-dev'
23
+ }
24
+ else
25
+ FOURCHETTE_CONFIG = {
26
+ env_name: 'fourchette'
27
+ }
28
+ end
29
+
2
30
 
3
31
  module Fourchette
4
- # Your code goes here...
5
32
  end
33
+
34
+ require_relative 'fourchette/logger'
35
+ require_relative 'fourchette/web'
36
+ require_relative 'fourchette/github'
37
+ require_relative 'fourchette/pull_request'
38
+ require_relative 'fourchette/fork'
39
+ require_relative 'fourchette/heroku'
40
+ require_relative 'fourchette/pgbackups'
41
+ require_relative 'fourchette/callbacks'
@@ -0,0 +1,15 @@
1
+ class Fourchette::Callbacks
2
+ include Fourchette::Logger
3
+
4
+ def initialize params
5
+ @params = params
6
+ end
7
+
8
+ def before
9
+ logger.info 'Running before steps...'
10
+ end
11
+
12
+ def after
13
+ logger.info 'Running after steps...'
14
+ end
15
+ end
@@ -0,0 +1,108 @@
1
+ class Fourchette::Fork
2
+ include Fourchette::Logger
3
+
4
+ def initialize params
5
+ @params = params
6
+ @heroku = Fourchette::Heroku.new
7
+ end
8
+
9
+ def update
10
+ create_unless_exists
11
+
12
+ heroku_git_url = @heroku.git_url(fork_name)
13
+
14
+ FileUtils.rm_rf('tmp/')
15
+
16
+ # Add key to current
17
+ logger.info "Creating an SSH key"
18
+ key_path = "~/.ssh/id_rsa-fourchette"
19
+ public_key_path = "#{key_path}.pub"
20
+ `ssh-keygen -t rsa -C "temporary@fourchetteapp" -N "" -f #{key_path} -q`
21
+ public_key_content = `cat #{public_key_path}`
22
+
23
+ # Create SSH config file, so that it uses the right SSH key
24
+ ssh_config_path = "~/.ssh/config"
25
+ if `cat #{ssh_config_path}`.length == 0
26
+ # Set the SSH key used, and disable strict host key checking
27
+ `echo "Host heroku.com\n IdentityFile #{key_path}\n StrictHostKeyChecking no" >> ~/.ssh/config`
28
+ end
29
+
30
+ # Add SSH key to the Heroku account
31
+ logger.info "Adding the SSH key to your Heroku account"
32
+ heroku_public_key = @heroku.client.key.create(public_key: public_key_content)
33
+
34
+ # Clone & push
35
+ logger.info "Cloning repository..."
36
+ repo = Git.clone(github_git_url, 'tmp')
37
+ repo.checkout(branch_name)
38
+
39
+ # TODO - HACK ALERT - Next couple lines are really hacky, and used
40
+ # instead of calling `git push heroku my_branch_name:master`
41
+ begin
42
+ repo.branch('master').delete
43
+ rescue Git::GitExecuteError
44
+ # There is no master branch? Hmmm
45
+ end
46
+
47
+ begin
48
+ repo.branch('master').merge(branch_name)
49
+ rescue Git::GitExecuteError
50
+ # TODO - HACK ALERT! There is certainly a cleaner way to do this...
51
+ end
52
+ repo.add_remote('heroku', heroku_git_url)
53
+
54
+ logger.info "Pushing to Heroku..."
55
+ repo.push(repo.remote('heroku'))
56
+ logger.info "Done pushing to Heroku, apparently!"
57
+
58
+ # REMOVE key to the Heroku account
59
+ logger.info "Removing SSH key from your Heroku account"
60
+ @heroku.client.key.delete(heroku_public_key['id'])
61
+
62
+ # Remove ssh key
63
+ logger.info "Removing SSH key for file system"
64
+ FileUtils.rm_rf("~./ssh/id_rsa-fourchette*")
65
+ end
66
+
67
+ def create
68
+ github.comment_pr(pr_number, "Fourchette is initializing a new fork.")
69
+ create_unless_exists
70
+ update
71
+ end
72
+
73
+ def delete
74
+ @heroku.delete(fork_name)
75
+
76
+ # Update PR with URL
77
+ github.comment_pr(pr_number, "Test app deleted!")
78
+ end
79
+
80
+ private
81
+ def github
82
+ @github ||= Fourchette::GitHub.new
83
+ end
84
+
85
+ def create_unless_exists
86
+ unless @heroku.app_exists?(fork_name)
87
+ @heroku.fork(ENV['FOURCHETTE_HEROKU_APP_TO_FORK'] ,fork_name)
88
+ # Update PR with URL
89
+ github.comment_pr(pr_number, "Test URL: #{@heroku.client.app.info(fork_name)['web_url']}")
90
+ end
91
+ end
92
+
93
+ def fork_name
94
+ "#{ENV['FOURCHETTE_HEROKU_APP_PREFIX']}-PR-#{pr_number}".downcase # It needs to be lowercase only.
95
+ end
96
+
97
+ def github_git_url
98
+ @params['pull_request']['head']['repo']['clone_url'].gsub("//github.com", "//#{ENV['FOURCHETTE_GITHUB_USERNAME']}:#{ENV['FOURCHETTE_GITHUB_PERSONAL_TOKEN']}@github.com")
99
+ end
100
+
101
+ def branch_name
102
+ @branch_name ||= "remotes/origin/#{@params['pull_request']['head']['ref']}"
103
+ end
104
+
105
+ def pr_number
106
+ @pr_number ||= @params['pull_request']['number']
107
+ end
108
+ end
@@ -0,0 +1,101 @@
1
+ class Fourchette::GitHub
2
+ include Fourchette::Logger
3
+
4
+ def enable_hook
5
+ logger.info 'Enabling the hooks for your app...'
6
+ if fourchette_hook
7
+ enable(fourchette_hook)
8
+ else
9
+ create_hook
10
+ end
11
+ end
12
+
13
+ def disable_hook
14
+ logger.info 'Disabling the hook for your app...'
15
+ if fourchette_hook && fourchette_hook.active == true
16
+ disable(fourchette_hook)
17
+ else
18
+ logger.error 'Nothing to disable, move along!'
19
+ end
20
+ end
21
+
22
+ def update_hook
23
+ logger.info 'Updating the hook for your app...'
24
+ toggle_active_state_to fourchette_hook, fourchette_hook.active
25
+ end
26
+
27
+ def delete_hook
28
+ logger.info 'Removing the hook for your app...'
29
+ octokit.remove_hook(ENV['FOURCHETTE_GITHUB_PROJECT'], fourchette_hook.id)
30
+ end
31
+
32
+ def comment_pr pr_number, comment
33
+ comment = "****** FOURCHETTE COMMENT ******\n\n#{comment}\n\n****** END OF FOURCHETTE COMMENT ******"
34
+ octokit.add_comment(ENV['FOURCHETTE_GITHUB_PROJECT'], pr_number, comment)
35
+ end
36
+
37
+ private
38
+ def octokit
39
+ @octokit_client ||= Octokit::Client.new(login: ENV['FOURCHETTE_GITHUB_USERNAME'], password: ENV['FOURCHETTE_GITHUB_PERSONAL_TOKEN'])
40
+ end
41
+
42
+ def create_hook
43
+ logger.info 'Creating a new hook...'
44
+ octokit.create_hook(
45
+ ENV['FOURCHETTE_GITHUB_PROJECT'],
46
+ 'web',
47
+ {
48
+ url: "#{ENV['FOURCHETTE_APP_URL']}/hooks",
49
+ content_type: 'json',
50
+ fourchette_env: FOURCHETTE_CONFIG[:env_name]
51
+ },
52
+ {
53
+ :events => ['pull_request'],
54
+ :active => true
55
+ }
56
+ )
57
+ end
58
+
59
+ def hooks
60
+ octokit.hooks(ENV['FOURCHETTE_GITHUB_PROJECT'])
61
+ end
62
+
63
+ def fourchette_hook
64
+ existing_hook = nil
65
+
66
+ hooks.each do |hook|
67
+ existing_hook = hook unless hook.config && hook.config.fourchette_env.nil?
68
+ end
69
+
70
+ existing_hook
71
+ end
72
+
73
+ def enable(hook)
74
+ if hook.active
75
+ logger.error 'The hook is already active, dude!'
76
+ else
77
+ toggle_active_state_to hook, true
78
+ end
79
+ end
80
+
81
+ def disable(hook)
82
+ toggle_active_state_to hook, false
83
+ end
84
+
85
+ def toggle_active_state_to hook, active_value
86
+ octokit.edit_hook(
87
+ ENV['FOURCHETTE_GITHUB_PROJECT'],
88
+ hook.id,
89
+ 'web',
90
+ {
91
+ url: "#{ENV['FOURCHETTE_APP_URL']}/hooks",
92
+ content_type: 'json',
93
+ fourchette_env: FOURCHETTE_CONFIG[:env_name]
94
+ },
95
+ {
96
+ :events => ['pull_request'],
97
+ :active => active_value
98
+ }
99
+ )
100
+ end
101
+ end
@@ -0,0 +1,74 @@
1
+ class Fourchette::Heroku
2
+ include Fourchette::Logger
3
+
4
+ def app_exists? name
5
+ client.app.list.collect { |app| app if app['name'] == name }.reject(&:nil?).any?
6
+ end
7
+
8
+ def fork from, to
9
+ create_app(to)
10
+ copy_config(from, to)
11
+ copy_add_ons(from, to)
12
+ copy_pg(from, to)
13
+ end
14
+
15
+ def delete app_name
16
+ logger.info "Deleting #{app_name}"
17
+ client.app.delete(app_name)
18
+ end
19
+
20
+ def client
21
+ # TODO: add caching... https://github.com/heroku/heroics/#client-side-caching
22
+ unless @heroku_client
23
+ username = CGI.escape(ENV['FOURCHETTE_HEROKU_USERNAME'])
24
+ token = ENV['FOURCHETTE_HEROKU_API_KEY']
25
+ url = "https://#{username}:#{token}@api.heroku.com/schema"
26
+ options = {default_headers: {'Accept' => 'application/vnd.heroku+json; version=3'}}
27
+ @heroku_client = Heroics.client_from_schema_url(url, options)
28
+ end
29
+ @heroku_client
30
+ end
31
+
32
+ def config_vars app_name
33
+ client.config_var.info(app_name)
34
+ end
35
+
36
+ def git_url app_name
37
+ client.app.info(app_name)['git_url']
38
+ end
39
+
40
+ private
41
+ def create_app name
42
+ logger.info "Creating #{name}"
43
+ client.app.create({ name: name })
44
+ end
45
+
46
+ def copy_config from, to
47
+ logger.info "Copying configs from #{from} to #{to}"
48
+ from_congig_vars = config_vars(from)
49
+ # WE SHOULD NOT MOVE THE HEROKU_POSTGRES_*_URL...
50
+ from_congig_vars.reject! { |k, v| k.start_with?('HEROKU_POSTGRESQL_') && k.end_with?('_URL') }
51
+ client.config_var.update(to, from_congig_vars)
52
+ end
53
+
54
+ def copy_add_ons from, to
55
+ logger.info "Copying addons from #{from} to #{to}"
56
+ from_addons = client.addon.list(from)
57
+ from_addons.each do |addon|
58
+ name = addon['plan']['name']
59
+ begin
60
+ logger.info "Adding #{name} to #{to}"
61
+ client.addon.create(to, { plan: name })
62
+ rescue Excon::Errors::UnprocessableEntity => e
63
+ logger.error "Failed to copy addon #{name}"
64
+ logger.error e
65
+ end
66
+ end
67
+ end
68
+
69
+ def copy_pg from, to
70
+ logger.info "Copying Postgres's data from #{from} to #{to}"
71
+ backup = Fourchette::Pgbackups.new
72
+ backup.copy(from, to)
73
+ end
74
+ end
@@ -0,0 +1,12 @@
1
+ require 'logger'
2
+
3
+ module Fourchette::Logger
4
+ def logger
5
+ unless @logger
6
+ @logger = Logger.new(STDOUT)
7
+ @logger.level = Logger::INFO
8
+ end
9
+
10
+ @logger
11
+ end
12
+ end
@@ -0,0 +1,39 @@
1
+ require "heroku/client/pgbackups"
2
+ class Fourchette::Pgbackups
3
+ include Fourchette::Logger
4
+
5
+ def initialize
6
+ @heroku = Fourchette::Heroku.new
7
+ end
8
+
9
+ def copy from, to
10
+ ensure_pgbackups_is_present(from)
11
+ ensure_pgbackups_is_present(to)
12
+
13
+ from_url, from_name = pg_details_for(from)
14
+ to_url, to_name = pg_details_for(to)
15
+
16
+ @client = Heroku::Client::Pgbackups.new pgbackup_url(from)+'/api'
17
+ @client.create_transfer(from_url, from_name, to_url, to_name)
18
+ end
19
+
20
+ private
21
+ def ensure_pgbackups_is_present heroku_app_name
22
+ unless @heroku.client.addon.list(heroku_app_name).select { |addon| addon['name'] == 'pgbackups' }.any?
23
+ logger.info "Adding pgbackups to #{heroku_app_name}"
24
+ @heroku.client.addon.create(heroku_app_name, { plan: 'pgbackups' })
25
+ end
26
+ end
27
+
28
+ def pg_details_for app_name
29
+ @heroku.config_vars(app_name).each do |key, value|
30
+ return [value, key] if key.start_with?('HEROKU_POSTGRESQL_') && key.end_with?('_URL')
31
+ end
32
+ end
33
+
34
+ def pgbackup_url app_name
35
+ @heroku.config_vars(app_name).each do |k, v|
36
+ return v if k == 'PGBACKUPS_URL'
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,31 @@
1
+ class Fourchette::PullRequest
2
+ include SuckerPunch::Job
3
+
4
+ def perform params
5
+ callbacks = Fourchette::Callbacks.new(params)
6
+ @params = params
7
+
8
+ callbacks.before
9
+
10
+ case action
11
+ when 'synchronize' # new push against the PR
12
+ fork.update
13
+ when 'closed'
14
+ fork.delete
15
+ when 'reopened'
16
+ fork.create
17
+ when 'opened'
18
+ fork.create
19
+ end
20
+
21
+ callbacks.after
22
+ end
23
+
24
+ def action
25
+ @params['action']
26
+ end
27
+
28
+ def fork
29
+ @fork ||= Fourchette::Fork.new(@params)
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ require 'fourchette'
2
+
3
+ namespace :fourchette do
4
+ desc 'This enables Fourchette hook'
5
+ task :enable do
6
+ Fourchette::GitHub.new.enable_hook
7
+ end
8
+
9
+ desc 'This disables Fourchette hook'
10
+ task :disable do
11
+ Fourchette::GitHub.new.disable_hook
12
+ end
13
+
14
+ desc 'This updates the Fourchette hook with the current URL of the app'
15
+ task :update do
16
+ Fourchette::GitHub.new.update_hook
17
+ end
18
+
19
+ desc 'This deletes the Fourchette hook'
20
+ task :delete do
21
+ Fourchette::GitHub.new.delete_hook
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
1
  module Fourchette
2
- VERSION = "0.0.0"
3
- end
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1 @@
1
+ require_relative 'web/hooks'
@@ -0,0 +1,5 @@
1
+ post '/hooks' do
2
+ params = JSON.parse(request.env["rack.input"].read)
3
+ Fourchette::PullRequest.new.async.perform(params)
4
+ "Got it, thanks!"
5
+ end
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+
3
+ describe Fourchette::Heroku do
4
+ let(:heroku) { Fourchette::Heroku.new }
5
+ let(:from_app_name) { 'awesome app' }
6
+ let(:to_app_name) { 'awesomer app!' }
7
+
8
+ before do
9
+ client = double('client')
10
+ client_app = double('client')
11
+ app_list = [ { 'name' => 'fourchette-pr-7' }, { 'name' => 'fourchette-pr-8' } ]
12
+ client_app.stub(:list).and_return(app_list)
13
+ client.stub(:app).and_return(client_app)
14
+
15
+ config_var = double('config_var')
16
+ client.stub(:config_var).and_return(config_var)
17
+
18
+ client.app.stub(:info).and_return( { 'git_url' => 'git@heroku.com/something.git' } )
19
+
20
+ heroku.stub(:client).and_return(client)
21
+ end
22
+
23
+ describe '#app_exists?' do
24
+ it { expect(heroku.app_exists?('fourchette-pr-7')).to eq true }
25
+ it { expect(heroku.app_exists?('fourchette-pr-8')).to eq true }
26
+ it { expect(heroku.app_exists?('fourchette-pr-333')).to eq false }
27
+ end
28
+
29
+ describe '#fork' do
30
+ before do
31
+ heroku.stub(:create_app)
32
+ heroku.stub(:copy_config)
33
+ heroku.stub(:copy_add_ons)
34
+ heroku.stub(:copy_pg)
35
+ end
36
+
37
+ ['create_app', 'copy_config', 'copy_add_ons', 'copy_pg'].each do |method_name|
38
+ it "calls `#{method_name}'" do
39
+ heroku.should_receive(method_name)
40
+ heroku.fork(from_app_name, to_app_name)
41
+ end
42
+ end
43
+ end
44
+
45
+ describe '#git_url' do
46
+ it { expect(heroku.git_url(to_app_name)).to eq 'git@heroku.com/something.git' }
47
+ end
48
+
49
+ describe '#delete' do
50
+ it 'calls delete on the Heroku client' do
51
+ heroku.client.app.should_receive(:delete).with(to_app_name)
52
+ heroku.delete(to_app_name)
53
+ end
54
+ end
55
+
56
+ describe '#config_vars' do
57
+ it 'calls config_var.info on the Heroku client' do
58
+ heroku.client.config_var.should_receive(:info).with(from_app_name)
59
+ heroku.config_vars(from_app_name)
60
+ end
61
+ end
62
+
63
+ describe 'private functions' do
64
+ describe '#create_app' do
65
+ it 'calls app.create on the Heroku client' do
66
+ heroku.client.app.should_receive(:create).with({ name: to_app_name })
67
+ heroku.send(:create_app, to_app_name)
68
+ end
69
+ end
70
+
71
+ describe '#copy_config' do
72
+ let(:vars) { { 'WHATEVER' => 'ok', 'HEROKU_POSTGRESQL_SOMETHING_URL' => 'FAIL@POSTGRES/DB' } }
73
+ let(:cleaned_vars) { { 'WHATEVER' => 'ok'} }
74
+
75
+ it 'calls #config_vars' do
76
+ heroku.client.config_var.stub(:update)
77
+ heroku.should_receive(:config_vars).with(from_app_name).and_return(vars)
78
+ heroku.send(:copy_config, from_app_name, to_app_name)
79
+ end
80
+
81
+ it 'updates config vars without postgres URLs' do
82
+ heroku.client.config_var.should_receive(:update).with(to_app_name, cleaned_vars )
83
+ heroku.stub(:config_vars).and_return(vars)
84
+ heroku.send(:copy_config, 'from', to_app_name)
85
+ end
86
+ end
87
+
88
+ describe '#copy_add_ons' do
89
+ let(:addon_list) { [ { 'plan' => { 'name' => 'redistogo' } } ] }
90
+
91
+ before do
92
+ heroku.client.stub(:addon).and_return( double('addon') )
93
+ heroku.client.addon.stub(:create)
94
+ heroku.client.addon.stub(:list).and_return(addon_list)
95
+ end
96
+
97
+ it 'gets the addon list' do
98
+ heroku.client.addon.should_receive(:list).with(from_app_name).and_return(addon_list)
99
+ heroku.send(:copy_add_ons, from_app_name, to_app_name)
100
+ end
101
+
102
+ it 'creates addons' do
103
+ heroku.client.addon.should_receive(:create).with(to_app_name, { plan: 'redistogo' })
104
+ heroku.send(:copy_add_ons, from_app_name, to_app_name)
105
+ end
106
+ end
107
+
108
+ describe '#copy_pg' do
109
+ it 'calls Fourchette::Pgbackups#copy' do
110
+ Fourchette::Pgbackups.any_instance.should_receive(:copy).with(from_app_name, to_app_name)
111
+ heroku.send(:copy_pg, from_app_name, to_app_name)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'coveralls'
3
+ Coveralls.wear!
4
+
5
+ require_relative '../lib/fourchette'
6
+
7
+ support_include_path = "#{Dir.pwd}/spec/support/**/*.rb"
8
+ Dir[support_include_path].each {|f| require f}
@@ -0,0 +1,4 @@
1
+ Logger.class_eval do
2
+ def info(*args)
3
+ end
4
+ end
metadata CHANGED
@@ -1,59 +1,234 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fourchette
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean-Philippe Boily
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-01-30 00:00:00.000000000 Z
11
+ date: 2014-04-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ~>
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.3'
20
- type: :development
19
+ version: '1.5'
20
+ type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ~>
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.3'
26
+ version: '1.5'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - '>='
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sinatra
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 1.4.4
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 1.4.4
55
+ - !ruby/object:Gem::Dependency
56
+ name: sinatra-contrib
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.4.2
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.4.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: octokit
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 3.0.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 3.0.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: git
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 1.2.6
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 1.2.6
97
+ - !ruby/object:Gem::Dependency
98
+ name: heroics
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '='
102
+ - !ruby/object:Gem::Version
103
+ version: 0.0.2
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '='
109
+ - !ruby/object:Gem::Version
110
+ version: 0.0.2
111
+ - !ruby/object:Gem::Dependency
112
+ name: heroku
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 3.3.0
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 3.3.0
125
+ - !ruby/object:Gem::Dependency
126
+ name: sucker_punch
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 1.0.2
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 1.0.2
139
+ - !ruby/object:Gem::Dependency
140
+ name: foreman
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 0.63.0
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 0.63.0
153
+ - !ruby/object:Gem::Dependency
154
+ name: pry-debugger
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rspec
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 2.14.1
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 2.14.1
181
+ - !ruby/object:Gem::Dependency
182
+ name: coveralls
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
32
186
  - !ruby/object:Gem::Version
33
187
  version: '0'
34
188
  type: :development
35
189
  prerelease: false
36
190
  version_requirements: !ruby/object:Gem::Requirement
37
191
  requirements:
38
- - - '>='
192
+ - - ">="
39
193
  - !ruby/object:Gem::Version
40
194
  version: '0'
41
195
  description: Fourchette is your new best friend for having isolated testing environements.
196
+ It will help you test your GitHub PRs against a fork of one your Heroku apps. You
197
+ will have one Heroku app per PR now. Isn't that amazing? It will make testing way
198
+ easier and you won't have the (maybe) broken code from other PRs on staging but
199
+ only the code that requires testing.
42
200
  email:
43
201
  - j@jipi.ca
44
202
  executables: []
45
203
  extensions: []
46
204
  extra_rdoc_files: []
47
205
  files:
48
- - .gitignore
206
+ - ".gitignore"
207
+ - ".travis.yml"
49
208
  - Gemfile
209
+ - Gemfile.lock
50
210
  - LICENSE.txt
211
+ - Procfile
51
212
  - README.md
52
213
  - Rakefile
214
+ - config.ru
53
215
  - fourchette.gemspec
54
216
  - lib/fourchette.rb
217
+ - lib/fourchette/callbacks.rb
218
+ - lib/fourchette/fork.rb
219
+ - lib/fourchette/github.rb
220
+ - lib/fourchette/heroku.rb
221
+ - lib/fourchette/logger.rb
222
+ - lib/fourchette/pgbackups.rb
223
+ - lib/fourchette/pull_request.rb
224
+ - lib/fourchette/rake_tasks.rb
55
225
  - lib/fourchette/version.rb
56
- homepage: ''
226
+ - lib/fourchette/web.rb
227
+ - lib/fourchette/web/hooks.rb
228
+ - spec/lib/fourchette/heroku_spec.rb
229
+ - spec/spec_helper.rb
230
+ - spec/support/silent-logger.rb
231
+ homepage: https://github.com/jipiboily/fourchette
57
232
  licenses:
58
233
  - MIT
59
234
  metadata: {}
@@ -63,18 +238,21 @@ require_paths:
63
238
  - lib
64
239
  required_ruby_version: !ruby/object:Gem::Requirement
65
240
  requirements:
66
- - - '>='
241
+ - - ">="
67
242
  - !ruby/object:Gem::Version
68
243
  version: '0'
69
244
  required_rubygems_version: !ruby/object:Gem::Requirement
70
245
  requirements:
71
- - - '>='
246
+ - - ">="
72
247
  - !ruby/object:Gem::Version
73
248
  version: '0'
74
249
  requirements: []
75
250
  rubyforge_project:
76
- rubygems_version: 2.0.2
251
+ rubygems_version: 2.2.0
77
252
  signing_key:
78
253
  specification_version: 4
79
- summary: Fourchette is your new best friend for having isolated testing environements.
80
- test_files: []
254
+ summary: Your new best friend for isolated testing environments on Heroku.
255
+ test_files:
256
+ - spec/lib/fourchette/heroku_spec.rb
257
+ - spec/spec_helper.rb
258
+ - spec/support/silent-logger.rb