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 +4 -4
- data/.gitignore +8 -0
- data/.travis.yml +6 -0
- data/Gemfile +2 -3
- data/Procfile +1 -0
- data/README.md +102 -16
- data/Rakefile +16 -1
- data/config.ru +3 -0
- data/fourchette.gemspec +18 -6
- data/lib/fourchette.rb +37 -1
- data/lib/fourchette/callbacks.rb +15 -0
- data/lib/fourchette/fork.rb +108 -0
- data/lib/fourchette/github.rb +101 -0
- data/lib/fourchette/heroku.rb +74 -0
- data/lib/fourchette/logger.rb +12 -0
- data/lib/fourchette/pgbackups.rb +39 -0
- data/lib/fourchette/pull_request.rb +31 -0
- data/lib/fourchette/rake_tasks.rb +23 -0
- data/lib/fourchette/version.rb +2 -2
- data/lib/fourchette/web.rb +1 -0
- data/lib/fourchette/web/hooks.rb +5 -0
- data/spec/lib/fourchette/heroku_spec.rb +115 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/silent-logger.rb +4 -0
- metadata +194 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a81c926b6fccb25d10111bbd826e6f12ac1b4925
|
4
|
+
data.tar.gz: b99d9faac8fc580772d1c5e45517343b9ae07427
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 486ac1c0d082e82aab0483acb3acfa34abbbc339c916962fa0bb38c1ef8a01299a9ece417bf3b526bf4f52d9abd06b780415f002e60fd83da721fb39f314f965
|
7
|
+
data.tar.gz: e833fb94778d18cf6621d9e96524be64779119bb554796d7a50c3531dac281ae889f9c6b7e3da5dbf4632c5203039758d91c9a8f7facfc89a49a1b907483ae3d
|
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/Procfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
web: bundle exec rackup -s puma -p $PORT
|
data/README.md
CHANGED
@@ -1,29 +1,115 @@
|
|
1
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
93
|
+
## Contribute
|
12
94
|
|
13
|
-
|
95
|
+
- fork & clone
|
96
|
+
- `bundle install`
|
97
|
+
- `foreman start`
|
98
|
+
- You now have the app running on port 9292
|
14
99
|
|
15
|
-
|
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
|
-
|
102
|
+
## It needs some love...
|
18
103
|
|
19
|
-
|
104
|
+
What needs to be improved?
|
20
105
|
|
21
|
-
|
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
|
-
|
113
|
+
# Contributors
|
24
114
|
|
25
|
-
|
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 "
|
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
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.
|
12
|
-
spec.
|
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.
|
22
|
-
spec.
|
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,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,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
|
data/lib/fourchette/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module Fourchette
|
2
|
-
VERSION = "0.0.
|
3
|
-
end
|
2
|
+
VERSION = "0.0.1"
|
3
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'web/hooks'
|
@@ -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
|
data/spec/spec_helper.rb
ADDED
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.
|
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
|
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.
|
20
|
-
type: :
|
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.
|
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
|
-
|
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
|
251
|
+
rubygems_version: 2.2.0
|
77
252
|
signing_key:
|
78
253
|
specification_version: 4
|
79
|
-
summary:
|
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
|