site_hook 0.6.8 → 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab4199b8ddec4e89c17d5dd8dee0e5f76b0808952c48c8207c889741755a3350
4
- data.tar.gz: 8fa41c7c5bc360fb097ee12d7525e0b1a1df98ca3b51b12b7cc4b2fc57cdd664
3
+ metadata.gz: c99430b340e0a3ac2c311d2e7688289fb9252598ec0bdc752566f41394503161
4
+ data.tar.gz: a258fbcacc3b2a918b807530c64c0efab06adde90e910ade3915c33d297c97c0
5
5
  SHA512:
6
- metadata.gz: 72548825721c132e94a4b24831bc888e0fc8cfc5f818a27d5041da7a5ef5e33dea1538bd0972890cca9ef9d5332573934a47f816531b86e63f94ef6308e06e73
7
- data.tar.gz: a9cc0f7acf41daf93cf6ad3feb368cf8de76c27f73479365b811afe6e5ae1b3d62773f97f099cf25fdcce738dfe2db5032d34116c0cdca70ca95fcc45493395b
6
+ metadata.gz: b62d2d23daeca75c2d03a63d0bd11b3f55a3206e1355f4946d2c22470fbe1b4f273760732484e135565db78085aa4d579b40dabd266ec59ecc8980f61f64e0a6
7
+ data.tar.gz: 8365b6eebe10b49633b4e070ebb491f6d2fea1b94774c2127b000a58130271227415229eb459bf9fc648c16c565b20a70a23a87b2e0e08cc320ea539b3249714
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- site_hook (0.6.7)
4
+ site_hook (0.6.8)
5
5
  activesupport (~> 5.1)
6
6
  git (~> 1.3)
7
7
  haml (~> 5.0)
@@ -2,7 +2,10 @@
2
2
 
3
3
  require 'site_hook/version'
4
4
  require 'site_hook/sender'
5
+ require 'site_hook/gem'
6
+ require 'site_hook/log'
5
7
  require 'site_hook/logger'
8
+ require 'site_hook/spinner'
6
9
  require 'recursive-open-struct'
7
10
  require 'site_hook/cli'
8
11
  require 'sinatra'
@@ -11,35 +14,12 @@ require 'sass'
11
14
  require 'json'
12
15
  require 'sinatra/json'
13
16
  require 'yaml'
17
+
14
18
  module SiteHook
19
+ autoload :Logs, 'site_hook/log'
20
+ autoload :Gem, 'site_hook/gem'
21
+ autoload :Paths, 'site_hook/paths'
15
22
  # rubocop:disable Metrics/ClassLength, Metrics/LineLength, MethodLength, BlockLength
16
- module Gem
17
- # class Info
18
- class Info
19
- def self.name
20
- 'site_hook'
21
- end
22
-
23
- def self.constant_name
24
- 'SiteHook'
25
- end
26
-
27
- def self.author
28
- 'Ken Spencer <me@iotaspencer.me>'
29
- end
30
- end
31
-
32
- # Paths: Paths to gem resources and things
33
- class Paths
34
- def self.config
35
- Pathname(Dir.home).join('.jph', 'config').to_s
36
- end
37
-
38
- def self.logs
39
- Pathname(Dir.home).join('.jph', 'logs')
40
- end
41
- end
42
- end
43
23
  # class SassHandler (inherits from Sinatra::Base)
44
24
  class SassHandler < Sinatra::Base
45
25
  set :views, Pathname(app_file).dirname.join('site_hook', 'static', 'sass').to_s
@@ -57,160 +37,6 @@ module SiteHook
57
37
  end
58
38
  end
59
39
  # class Webhook (inherits from Sinatra::Base)
