Skip to content
This repository was archived by the owner on Jul 19, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,28 @@ Event-specific attributes:
}
```

### Pull Request

Event name: `pull_request`

Event-specific attributes:

```javascript
{
"state": String, // "pending", or "success"
"github_slug": String, // user/repo
"number": String,
"commit_sha": String,
}
```

## Other Events

The following are not fully implemented yet.

* :issue
* :unit
* :snapshot
* :pull\_request

## License

Expand Down
2 changes: 1 addition & 1 deletion lib/cc/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def self.load_services

attr_reader :event, :config, :payload

ALL_EVENTS = %w[test unit coverage quality vulnerability snapshot]
ALL_EVENTS = %w[test unit coverage quality vulnerability snapshot pull_request]

# Tracks the defined services.
def self.services
Expand Down
103 changes: 103 additions & 0 deletions lib/cc/services/github_pull_requests.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
class CC::Service::GitHubPullRequests < CC::Service
class Config < CC::Service::Config
attribute :oauth_token, String,
label: "OAuth Token",
description: "A personal OAuth token with permissions for the repo"
attribute :update_status, Boolean,
label: "Update status?",
description: "Update the pull request status after analyzing?"
attribute :add_comment, Boolean,
label: "Add a comment?",
description: "Comment on the pull request after analyzing?"

validates :oauth_token, presence: true
end

self.title = "GitHub Pull Requests"
self.description = "Update pull requests on on GitHub"

BASE_URL = "https://api.github.com"
BODY_REGEX = %r{<b>Code Climate</b> has <a href=".*">analyzed this pull request</a>}
COMMENT_BODY = '<img src="https://codeclimate.com/favicon.png" width="20" height="20" />&nbsp;<b>Code Climate</b> has <a href="%s">analyzed this pull request</a>.'

# Just make sure we can access GH using the configured token. Without
# additional information (github-slug, PR number, etc) we can't test much
# else.
def receive_test
setup_http

http_get("#{BASE_URL}")

nil
end

def receive_pull_request
setup_http

case @payload["state"]
when "pending"
update_status("pending", "Code Climate is analyzing this code.")
when "success"
add_comment
update_status("success", "Code Climate has analyzed this pull request.")
end
end

private

def update_status(state, description)
if config.update_status
body = {
state: state,
description: description,
target_url: @payload["details_url"],
}.to_json

http_post(status_url, body)
end
end

def add_comment
if config.add_comment && !comment_present?
body = {
body: COMMENT_BODY % @payload["compare_url"]
}.to_json

http_post(comments_url, body)
end
end

def comment_present?
response = http_get(comments_url)
comments = JSON.parse(response.body)

comments.any? { |comment| comment["body"] =~ BODY_REGEX }
end

def setup_http
http.headers["Content-Type"] = "application/json"
http.headers["Authorization"] = "token #{config.oauth_token}"
http.headers["User-Agent"] = "Code Climate"
end

def status_url
"#{BASE_URL}/repos/#{github_slug}/statuses/#{commit_sha}"
end

def comments_url
"#{BASE_URL}/repos/#{github_slug}/issues/#{number}/comments"
end

def github_slug
@payload.fetch("github_slug")
end

def commit_sha
@payload.fetch("commit_sha")
end

def number
@payload.fetch("number")
end

end
38 changes: 38 additions & 0 deletions pull_request_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env ruby
#
# Ad-hoc script for updating a pull request using our service.
#
# Usage:
#
# $ OAUTH_TOKEN="..." bundle exec ruby pull_request_test.rb
#
###
require 'cc/services'
CC::Service.load_services

class WithResponseLogging
def initialize(invocation)
@invocation = invocation
end

def call
@invocation.call.tap { |r| p r }
end
end

service = CC::Service::GitHubPullRequests.new({
oauth_token: ENV.fetch("OAUTH_TOKEN"),
update_status: true,
add_comment: true,
}, {
name: "pull_request",
# https://github.com/codeclimate/nillson/pull/33
state: "success",
github_slug: "codeclimate/nillson",
number: 33,
commit_sha: "986ec903b8420f4e8c8d696d8950f7bd0667ff0c"
})

CC::Service::Invocation.new(service) do |i|
i.wrap(WithResponseLogging)
end
95 changes: 95 additions & 0 deletions test/github_pull_requests_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
require File.expand_path('../helper', __FILE__)

class TestGitHubPullRequests < CC::Service::TestCase
def test_pull_request_status_pending
expect_status_update("pbrisbin/foo", "abc123", {
"state" => "pending",
"description" => /is analyzing/,
})

receive_pull_request({ update_status: true }, {
github_slug: "pbrisbin/foo",
commit_sha: "abc123",
state: "pending",
})
end

def test_pull_request_status_success
expect_status_update("pbrisbin/foo", "abc123", {
"state" => "success",
"description" => /has analyzed/,
})

receive_pull_request({ update_status: true }, {
github_slug: "pbrisbin/foo",
commit_sha: "abc123",
state: "success",
})
end

def test_pull_request_comment
stub_existing_comments("pbrisbin/foo", 1, %w[Hey Yo])

expect_comment("pbrisbin/foo", 1, %r{href="http://example.com">analyzed})

receive_pull_request({ add_comment: true }, {
github_slug: "pbrisbin/foo",
number: 1,
state: "success",
compare_url: "http://example.com",
})
end

def test_pull_request_comment_already_present
stub_existing_comments("pbrisbin/foo", 1, [
'<b>Code Climate</b> has <a href="">analyzed this pull request</a>'
])

# With no POST expectation, test will fail if request is made.

receive_pull_request({ add_comment: true }, {
github_slug: "pbrisbin/foo",
number: 1,
state: "success",
})
end

private

def expect_status_update(repo, commit_sha, params)
@stubs.post "repos/#{repo}/statuses/#{commit_sha}" do |env|
assert_equal "token 123", env[:request_headers]["Authorization"]

body = JSON.parse(env[:body])

params.each do |k, v|
assert v === body[k],
"Unexpected value for #{k}. #{v.inspect} !== #{body[k].inspect}"
end
end
end

def stub_existing_comments(repo, number, bodies)
body = bodies.map { |b| { body: b } }.to_json

@stubs.get("repos/#{repo}/issues/#{number}/comments") { [200, {}, body] }
end

def expect_comment(repo, number, content)
@stubs.post "repos/#{repo}/issues/#{number}/comments" do |env|
body = JSON.parse(env[:body])
assert_equal "token 123", env[:request_headers]["Authorization"]
assert content === body["body"],
"Unexpected comment body. #{content.inspect} !== #{body["body"].inspect}"
end
end

def receive_pull_request(config, event_data)
receive(
CC::Service::GitHubPullRequests,
{ oauth_token: "123" }.merge(config),
{ name: "pull_request" }.merge(event_data)
)
end

end