Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Event handling #35

Merged
merged 7 commits into from
Jul 30, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2019-07-20 16:07:34 +0200 using RuboCop version 0.73.0.
# on 2019-07-30 20:31:00 +0200 using RuboCop version 0.73.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
Expand Down Expand Up @@ -118,7 +118,7 @@ Layout/Tab:
Exclude:
- 'config/initializers/omniauth.rb'

# Offense count: 6
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: final_newline, final_blank_line
Expand All @@ -129,7 +129,6 @@ Layout/TrailingBlankLines:
- 'app/controllers/sessions_controller.rb'
- 'app/models/user.rb'
- 'config/initializers/sidekiq.rb'
- 'config/initializers/voxpupuli.rb'

# Offense count: 2
# Cop supports --auto-correct.
Expand All @@ -140,7 +139,7 @@ Layout/TrailingWhitespace:

# Offense count: 6
Metrics/AbcSize:
Max: 111
Max: 118

# Offense count: 1
Metrics/CyclomaticComplexity:
Expand Down Expand Up @@ -253,11 +252,11 @@ Style/CommentAnnotation:
Exclude:
- 'app/workers/repo_status_worker.rb'

# Offense count: 18
# Offense count: 23
Style/Documentation:
Enabled: false

# Offense count: 59
# Offense count: 64
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: always, never
Expand Down Expand Up @@ -333,15 +332,15 @@ Style/TrailingCommaInArguments:
Exclude:
- 'config/initializers/semantic_logger.rb'

# Offense count: 3
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinSize, WordRegex.
# SupportedStyles: percent, brackets
Style/WordArray:
Exclude:
- 'config/initializers/voxpupuli.rb'

# Offense count: 58
# Offense count: 69
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Expand Down
7 changes: 6 additions & 1 deletion app/controllers/incoming_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ class IncomingController < ApplicationController
skip_before_action :verify_authenticity_token
skip_before_action :logged_in

##
# Parse the whole payload from Github and hand it over to the GithubEvent handler
def github
# parse the JSON payload, that we get as string, to a hash
useable_body = JSON.parse(request.body.read).to_h

# For debug purposes we currently log the requets into Sentry
# TODO: Remove this line since the GithubEvent handler reports unknown events.
Raven.capture_message('Received hook', extra: useable_body)

GithubEvent.new(useable_body, request.headers['X-GitHub-Event'])
end
end
19 changes: 19 additions & 0 deletions app/lib/github_event.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class GithubEvent
attr_reader :processor
delegate :process, to: :processor

##
# The GithubEvent handler checks which type of event we are dealing with.
#
# If it is a known event, create a specific handler and let it take care of
# the next steps. If not kick off a Sentry error.

def initialize(payload, type)
case type
when 'pull_request'
@processor = GithubEvent::PullRequest.new(payload)
else
Raven.capture_message('Unknown Hook Received', extra: useable_body)
Flipez marked this conversation as resolved.
Show resolved Hide resolved
end
end
end
13 changes: 13 additions & 0 deletions app/lib/github_event/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class GithubEvent
class Base
attr_reader :payload

##
# Receive the payload and start processing it

def initialize(payload)
@payload = payload
process
end
end
end
13 changes: 13 additions & 0 deletions app/lib/github_event/pull_request.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class GithubEvent
class PullRequest < GithubEvent::Base
attr_reader :gh_pull_request

##
# Currently we only care about syncing our database with GitHub as our validation
# will take action if necessary.

def process
::PullRequest.update_with_github(payload['pull_request'])
end
end
end
5 changes: 5 additions & 0 deletions app/models/label.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
class Label < ApplicationRecord
has_and_belongs_to_many :pull_requests

# TODO: Find out how to manage predefined labels
def self.needs_rebase
find_or_create_by(name: 'needs-rebase', color: '207de5')
end
end
87 changes: 86 additions & 1 deletion app/models/pull_request.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,89 @@
class PullRequest < ApplicationRecord
belongs_to :repository
# It is easier overall to use the GitHub ID for relation management.
# It allows us to maintain, update or the Repository or PullRequest without
# the counterpart.
belongs_to :repository, primary_key: :github_id, foreign_key: :gh_repository_id, inverse_of: :pull_requests

has_and_belongs_to_many :labels
after_save :queue_validation

##
# This method is used for updating our database with the current state of the PullRequest
# in GitHub. Therefore it's used with the payload of the webhook content processed by
# GithubEvent handler and by the content from the api fetched by the periodic check.

def self.update_with_github(gh_pull_request)
PullRequest.where(github_id: gh_pull_request['id']).first_or_initialize.tap do |pull_request|
pull_request.number = gh_pull_request['number']
pull_request.state = gh_pull_request['state']
pull_request.title = gh_pull_request['title']
pull_request.body = gh_pull_request['body']
pull_request.gh_created_at = gh_pull_request['created_at']
pull_request.gh_updated_at = gh_pull_request['updated_at']
pull_request.gh_repository_id = gh_pull_request['base']['repo']['id']
pull_request.closed_at = gh_pull_request['closed_at']
pull_request.merged_at = gh_pull_request['merged_at']
pull_request.mergeable = gh_pull_request['mergeable']
pull_request.save