60
- class Webhook < Sinatra::Base
61
- HOOKLOG = SiteHook::HookLogger::HookLog.new(SiteHook.log_levels['hook']).log
62
- BUILDLOG = SiteHook::HookLogger::BuildLog.new(SiteHook.log_levels['build']).log
63
- APPLOG = SiteHook::HookLogger::AppLog.new(SiteHook.log_levels['app']).log
64
- JPHRC = YAML.load_file(Pathname(Dir.home).join('.jph', 'config'))
65
- set port: JPHRC.fetch('port', 9090)
66
- set bind: '127.0.0.1'
67
- set server: %w[thin]
68
- set quiet: true
69
- set raise_errors: true
70
- set views: Pathname(app_file).dirname.join('site_hook', 'views')
71
- set :public_folder, Pathname(app_file).dirname.join('site_hook', 'static')
72
- use SassHandler
73
- use CoffeeHandler
74
-
75
- #
76
- # @param [String] body JSON String of body
77
- # @param [String] sig Signature or token from git service
78
- # @param [String] secret User-defined verification token
79
- # @param [Boolean] plaintext Whether the verification is plaintext
80
- def self.verified?(body, sig, secret, plaintext:, service:)
81
- if plaintext
82
- sig == secret
83
- else
84
- case service
85
- when 'gogs'
86
- if sig == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, body)
87
- APPLOG.debug "Secret verified: #{sig} === #{OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, body)}"
88
- true
89
- end
90
- when 'github'
91
- if sig == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, body)
92
- APPLOG.debug "Secret verified: #{sig} === #{OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, body)}"
93
- true
94
- end
95
- end
96
-
97
- end
98
- end
99
40
 
100
- get '/' do
101
- halt 403, { 'Content-Type' => 'text/html' }, '<h1>See <a href="/webhooks/">here</a> for the active webhooks</h1>'
102
- end
103
-
104
- get '/webhooks.json', provides: :json do
105
- content_type 'application/json'
106
- public_projects = JPHRC['projects'].select do |_project, hsh|
107
- (hsh.fetch('private', nil) == false) || hsh.fetch('private', nil).nil?
108
- end
109
- result = {}
110
- public_projects.each do |project, hsh|
111
- result[project] = {}
112
- hsh.delete('hookpass')
113
- result[project].merge!(hsh)
114
- end
115
- headers 'Content-Type' => 'application/json', 'Accept' => 'application/json'
116
- json result, layout: false
117
- end
118
-
119
- get '/webhooks/?' do
120
- haml :webhooks, locals: { 'projects' => JPHRC['projects'] }
121
- end
122
-
123
- get '/webhook/*' do
124
- if params[:splat]
125
- pass
126
- else
127
- halt 405, { 'Content-Type' => 'application/json' }, { message: 'GET not allowed' }.to_json
128
- end
129
- end
130
- post '/webhook/:hook_name/?' do
131
- service = nil
132
- request.body.rewind
133
- req_body = request.body.read
134
- js = RecursiveOpenStruct.new(JSON.parse(req_body))
135
-
136
- projects = JPHRC['projects']
137
- begin
138
- project = projects.fetch(params[:hook_name])
139
- rescue KeyError => e
140
- halt 404, { 'Content-Type' => 'application/json' }, { message: 'no such project', status: 1 }.to_json
141
- end
142
- plaintext = false
143
- signature = nil
144
- event = nil
145
- github = request.env.fetch('HTTP_X_GITHUB_EVENT', nil)
146
- unless github.nil?
147
- event = 'push' if github == 'push'
148
- end
149
- gitlab = request.env.fetch('HTTP_X_GITLAB_EVENT', nil)
150
- unless gitlab.nil?
151
- event = 'push' if gitlab == 'push'
152
- end
153
- gogs = request.env.fetch('HTTP_X_GOGS_EVENT', nil)
154
- unless gogs.nil?
155
- event = 'push' if gogs == 'push'
156
- end
157
- events = { 'github' => github, 'gitlab' => gitlab, 'gogs' => gogs }
158
- events_m_e = events.values.one?
159
- case events_m_e
160
- when true
161
- event = 'push'
162
- service = events.select { |_key, value| value }.keys.first
163
- when false
164
- halt 400, { 'Content-Type': 'application/json' }, { message: 'events are mutually exclusive', status: 'failure' }.to_json
165
-
166
- else
167
- halt 400,
168
- { 'Content-Type': 'application/json' },
169
- 'status': 'failure', 'message': 'something weird happened'
170
- end
171
- if event != 'push'
172
- if event.nil?
173
- halt 400, { 'Content-Type': 'application/json' }, { message: 'no event header' }.to_json
174
- end
175
- end
176
- case service
177
- when 'gitlab'
178
- signature = request.env.fetch('HTTP_X_GITLAB_TOKEN', '')
179
- plaintext = true
180
- when 'github'
181
- signature = request.env.fetch('HTTP_X_HUB_SIGNATURE', '').sub!(/^sha1=/, '')
182
- plaintext = false
183
-
184
- when 'gogs'
185
- signature = request.env.fetch('HTTP_X_GOGS_SIGNATURE', '')
186
- plaintext = false
187
- end
188
- if Webhook.verified?(req_body.to_s, signature, project['hookpass'], plaintext: plaintext, service: service)
189
- BUILDLOG.info 'Building...'
190
-
191
- jekyllbuild = SiteHook::Senders::Jekyll.build(project['src'], project['dst'], BUILDLOG)
192
- jekyll_status = jekyllbuild.fetch(:status, 1)
193
- case jekyll_status
194
-
195
- when 0
196
- status 200
197
- headers 'Content-Type' => 'application/json'
198
- body { { 'status': 'success' }.to_json }
199
- when -1, -2, -3
200
- status 400
201
- headers 'Content-Type' => 'application/json'
202
- body do
203
- { 'status': 'exception', error: jekyll_status.fetch(:message).to_s }
204
- end
205
- end
206
-
207
- else
208
- halt 403, { 'Content-Type' => 'application/json' }, { message: 'incorrect secret', 'status': 'failure' }.to_json
209
- end
210
- end
211
- post '/webhook/?' do
212
- halt 403, { 'Content-Type' => 'application/json' }, { message: 'pick a hook', error: 'root webhook hit', 'status': 'failure' }.to_json
213
- end
214
- end
215
41
  # rubocop:enable Metrics/ClassLength, Metrics/LineLength, MethodLength, BlockLength
