github_bot 0.1.0
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 +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +32 -0
- data/.github/ISSUE_TEMPLATE/config.yml +6 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +30 -0
- data/.github/pull_request_template.md +32 -0
- data/.github/workflows/cd.yml +53 -0
- data/.github/workflows/ci.yml +39 -0
- data/.gitignore +23 -0
- data/.rspec +4 -0
- data/.rubocop.yml +927 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +2 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +40 -0
- data/CONTRIBUTORS.md +3 -0
- data/Gemfile +22 -0
- data/LICENSE +205 -0
- data/NOTICE +13 -0
- data/README.md +66 -0
- data/Rakefile +8 -0
- data/app/controllers/github_bot/application_controller.rb +40 -0
- data/app/controllers/github_bot/concerns/response.rb +25 -0
- data/app/controllers/github_bot/webhooks/github_controller.rb +36 -0
- data/app/helpers/github_bot/github_request_helper.rb +123 -0
- data/bin/bundle-audit +29 -0
- data/bin/bundler-audit +29 -0
- data/bin/rails +26 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/config/routes.rb +13 -0
- data/github_bot.gemspec +36 -0
- data/lib/github_bot.rb +10 -0
- data/lib/github_bot/engine.rb +9 -0
- data/lib/github_bot/github/check_run.rb +56 -0
- data/lib/github_bot/github/client.rb +203 -0
- data/lib/github_bot/github/payload.rb +225 -0
- data/lib/github_bot/rails/railtie.rb +15 -0
- data/lib/github_bot/tasks/base.rb +42 -0
- data/lib/github_bot/validator/base.rb +232 -0
- data/lib/github_bot/version.rb +11 -0
- metadata +154 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require_relative 'concerns/response'
|
5
|
+
|
6
|
+
module GithubBot
|
7
|
+
# Public: Base controller for handing rails routes into the GithubBot
|
8
|
+
class ApplicationController < ActionController::API
|
9
|
+
include Response
|
10
|
+
include GithubRequestHelper
|
11
|
+
|
12
|
+
before_action :valid_request?
|
13
|
+
before_action :event_processing
|
14
|
+
|
15
|
+
# Public: Initial the request for event processing
|
16
|
+
def event_processing
|
17
|
+
Github::Client.initialize(request)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Public: Returns <true> if the incoming request if properly authorized
|
21
|
+
def valid_request?
|
22
|
+
signature = github_signature
|
23
|
+
my_signature = 'sha1=' + OpenSSL::HMAC.hexdigest(
|
24
|
+
OpenSSL::Digest.new('sha1'),
|
25
|
+
ENV['GITHUB_WEBHOOK_SECRET'],
|
26
|
+
github_payload_raw
|
27
|
+
)
|
28
|
+
|
29
|
+
json_response(json_access_denied, :unauthorized) unless Rack::Utils.secure_compare(my_signature, signature)
|
30
|
+
rescue StandardError => e
|
31
|
+
msg = "#{self.class}##{__method__} An error occurred while determine if request is valid"
|
32
|
+
Rails.logger.error(
|
33
|
+
message: msg,
|
34
|
+
exception: e
|
35
|
+
)
|
36
|
+
|
37
|
+
json_response(json_access_denied(errors: { message: "#{msg}, exception: #{e.message}" }), :unauthorized)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubBot
|
4
|
+
# Public: A helper to the controllers for assisting in providing json responses
|
5
|
+
module Response
|
6
|
+
# Public: Renders a json response
|
7
|
+
def json_response(object, status = :ok)
|
8
|
+
render json: object, status: status
|
9
|
+
end
|
10
|
+
|
11
|
+
# Public: Returns a json response indicating a 404
|
12
|
+
def json_not_found(params)
|
13
|
+
{
|
14
|
+
errors: {
|
15
|
+
message: "Not found with parameter #{params}"
|
16
|
+
}
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Public: Returns a json response indicating a 403
|
21
|
+
def json_access_denied(**args)
|
22
|
+
{ errors: 'access denied' }.merge(args)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../application_controller'
|
4
|
+
|
5
|
+
module GithubBot
|
6
|
+
module Webhooks
|
7
|
+
# Public: The GitHub controller for handling all incoming requests and determining
|
8
|
+
# what validator that should be utilized.
|
9
|
+
class GithubController < ApplicationController
|
10
|
+
# POST /webhooks
|
11
|
+
def validate
|
12
|
+
return unless pull_request? || check_run?
|
13
|
+
|
14
|
+
client_api = ::GithubBot::Github::Client.instance
|
15
|
+
|
16
|
+
client_api.repository_pull_request_bots.each do |bot_class|
|
17
|
+
clazz =
|
18
|
+
begin
|
19
|
+
bot_class.constantize
|
20
|
+
rescue StandardError => e
|
21
|
+
Rails.logger.error(
|
22
|
+
message: "#{self.class}##{__method__} Pull request validator '#{bot_class}' is not defined",
|
23
|
+
exception: e
|
24
|
+
)
|
25
|
+
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
clazz&.validate
|
30
|
+
end
|
31
|
+
rescue StandardError => e
|
32
|
+
Rails.logger.error message: 'GitHub Bot failure', exception: e
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubBot
|
4
|
+
# Public: A request helper for understanding the incoming request context
|
5
|
+
module GithubRequestHelper
|
6
|
+
# Public: Returns the GitHub event type of the incoming request
|
7
|
+
def github_event
|
8
|
+
request.env['HTTP_X_GITHUB_EVENT']
|
9
|
+
end
|
10
|
+
|
11
|
+
# Public: Returns if the GitHub signature of the incoming request
|
12
|
+
def github_signature
|
13
|
+
request.env['HTTP_X_HUB_SIGNATURE'] || 'no-signature'
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public: Returns the raw content of the github payload's body content
|
17
|
+
def github_payload_raw
|
18
|
+
@github_payload_raw ||= request.body.read
|
19
|
+
end
|
20
|
+
|
21
|
+
# Public: Returns the <Json> representation of the github payload
|
22
|
+
def github_payload
|
23
|
+
return @github_payload if @github_payload
|
24
|
+
|
25
|
+
begin
|
26
|
+
@github_payload = JSON.parse(github_payload_raw).with_indifferent_access
|
27
|
+
rescue StandardError => e
|
28
|
+
raise StandardError, "Invalid JSON (#{e}): #{@github_payload_raw}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Public: Returns <true> if the event type is of type 'ping'; otherwise, <false>
|
33
|
+
def ping?
|
34
|
+
github_event == 'ping'
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Return <true> if the event type is of type 'pull_request'; otherwise, <false>
|
38
|
+
def pull_request?
|
39
|
+
github_event == 'pull_request'
|
40
|
+
end
|
41
|
+
|
42
|
+
# Public: Return <true> if the event type is of type 'pull_request_review'; otherwise, <false>
|
43
|
+
def pull_request_review?
|
44
|
+
github_event == 'pull_request_review'
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public: Return <true> if the event type is of type 'check_run'; otherwise, <false>
|
48
|
+
def check_run?
|
49
|
+
github_event == 'check_run'
|
50
|
+
end
|
51
|
+
|
52
|
+
# Public: Return <true> if the action type is of type 'review_requested'; otherwise, <false>
|
53
|
+
def review_requested?
|
54
|
+
github_event == 'review_requested'
|
55
|
+
end
|
56
|
+
|
57
|
+
# Public: Return <true> if the action type is of type 'review_request_removed'; otherwise, <false>
|
58
|
+
def review_request_removed?
|
59
|
+
github_event == 'review_request_removed'
|
60
|
+
end
|
61
|
+
|
62
|
+
# Public: Return <true> if the action type is of type 'labeled'; otherwise, <false>
|
63
|
+
def labeled?
|
64
|
+
github_event == 'labeled'
|
65
|
+
end
|
66
|
+
|
67
|
+
# Public: Return <true> if the action type is of type 'issue_comment'; otherwise, <false>
|
68
|
+
def issue_comment?
|
69
|
+
github_event == 'issue_comment'
|
70
|
+
end
|
71
|
+
|
72
|
+
# Public: Returns <String> of the comment's body from the github payload
|
73
|
+
def comment_body
|
74
|
+
github_payload['comment']['body']
|
75
|
+
end
|
76
|
+
|
77
|
+
# Public: Return <true> if the action type is of type 'issue_comment'
|
78
|
+
# and the comment message contains "run_validation"; otherwise, <false>
|
79
|
+
def issue_comment_recheck?
|
80
|
+
github_event == 'issue_comment' && comment_body.include?('run_validation')
|
81
|
+
end
|
82
|
+
|
83
|
+
# Public: Return <true> if issue_comment_recheck? is true
|
84
|
+
# and the comment message contains option; otherwise, <false>
|
85
|
+
#
|
86
|
+
# @param [String] option represents which validation to run
|
87
|
+
def recheck_application?(option)
|
88
|
+
return false unless issue_comment_recheck?
|
89
|
+
|
90
|
+
options = recheck_options(comment_body)
|
91
|
+
options.include?(option) || options.include?('all')
|
92
|
+
end
|
93
|
+
|
94
|
+
# Public: Return <true> if the action type is of type 'unlabeled'; otherwise, <false>
|
95
|
+
def unlabeled?
|
96
|
+
github_event == 'unlabeled'
|
97
|
+
end
|
98
|
+
|
99
|
+
# Public: Returns the pull request action type from the payload
|
100
|
+
def pull_request_action
|
101
|
+
github_payload['action']
|
102
|
+
end
|
103
|
+
|
104
|
+
# Public: Returns the pull request content from the payload
|
105
|
+
def pull_request
|
106
|
+
github_payload['pull_request']
|
107
|
+
end
|
108
|
+
|
109
|
+
# Public: Returns the repository payload
|
110
|
+
def repository
|
111
|
+
github_payload['repository']
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
# Private: Return an array of strings
|
117
|
+
#
|
118
|
+
# @param str input like "run_validation: a, b, c" with return of ["a","b","c"]
|
119
|
+
def recheck_options(str)
|
120
|
+
str.gsub(/\s+/, '').partition(':').last.split(',')
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/bin/bundle-audit
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'bundle-audit' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("bundler-audit", "bundle-audit")
|
data/bin/bundler-audit
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'bundler-audit' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("bundler-audit", "bundler-audit")
|
data/bin/rails
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# This command will automatically be run when you run "rails" with Rails gems
|
5
|
+
# installed from the root of your application.
|
6
|
+
|
7
|
+
ENGINE_ROOT = File.expand_path('..', __dir__)
|
8
|
+
ENGINE_PATH = File.expand_path('../lib/github_bot/engine', __dir__)
|
9
|
+
|
10
|
+
# Set up gems listed in the Gemfile.
|
11
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
12
|
+
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
|
13
|
+
|
14
|
+
require 'rails'
|
15
|
+
# Pick the frameworks you want:
|
16
|
+
require 'active_model/railtie'
|
17
|
+
require 'active_job/railtie'
|
18
|
+
require 'active_record/railtie'
|
19
|
+
require 'active_storage/engine'
|
20
|
+
require 'action_controller/railtie'
|
21
|
+
require 'action_mailer/railtie'
|
22
|
+
require 'action_view/railtie'
|
23
|
+
require 'action_cable/engine'
|
24
|
+
require 'sprockets/railtie'
|
25
|
+
# require "rails/test_unit/railtie"
|
26
|
+
require 'rails/engine/commands'
|
data/bin/rake
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rake' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rake", "rake")
|
data/bin/rspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rspec' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rspec-core", "rspec")
|
data/bin/rubocop
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rubocop' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rubocop", "rubocop")
|
data/config/routes.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
GithubBot::Engine.routes.draw do
|
4
|
+
namespace :webhooks do
|
5
|
+
post '/', action: :validate, controller: 'github', defaults: { format: :json }
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
# Adding this to mount the above routes into the application level in order to be able to use
|
10
|
+
# the path helpers without having to access them with the main_app.
|
11
|
+
Rails.application.routes.draw do
|
12
|
+
mount GithubBot::Engine => '/' if GithubBot.draw_routes_in_host_app
|
13
|
+
end
|
data/github_bot.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'github_bot/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'github_bot'
|
9
|
+
spec.version = GithubBot::VERSION
|
10
|
+
spec.authors = ['Greg Howdeshell']
|
11
|
+
spec.email = ['greg.howdeshell@gmail.com']
|
12
|
+
|
13
|
+
spec.summary = 'A rubygem designed to assist in the creation of GitHub bot applications.'
|
14
|
+
spec.description = <<~DESC
|
15
|
+
A rubygem designed to assist in the creation of GitHub bot applications.
|
16
|
+
DESC
|
17
|
+
spec.homepage = 'https://github.com/cerner/github_bot-ruby'
|
18
|
+
spec.license = 'Apache-2.0'
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
+
spec.files =
|
23
|
+
Dir.chdir(File.expand_path(__dir__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(/^(test|spec|features)\//) }
|
25
|
+
end
|
26
|
+
spec.bindir = 'bin'
|
27
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ['lib']
|
29
|
+
|
30
|
+
spec.required_ruby_version = '>= 2.6.2'
|
31
|
+
|
32
|
+
spec.add_dependency 'git', '~> 1.0'
|
33
|
+
spec.add_dependency 'jwt', '~> 1.0'
|
34
|
+
spec.add_dependency 'octokit', '~> 4.18'
|
35
|
+
spec.add_dependency 'rails', '>= 5.0.0.1', '< 7.0.0'
|
36
|
+
end
|