gh_pull_request['labels'].each do |label|
pull_request.labels << Label.find_or_create_by(name: label['name'], color: label['color'])
end
end
end

##
# Ensure that the Label is attached to the PullRequest
#
# Therefore ensure that the Label exists for the corresponding repository
#
# Then ensure the Label is attached to this PullRequest by checking the list
# of attached Labels. Unfortunately this seems to be the only option
#
# If the list does not include the given Label we attach it

def ensure_label_is_attached(label)
repository.ensure_label_exists(label)

attached_labels = Github.client.labels_for_issue(gh_repository_id, number)
return if attached_labels.any? { |attached_label| attached_label['name'] == label.name }

Github.client.add_labels_to_an_issue(gh_repository_id, number, [label.name])
end

##
# We simply remove the given Label if it exists

def ensure_label_is_detached(label)
Github.client.remove_label(gh_repository_id, number, label.name)
end

##
# if the PullRequest is mergeable we need to check if the 'needs-rebase' Label
# is attached. If so, we need to remove it.
#
# If the PullRequest is not yet mergeable we need to attach the 'needs-rebase'
# Label. Therefore we also need to check if the Label exists on repository level.
#
# The saved_changes variable includes all the stuff that has changed.
# We currently don't care about them

def validate(_saved_changes)
if mergeable
ensure_label_is_detached(Label.needs_rebase)
else
repository.ensure_label_exists(Label.needs_rebase)
ensure_label_is_attached(label)
end
end

private

##
# Since we want to be a fancy responsive application we to all the validation
# stuff which might result in some new querys and api requests asyncronously.

def queue_validation
ValidatePullRequestWorker.perform_async(id, saved_changes) if saved_changes?
end
end
43 changes: 26 additions & 17 deletions app/models/repository.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
class Repository < ApplicationRecord
has_many :pull_requests
has_many :open_pull_requests, -> { where(state: 'open') }, class_name: 'PullRequest'
# It is easier overall to use the GitHub ID for relation management.
# It allows us to maintain, update or the Repository or PullRequest without
# the counterpart.
has_many :pull_requests, primary_key: :github_id, foreign_key: :gh_repository_id, inverse_of: :repository
has_many :open_pull_requests, -> { where(state: 'open') }, class_name: 'PullRequest', primary_key: :github_id, foreign_key: :gh_repository_id

def actions_needed
@action_needed ||= begin
Expand All @@ -19,25 +22,31 @@ def github_url
'https://github.com/' + full_name
end

##
# Check if the Label exists in the Repository
# If we get a 404 create the Label.

def ensure_label_exists(label)
GitHub.client.label(github_id, label.name)
rescue Octokit::NotFound
Github.client.add_label(github_id, label.name, label.color)
end

##
# Delete the given Label from the Repository
#
def ensure_label_missing(label)
GitHub.client.delete_label!(github_id, label.name)
end

##
# Fetch all open and closed PullRequests and sync our database with them
#
def update_pull_requests
open_pull_requests = Github.client.pull_requests("voxpupuli/#{name}")
closed_pull_requests = Github.client.pull_requests("voxpupuli/#{name}", state: :closed)
(open_pull_requests + closed_pull_requests).each do |gh_pull_request|
PullRequest.where(repository_id: id, number: gh_pull_request.number).first_or_initialize.tap do |pull_request|
pull_request.number = gh_pull_request.number
pull_request.state = gh_pull_request.state
pull_request.title = gh_pull_request.title
pull_request.body = gh_pull_request.body
pull_request.gh_created_at = gh_pull_request.created_at
pull_request.gh_updated_at = gh_pull_request.updated_at
pull_request.closed_at = gh_pull_request.closed_at
pull_request.merged_at = gh_pull_request.merged_at
pull_request.save

gh_pull_request.labels.each do |label|
pull_request.labels << Label.find_or_create_by(name: label.name, color: label.color)
end
end
PullRequest.update_with_github(gh_pull_request)
end

pull_requests.count
Expand Down
12 changes: 12 additions & 0 deletions app/workers/validate_pull_request_worker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class ValidatePullRequestWorker
include Sidekiq::Worker

##
# As validation may take a bit more time we do it async.
# This worker is not meant to hold any logic but only trigger the
# validation in its asyncronous context.

def perform(id, saved_changes)
PullRequest.find(id).validate(saved_changes)
end
end
2 changes: 1 addition & 1 deletion config/initializers/array.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Array
def all_i
self.map(&:to_i)
map(&:to_i)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AddMergeableAndGhRepoIdToPullRequests < ActiveRecord::Migration[5.2]
def change
add_column :pull_requests, :mergeable, :boolean
add_column :pull_requests, :gh_repository_id, :integer
add_column :pull_requests, :github_id, :integer
end
end
5 changes: 4 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2019_06_20_115024) do
ActiveRecord::Schema.define(version: 2019_07_30_094710) do

create_table "labels", force: :cascade do |t|
t.string "name"
Expand Down Expand Up @@ -38,6 +38,9 @@
t.integer "repository_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "mergeable"
t.integer "gh_repository_id"
t.integer "github_id"
t.index ["repository_id"], name: "index_pull_requests_on_repository_id"
end

Expand Down