216
42
  end
@@ -1,26 +1,7 @@
1
1
  require 'thor'
2
-
3
2
  require 'site_hook/config_class'
3
+ require 'site_hook/server_class'
4
4
  module SiteHook
5
- def self.log_levels
6
- default = {
7
- 'hook' => 'info',
8
- 'build' => 'info',
9
- 'git' => 'info',
10
- 'app' => 'info'
11
- }
12
- begin
13
- log_level = YAML.load_file(Pathname(Dir.home).join('.jph-rc')).fetch('log_levels')
14
- if log_level
15
- log_level
16
- end
17
- rescue KeyError
18
- default
19
- rescue Errno::ENOENT
20
- default
21
- end
22
- end
23
-
24
5
  class CLI < Thor
25
6
  map %w[--version -v] => :__print_version
26
7
  desc '--version, -v', 'Print the version'
@@ -39,15 +20,11 @@ module SiteHook
39
20
  say "Gem Author: #{SiteHook::Gem::Info.author}"
40
21
  say "Gem Version: v#{SiteHook::VERSION}"
41
22
  end
42
-
43
- method_option(:log_levels, type: :hash, banner: 'LEVELS', default: SiteHook.log_levels)
44
- desc 'start', 'Start SiteHook'
45
- def start
46
-
47
- SiteHook.mklogdir unless SiteHook::Gem::Paths.logs.exist?
48
- SiteHook::Webhook.run!
49
- end
50
23
  desc 'config SUBCOMMAND [OPTIONS]', 'Configure site_hook options'
51
24
  subcommand('config', SiteHook::ConfigClass)
25
+ desc 'server SUBCOMMAND [OPTIONS]', 'Start the server'
26
+ subcommand('server', SiteHook::ServerClass)
27
+
28
+
52
29
  end
53
30
  end
@@ -33,7 +33,7 @@ module SiteHook
33
33
  ' private: true/false # hidden from the public list',
34
34
  ''
35
35
  ]
