github-web-hooks-receiver 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: de99b9c01f899326fd8b4cf3422244ab1afbe6bc
4
+ data.tar.gz: 0638ab1bc10af285fce9e117074df11959e02975
5
+ SHA512:
6
+ metadata.gz: 9e1ef74e2f4d9d411c7d69e4e9cb87b56c5396db505534b7cd3f51b85e31778255deaea5a6a6dadd90d0d4437b84c6214980b6557371067ffa7b5cea8938d94b
7
+ data.tar.gz: fd4b1e4779938c54b116d4d7a1ccddbf77699c0f4ec97bf61747578dced26904a7622394fa12be39d2373010e8dddb3d27f23451409d362c0d52b9bfc9f08147
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ /.bundle/
2
+ /Gemfile.lock
3
+ /vendor/
4
+ /test/.test-result/
5
+
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ rvm:
2
+ - 2.0.0
3
+ - 2.1
4
+ - 2.2
5
+ notifications:
6
+ email:
7
+ recipients:
8
+ - commit@clear-code.com
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,212 @@
1
+ [![Build Status](https://travis-ci.org/clear-code/github-web-hooks-receiver.svg?branch=master)](https://travis-ci.org/clear-code/github-web-hooks-receiver)
2
+
3
+ # GitHub Web hooks receiver
4
+
5
+ This is a Rack based web application that can process POST request from GitHub, GitLab and GHE.
6
+
7
+ ## Set up
8
+
9
+ Prepare following files.
10
+
11
+ /home/github-web-hooks-receiver/github-web-hooks-receiver/Gemfile:
12
+ ```
13
+ source "https://rubygems.org"
14
+ gem "github-web-hooks-receiver"
15
+ # gem "unicorn" # Enable this line if you use Unicorn.
16
+ # gem "passenger" # Enable this line if you use latest Passenger.
17
+ ```
18
+
19
+ /home/github-web-hooks-receiver/github-web-hooks-receiver/config.ru:
20
+ ```ruby
21
+ require "yaml"
22
+ require "pathname"
23
+ require "github-web-hooks-receiver"
24
+
25
+ use Rack::CommonLogger
26
+ use Rack::Runtime
27
+ use Rack::ContentLength
28
+
29
+ base_dir = Pathname(__FILE__).dirname
30
+ config_file = base_dir + "config.yaml"
31
+
32
+ options = YAML.load_file(config_file.to_s)
33
+
34
+ map "/post-receiver/" do
35
+ run GitHubWebHooksReceiver::App.new(options)
36
+ end
37
+ ```
38
+
39
+ /home/github-web-hooks-receiver/github-web-hooks-receiver/config.yaml:
40
+ ```
41
+ to: receiver@example.com
42
+ sender: sender@example.com
43
+ add_html: true
44
+ owners:
45
+ groonga:
46
+ to: groonga-commit@lists.sourceforge.jp
47
+ ```
48
+
49
+ ### Apache + Passenger
50
+
51
+ On Debian GNU/Linux wheezy.
52
+
53
+ See also [Phusion Passenger users guide, Apache version](https://www.phusionpassenger.com/documentation/Users%20guide%20Apache.html).
54
+
55
+ Install Passenger or write `gem "passenger"` in your Gemfile.
56
+
57
+ ```
58
+ $ sudo apt-get install -y ruby-passenger
59
+ ```
60
+
61
+ Install gems.
62
+
63
+ ```
64
+ $ sudo -u github-web-hooks-receiver -H bundle install --path vendor/bundle
65
+ ```
66
+
67
+ Prepare following files.
68
+
69
+ /etc/apache2/mods-available.conf:
70
+ ```
71
+ PassengerRoot /path/to/passenger-x.x.x
72
+ PassengerRuby /path/to/ruby
73
+
74
+ PassengerMaxRequests 100
75
+ ```
76
+
77
+ /etc/apache2/mods-available.load:
78
+ ```
79
+ LoadModule passenger_module /path/to/mod_passenger.so
80
+ ```
81
+
82
+ /etc/apache2/sites-available/github-web-hooks-receiver:
83
+ ```
84
+ <VirtualHost *:80>
85
+ ServerName github-web-hooks-receiver.example.com
86
+ DocumentRoot /home/github-web-hooks-receiver/github-web-hooks-receiver/public
87
+ <Directory /home/github-web-hooks-receiver/github-web-hooks-receiver/public>
88
+ AllowOverride all
89
+ Options -MultiViews
90
+ </Directory>
91
+
92
+ ErrorLog ${APACHE_LOG_DIR}/github-web-hooks-receiver_error.log
93
+ CustomLog ${APACHE_LOG_DIR}/github-web-hooks-receiver_access.log combined
94
+
95
+ AllowEncodedSlashes On
96
+ AcceptPathInfo On
97
+ </VirtualHost>
98
+ ```
99
+
100
+ Enable the module.
101
+
102
+ ```
103
+ $ sudo a2enmod passenger
104
+ ```
105
+
106
+ Enable the virtual host.
107
+
108
+ ```
109
+ $ sudo a2ensite github-web-hooks-receiver
110
+ ```
111
+
112
+ Restart web server.
113
+
114
+ ```
115
+ $ sudo service apache2 restart
116
+ ```
117
+
118
+ ### Nginx + Unicorn
119
+
120
+ Prepare following files.
121
+
122
+ /etc/nginx/sites-enabled/github-web-hooks-receiver:
123
+ ```
124
+ upstream github-web-hooks-receiver {
125
+ server unix:/tmp/unicorn-github-web-hooks-receiver.sock;
126
+ }
127
+
128
+ server {
129
+ listen 80;
130
+ server_name github-web-hooks-receiver.example.com;
131
+ access_log /var/log/nginx/github-web-hooks-receiver.example.com-access.log combined;
132
+
133
+ root /srv/www/github-web-hooks-receiver;
134
+ index index.html;
135
+
136
+ proxy_set_header X-Real-IP $remote_addr;
137
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
138
+ proxy_set_header Host $http_host;
139
+ #proxy_redirect off;
140
+
141
+ location / {
142
+ root /home/github-web-hooks-receiver/github-web-hooks-receiver/public;
143
+ include maintenance;
144
+ if (-f $request_filename){
145
+ break;
146
+ }
147
+ if (!-f $request_filename){
148
+ proxy_pass http://github-web-hooks-receiver;
149
+ break;
150
+ }
151
+ }
152
+ }
153
+ ```
154
+
155
+ /home/github-web-hooks-receiver/github-web-hooks-receiver/unicorn.conf:
156
+ ```
157
+ # -*- ruby -*-
158
+ worker_processes 2
159
+ working_directory "/home/github-web-hooks-receiver/github-web-hooks-receiver"
160
+ listen '/tmp/unicorn-github-post-receiver.sock', :backlog => 1
161
+ timeout 120
162
+ pid 'tmp/pids/unicorn.pid'
163
+ preload_app true
164
+ stderr_path 'log/unicorn.log'
165
+ stdout_path "log/stdout.log"
166
+ user "github-web-hooks-receiver", "github-web-hooks-receiver"
167
+ ```
168
+
169
+ /home/github-web-hooks-receiver/bin/github-web-hooks-receiver:
170
+ ```
171
+ #! /bin/zsh
172
+ BASE_DIR=/home/github-web-hooks-receiver/github-web-hooks-receiver
173
+ export RACK_ENV=production
174
+ cd $BASE_DIR
175
+ rbenv version
176
+
177
+ command=$1
178
+
179
+ function start() {
180
+ mkdir -p $BASE_DIR/tmp/pids
181
+ mkdir -p $BASE_DIR/log
182
+ bundle exec unicorn -D -c unicorn.conf config.ru
183
+ }
184
+
185
+ function stop() {
186
+ kill $(cat $BASE_DIR/tmp/pids/unicorn.pid)
187
+ }
188
+
189
+ function restart() {
190
+ kill -USR2 $(cat $BASE_DIR/tmp/pids/unicorn.pid)
191
+ }
192
+
193
+ $command
194
+ ```
195
+
196
+ Install gems.
197
+
198
+ ```
199
+ $ sudo -u github-web-hooks-receiver -H bundle install --path vendor/bundle
200
+ ```
201
+
202
+ Run the application.
203
+
204
+ ```
205
+ $ sudo -u github-web-hooks-receiver -H ~github-web-hooks-receiver/bin/github-web-hooks-receiver start
206
+ ```
207
+
208
+ ## Configuration
209
+
210
+ You need to edit config.yaml to configure this web application.
211
+ See config.yaml.example and test codes.
212
+
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ # Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+
16
+ require "bundler/gem_tasks"
17
+
18
+ task :default => :test
19
+
20
+ desc "Run test"
21
+ task :test do
22
+ ruby("test/run-test.rb")
23
+ end
data/config.ru ADDED
@@ -0,0 +1,52 @@
1
+ # -*- mode: ruby; coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2010-2013 Kouhei Sutou <kou@clear-code.com>
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ require "yaml"
19
+
20
+ require "pathname"
21
+
22
+ base_dir = Pathname(__FILE__).dirname
23
+ lib_dir = base_dir + "lib"
24
+
25
+ racknga_base_dir = base_dir.parent.parent + "racknga"
26
+ racknga_lib_dir = racknga_base_dir + "lib"
27
+
28
+ $LOAD_PATH.unshift(racknga_lib_dir.to_s)
29
+ $LOAD_PATH.unshift(lib_dir.to_s)
30
+
31
+ require "github-web-hooks-receiver"
32
+
33
+ require "racknga/middleware/exception_notifier"
34
+
35
+ use Rack::CommonLogger
36
+ use Rack::Runtime
37
+ use Rack::ContentLength
38
+
39
+ config_file = base_dir + "config.yaml"
40
+ options = YAML.load_file(config_file.to_s)
41
+ notifier_options = options.dup
42
+ if options[:error_to]
43
+ notifier_options[:to] = options[:error_to]
44
+ end
45
+ notifier_options.merge!(options["exception_notifier"] || {})
46
+ notifiers = [Racknga::ExceptionMailNotifier.new(notifier_options)]
47
+ use Racknga::Middleware::ExceptionNotifier, :notifiers => notifiers
48
+
49
+ map "/post-receiver/" do
50
+ run GitHubWebHooksReceiver::App.new(options)
51
+ end
52
+
@@ -0,0 +1,20 @@
1
+ private_token: VERYSECRETTOKEN
2
+ gitlab_api_end_point: https://gitlab.example.com/api/v3
3
+ web_hooks:
4
+ - http://gh.example.com/post-receiver1
5
+ - http://gh.example.com/post-receiver2
6
+ - http://gh.example.com/post-receiver3
7
+ to: receiver@example.com
8
+ error_to: admin@example.com
9
+ exception_notifier:
10
+ subject_label: "[git-utils]"
11
+ sender: sender@example.com
12
+ add_html: true
13
+ owners:
14
+ ranguba:
15
+ to: groonga-commit@rubyforge.org
16
+ repositories:
17
+ examples:
18
+ to: null@example.com
19
+ groonga:
20
+ to: groonga-commit@lists.sourceforge.jp
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'github-web-hooks-receiver/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "github-web-hooks-receiver"
8
+ spec.version = GitHubWebHooksReceiver::VERSION
9
+ spec.authors = ["Kouhei Sutou", "Kenji Okimoto"]
10
+ spec.email = ["kou@clear-code.com", "okimoto@clear-code.com"]
11
+ spec.summary = %q{GitHub web hook receiver}
12
+ spec.description = %q{GitHub web hook receiver}
13
+ spec.homepage = ""
14
+ spec.license = "GPL-3.0+"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency "rack"
22
+
23
+ spec.add_development_dependency "bundler"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "test-unit"
26
+ spec.add_development_dependency "test-unit-rr"
27
+ spec.add_development_dependency "test-unit-capybara"
28
+ end
@@ -0,0 +1,29 @@
1
+ # Copyright (C) 2010-2013 Kouhei Sutou <kou@clear-code.com>
2
+ # Copyright (C) 2015 Kenji Okimoto <okimoto@clear-code.com>
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ require "fileutils"
18
+ require "webrick/httpstatus"
19
+ require "shellwords"
20
+ require "uri"
21
+
22
+ require "rubygems"
23
+ require "json"
24
+
25
+ module GitHubWebHooksReceiver
26
+ end
27
+
28
+ require "github-web-hooks-receiver/app"
29
+ require "github-web-hooks-receiver/version"
@@ -0,0 +1,239 @@
1
+ # Copyright (C) 2010-2013 Kouhei Sutou <kou@clear-code.com>
2
+ # Copyright (C) 2015 Kenji Okimoto <okimoto@clear-code.com>
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ require "github-web-hooks-receiver/base"
18
+ require "github-web-hooks-receiver/path-resolver"
19
+ require "github-web-hooks-receiver/payload"
20
+ require "github-web-hooks-receiver/repository"
21
+
22
+ module GitHubWebHooksReceiver
23
+ class App < Base
24
+ include PathResolver
25
+
26
+ private
27
+
28
+ def process_payload(request, response, raw_payload)
29
+ metadata = {
30
+ "x-github-event" => github_event(request),
31
+ }
32
+ payload = Payload.new(raw_payload, metadata)
33
+ case payload.event_name
34
+ when "ping"
35
+ # Do nothing
36
+ when "push", nil # nil is for GitLab
37
+ process_push_payload(request, response, payload)
38
+ when "gollum"
39
+ process_gollum_payload(request, response, payload)
40
+ else
41
+ set_error_response(response,
42
+ :bad_request,
43
+ "Unsupported event: <#{payload.event_name}>")
44
+ end
45
+ end
46
+
47
+ def github_event(request)
48
+ request.env["HTTP_X_GITHUB_EVENT"]
49
+ end
50
+
51
+ def process_push_payload(request, response, payload)
52
+ repository = process_payload_repository(request, response, payload)
53
+ return if repository.nil?
54
+ change = process_push_parameters(request, response, payload)
55
+ return if change.nil?
56
+ repository.process(*change)
57
+ end
58
+
59
+ def process_gollum_payload(request, response, payload)
60
+ repository = process_payload_repository(request, response, payload)
61
+ return if repository.nil?
62
+ change = process_gollum_parameters(request, response, payload)
63
+ return if change.nil?
64
+ repository.process(*change)
65
+ end
66
+
67
+ def process_payload_repository(request, response, payload)
68
+ repository = payload["repository"]
69
+ if repository.nil?
70
+ set_error_response(response, :bad_request,
71
+ "repository information is missing")
72
+ return
73
+ end
74
+
75
+ unless repository.is_a?(Hash)
76
+ set_error_response(response, :bad_request,
77
+ "invalid repository information format: " +
78
+ "<#{repository.inspect}>")
79
+ return
80
+ end
81
+
82
+ repository_uri = repository["url"]
83
+ domain = extract_domain(repository_uri)
84
+ if domain.nil?
85
+ set_error_response(response, :bad_request,
86
+ "invalid repository URI: <#{repository.inspect}>")
87
+ return
88
+ end
89
+
90
+ repository_name = repository["name"]
91
+ if repository_name.nil?
92
+ set_error_response(response, :bad_request,
93
+ "repository name is missing: <#{repository.inspect}>")
94
+ return
95
+ end
96
+
97
+ owner_name = extract_owner_name(repository_uri, payload)
98
+ if owner_name.nil?
99
+ set_error_response(response, :bad_request,
100
+ "repository owner or owner name is missing: " +
101
+ "<#{repository.inspect}>")
102
+ return
103
+ end
104
+
105
+ options = repository_options(domain, owner_name, repository_name)
106
+ repository = repository_class.new(domain, owner_name, repository_name,
107
+ payload, options)
108
+ unless repository.target?
109
+ set_error_response(response, :forbidden,
110
+ "unacceptable repository: " +
111
+ "<#{owner_name.inspect}>:<#{repository_name.inspect}>")
112
+ return
113
+ end
114
+
115
+ repository
116
+ end
117
+
118
+ def extract_domain(repository_uri)
119
+ domain = nil
120
+ case repository_uri
121
+ when /\Agit@/
122
+ domain = repository_uri[/@(.+):/, 1]
123
+ when /\Ahttps:\/\//
124
+ domain = URI.parse(repository_uri).hostname
125
+ else
126
+ return
127
+ end
128
+ domain
129
+ end
130
+
131
+ def extract_owner_name(repository_uri, payload)
132
+ owner_name = nil
133
+ repository = payload["repository"]
134
+ if payload.gitlab?
135
+ case repository_uri
136
+ when /\Agit@/
137
+ owner_name = repository_uri[%r!git@.+:(.+)/.+(?:.git)?!, 1]
138
+ when /\Ahttps:\/\//
139
+ owner_name = URI.parse(repository_uri).path.sub(/\A\//, "")
140
+ else
141
+ return
142
+ end
143
+ else
144
+ owner = repository["owner"]
145
+ return if owner.nil?
146
+
147
+ owner_name = owner["name"] || owner["login"]
148
+ return if owner_name.nil?
149
+ end
150
+ owner_name
151
+ end
152
+
153
+ def process_push_parameters(request, response, payload)
154
+ before = payload["before"]
155
+ if before.nil?
156
+ set_error_response(response, :bad_request,
157
+ "before commit ID is missing")
158
+ return
159
+ end
160
+
161
+ after = payload["after"]
162
+ if after.nil?
163
+ set_error_response(response, :bad_request,
164
+ "after commit ID is missing")
165
+ return
166
+ end
167
+
168
+ reference = payload["ref"]
169
+ if reference.nil?
170
+ set_error_response(response, :bad_request,
171
+ "reference is missing")
172
+ return
173
+ end
174
+
175
+ [before, after, reference]
176
+ end
177
+
178
+ def process_gollum_parameters(request, response, payload)
179
+ pages = payload["pages"]
180
+ if pages.nil?
181
+ set_error_response(response, :bad_request,
182
+ "pages are missing")
183
+ return
184
+ end
185
+ if pages.empty?
186
+ set_error_response(response, :bad_request,
187
+ "no pages")
188
+ end
189
+
190
+ revisions = pages.collect do |page|
191
+ page["sha"]
192
+ end
193
+
194
+ if revisions.size == 1
195
+ after = revisions.first
196
+ before = "#{after}^"
197
+ else
198
+ before = revisions.first
199
+ after = revisions.last
200
+ end
201
+
202
+ reference = "refs/heads/master"
203
+ [before, after, reference]
204
+ end
205
+
206
+ def set_error_response(response, status_keyword, message)
207
+ response.status = status(status_keyword)
208
+ response["Content-Type"] = "text/plain"
209
+ response.write(message)
210
+ end
211
+
212
+ def repository_class
213
+ @options[:repository_class] || Repository
214
+ end
215
+
216
+ def repository_options(domain, owner_name, repository_name)
217
+ domain_options = (@options[:domains] || {})[domain] || {}
218
+ domain_options = symbolize_options(domain_options)
219
+ domain_owner_options = (domain_options[:owners] || {})[owner_name] || {}
220
+ domain_owner_options = symbolize_options(domain_owner_options)
221
+ domain_repository_options = (domain_owner_options[:repositories] || {})[repository_name] || {}
222
+ domain_repository_options = symbolize_options(domain_repository_options)
223
+
224
+ owner_options = (@options[:owners] || {})[owner_name] || {}
225
+ owner_options = symbolize_options(owner_options)
226
+ _repository_options = (owner_options[:repositories] || {})[repository_name] || {}
227
+ _repository_options = symbolize_options(_repository_options)
228
+
229
+ options = @options.merge(owner_options)
230
+ options = options.merge(owner_options)
231
+ options = options.merge(_repository_options)
232
+
233
+ options = options.merge(domain_options)
234
+ options = options.merge(domain_owner_options)
235
+ options = options.merge(domain_repository_options)
236
+ options
237
+ end
238
+ end
239
+ end