36
- jphrc = SiteHook::Gem::Paths.config
36
+ jphrc = SiteHook::Paths.config
37
37
  if jphrc.exist?
38
38
  puts "#{jphrc} exists. Will not overwrite."
39
39
  else
@@ -0,0 +1,26 @@
1
+ ##########
2
+ # -> File: /home/ken/RubymineProjects/site_hook/lib/site_hook/gem.rb
3
+ # -> Project: site_hook
4
+ # -> Author: Ken Spencer <me@iotaspencer.me>
5
+ # -> Last Modified: 1/10/2018 21:20:45
6
+ # -> Copyright (c) 2018 Ken Spencer
7
+ # -> License: MIT
8
+ ##########
9
+ module SiteHook
10
+ module Gem
11
+ # class Info
12
+ class Info
13
+ def self.name
14
+ 'site_hook'
15
+ end
16
+
17
+ def self.constant_name
18
+ 'SiteHook'
19
+ end
20
+
21
+ def self.author
22
+ 'Ken Spencer <me@iotaspencer.me>'
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module SiteHook
2
+ autoload :Paths, 'site_hook/paths'
3
+ # Logs
4
+ # Give logs related methods
5
+ module Logs
6
+ module_function
7
+ def self.log_levels
8
+ default = {
9
+ 'hook' => 'info',
10
+ 'build' => 'info',
11
+ 'git' => 'info',
12
+ 'app' => 'info'
13
+ }
14
+ begin
15
+ log_level = YAML.load_file(SiteHook::Paths.config).fetch('log_levels')
16
+ if log_level
17
+ log_level
18
+ end
19
+ rescue KeyError
20
+ default
21
+ rescue Errno::ENOENT
22
+ default
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ ##########
2
+ # -> File: /home/ken/RubymineProjects/site_hook/lib/site_hook/paths.rb
3
+ # -> Project: site_hook
4
+ # -> Author: Ken Spencer <me@iotaspencer.me>
5
+ # -> Last Modified: 1/10/2018 21:23:00
6
+ # -> Copyright (c) 2018 Ken Spencer
7
+ # -> License: MIT
8
+ ##########
9
+
10
+ module SiteHook
11
+ # Paths: Paths to gem resources and things
12
+ class Paths
13
+ def self.config
14
+ Pathname(Dir.home).join('.jph', 'config')
15
+ end
16
+
17
+ def self.logs
18
+ Pathname(Dir.home).join('.jph', 'logs')
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ ##########
2
+ # -> File: /home/ken/RubymineProjects/site_hook/lib/site_hook/config_class.1.rb
3
+ # -> Project: site_hook
4
+ # -> Author: Ken Spencer <me@iotaspencer.me>
5
+ # -> Last Modified: 1/10/2018 21:45:36
6
+ # -> Copyright (c) 2018 Ken Spencer
7
+ # -> License: MIT
8
+ ##########
9
+ require 'thor'
10
+ module SiteHook
11
+ autoload :Webhook, 'site_hook/webhook'
12
+
13
+ # *ServerClass*
14
+ #
15
+ # Holds all of the commands for the config subcommand
16
+ class ServerClass < Thor
17
+ method_option(:log_levels, type: :hash, banner: 'LEVELS', default: SiteHook::Logs.log_levels)
18
+ desc 'listen', 'Start SiteHook'
19
+ def listen
20
+ SiteHook.mklogdir unless SiteHook::Paths.logs.exist?
21
+ SiteHook::Webhook.run!
22
+ end
23
+ end
24
+ end
25
+ # rubocop:enable Metrics/AbcSize
@@ -1,3 +1,3 @@
1
1
  module SiteHook
2
- VERSION = "0.6.8"
2
+ VERSION = "0.6.9"
3
3
  end
@@ -0,0 +1,169 @@
1
+ ##########
2
+ # -> File: /home/ken/RubymineProjects/site_hook/lib/site_hook/webhook.rb
3
+ # -> Project: site_hook
4
+ # -> Author: Ken Spencer <me@iotaspencer.me>
5
+ # -> Last Modified: 1/10/2018 21:35:44
6
+ # -> Copyright (c) 2018 Ken Spencer
7
+ # -> License: MIT
8
+ ##########
9
+
10
+ require 'sinatra'
11
+
12
+ module SiteHook
13
+ class Webhook < Sinatra::Base
14
+ HOOKLOG = SiteHook::HookLogger::HookLog.new(SiteHook::Logs.log_levels['hook']).log
15
+ BUILDLOG = SiteHook::HookLogger::BuildLog.new(SiteHook::Logs.log_levels['build']).log
16
+ APPLOG = SiteHook::HookLogger::AppLog.new(SiteHook::Logs.log_levels['app']).log
17
+ JPHRC = YAML.load_file(Pathname(Dir.home).join('.jph', 'config'))
18
+
19
+ set port: JPHRC.fetch('port', 9090)
20
+ set bind: '127.0.0.1'
21
+ set server: %w[thin]
22
+ set quiet: true
23
+ set raise_errors: true
24
+ set views: Pathname(app_file).dirname.join('site_hook', 'views')
25
+ set :public_folder, Pathname(app_file).dirname.join('site_hook', 'static')
26
+ use SassHandler
27
+ use CoffeeHandler
28
+
29
+ #
30
+ # @param [String] body JSON String of body
31
+ # @param [String] sig Signature or token from git service
32
+ # @param [String] secret User-defined verification token
33
+ # @param [Boolean] plaintext Whether the verification is plaintext
34
+ def self.verified?(body, sig, secret, plaintext:, service:)
35
+ if plaintext
36
+ sig == secret
37
+ else
38
+ case service
39
+ when 'gogs'
40
+ if sig == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, body)
41
+ APPLOG.debug "Secret verified: #{sig} === #{OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, body)}"
42
+ true
43
+ end
44
+ when 'github'
45
+ if sig == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, body)
46
+ APPLOG.debug "Secret verified: #{sig} === #{OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, body)}"
47
+ true
48
+ end
49
+ end
50
+
51
+ end
52
+ end
53
+
54
+ get '/' do
55
+ halt 403, { 'Content-Type' => 'text/html' }, '<h1>See <a href="/webhooks/">here</a> for the active webhooks</h1>'
56
+ end
57
+
58
+ get '/webhooks.json', provides: :json do
59
+ content_type 'application/json'
60
+ public_projects = JPHRC['projects'].select do |_project, hsh|
61
+ (hsh.fetch('private', nil) == false) || hsh.fetch('private', nil).nil?
62
+ end
63
+ result = {}
64
+ public_projects.each do |project, hsh|
65
+ result[project] = {}
66
+ hsh.delete('hookpass')
67
+ result[project].merge!(hsh)
68
+ end
69
+ headers 'Content-Type' => 'application/json', 'Accept' => 'application/json'
70
+ json result, layout: false
71
+ end
72
+
73
+ get '/webhooks/?' do
74
+ haml :webhooks, locals: { 'projects' => JPHRC['projects'] }
75
+ end
76
+
77
+ get '/webhook/*' do
78
+ if params[:splat]
79
+ pass
80
+ else
81
+ halt 405, { 'Content-Type' => 'application/json' }, { message: 'GET not allowed' }.to_json
82
+ end
83
+ end
84
+ post '/webhook/:hook_name/?' do
85
+ service = nil
86
+ request.body.rewind
87
+ req_body = request.body.read
88
+ js = RecursiveOpenStruct.new(JSON.parse(req_body))
89
+
90
+ projects = JPHRC['projects']
91
+ begin
92
+ project = projects.fetch(params[:hook_name])
93
+ rescue KeyError => e
94
+ halt 404, { 'Content-Type' => 'application/json' }, { message: 'no such project', status: 1 }.to_json
95
+ end
96
+ plaintext = false
97
+ signature = nil
98
+ event = nil
99
+ github = request.env.fetch('HTTP_X_GITHUB_EVENT', nil)
100
+ unless github.nil?
101
+ event = 'push' if github == 'push'
102
+ end
103
+ gitlab = request.env.fetch('HTTP_X_GITLAB_EVENT', nil)
104
+ unless gitlab.nil?
105
+ event = 'push' if gitlab == 'push'
106
+ end
107
+ gogs = request.env.fetch('HTTP_X_GOGS_EVENT', nil)
108
+ unless gogs.nil?
109
+ event = 'push' if gogs == 'push'
110
+ end
111
+ events = { 'github' => github, 'gitlab' => gitlab, 'gogs' => gogs }
112
+ events_m_e = events.values.one?
113
+ case events_m_e
114
+ when true
115
+ event = 'push'
116
+ service = events.select { |_key, value| value }.keys.first
117
+ when false
118
+ halt 400, { 'Content-Type': 'application/json' }, { message: 'events are mutually exclusive', status: 'failure' }.to_json
119
+
120
+ else
121
+ halt 400,
122
+ { 'Content-Type': 'application/json' },
123
+ 'status': 'failure', 'message': 'something weird happened'
124
+ end
125
+ if event != 'push'
126
+ if event.nil?
127
+ halt 400, { 'Content-Type': 'application/json' }, { message: 'no event header' }.to_json
128
+ end
129
+ end
130
+ case service
131
+ when 'gitlab'
132
+ signature = request.env.fetch('HTTP_X_GITLAB_TOKEN', '')
133
+ plaintext = true
134
+ when 'github'
135
+ signature = request.env.fetch('HTTP_X_HUB_SIGNATURE', '').sub!(/^sha1=/, '')
136
+ plaintext = false
137
+
138
+ when 'gogs'
139
+ signature = request.env.fetch('HTTP_X_GOGS_SIGNATURE', '')
140
+ plaintext = false
141
+ end
142
+ if Webhook.verified?(req_body.to_s, signature, project['hookpass'], plaintext: plaintext, service: service)
143
+ BUILDLOG.info 'Building...'
144
+
145
+ jekyllbuild = SiteHook::Senders::Jekyll.build(project['src'], project['dst'], BUILDLOG)
146
+ jekyll_status = jekyllbuild.fetch(:status, 1)
147
+ case jekyll_status
148
+
149
+ when 0
150
+ status 200
151
+ headers 'Content-Type' => 'application/json'
152
+ body { { 'status': 'success' }.to_json }
153
+ when -1, -2, -3
154
+ status 400
155
+ headers 'Content-Type' => 'application/json'
156
+ body do
157
+ { 'status': 'exception', error: jekyll_status.fetch(:message).to_s }
158
+ end
159
+ end
160
+
161
+ else
162
+ halt 403, { 'Content-Type' => 'application/json' }, { message: 'incorrect secret', 'status': 'failure' }.to_json
163
+ end
164
+ end
165
+ post '/webhook/?' do
166
+ halt 403, { 'Content-Type' => 'application/json' }, { message: 'pick a hook', error: 'root webhook hit', 'status': 'failure' }.to_json
167
+ end
168
+ end
169
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: site_hook
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.8
4
+ version: 0.6.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken Spencer
@@ -290,13 +290,18 @@ files:
290
290
  - lib/site_hook.rb
291
291
  - lib/site_hook/cli.rb
292
292
  - lib/site_hook/config_class.rb
293
+ - lib/site_hook/gem.rb
294
+ - lib/site_hook/log.rb
293
295
  - lib/site_hook/logger.rb
296
+ - lib/site_hook/paths.rb
294
297
  - lib/site_hook/sender.rb
298
+ - lib/site_hook/server_class.rb
295
299
  - lib/site_hook/spinner.rb
296
300
  - lib/site_hook/static/sass/styles.scss
297
301
  - lib/site_hook/version.rb
298
302
  - lib/site_hook/views/layout.haml
299
303
  - lib/site_hook/views/webhooks.haml
304
+ - lib/site_hook/webhook.rb
300
305
  - site_hook.gemspec
301
306
  homepage: https://iotaspencer.me/projects/site_hook/
302
307
  licenses: