Skip to content
This repository was archived by the owner on Jul 19, 2025. It is now read-only.

Commit a372f51

Browse files
committed
Add GithubPullRequests service
- Receives pull_request event - Requires a personal OAuth token - Updates status and/or adds a comment depending on configuration
1 parent 8dc700a commit a372f51

File tree

4 files changed

+226
-1
lines changed

4 files changed

+226
-1
lines changed

lib/cc/service.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def self.load_services
3030

3131
attr_reader :event, :config, :payload
3232

33-
ALL_EVENTS = %w[test unit coverage quality vulnerability snapshot]
33+
ALL_EVENTS = %w[test unit coverage quality vulnerability snapshot pull_request]
3434

3535
# Tracks the defined services.
3636
def self.services
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
class CC::Service::GitHubPullRequests < CC::Service
2+
class Config < CC::Service::Config
3+
attribute :oauth_token, String,
4+
label: "OAuth Token",
5+
description: "A personal OAuth token with permissions for the repo"
6+
attribute :update_status, Boolean,
7+
label: "Update status?",
8+
description: "Update the pull request status after analyzing?"
9+
attribute :add_comment, Boolean,
10+
label: "Add a comment?",
11+
description: "Comment on the pull request after analyzing?"
12+
13+
validates :oauth_token, presence: true
14+
end
15+
16+
self.title = "GitHub Pull Requests"
17+
self.description = "Update pull requests on on GitHub"
18+
19+
BASE_URL = "https://api.github.com"
20+
BODY_REGEX = %r{<b>Code Climate</b> has <a href=".*">analyzed this pull request</a>}
21+
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>.'
22+
23+
def receive_pull_request
24+
setup_http
25+
26+
case @payload["state"]
27+
when "pending"
28+
update_status("pending", "Code Climate is analyzing this code.")
29+
when "success"
30+
add_comment
31+
update_status("success", "Code Climate has analyzed this pull request.")
32+
end
33+
end
34+
35+
private
36+
37+
def update_status(state, description)
38+
if config.update_status
39+
body = {
40+
state: state,
41+
description: description,
42+
target_url: @payload["details_url"],
43+
}.to_json
44+
45+
http_post(status_url, body)
46+
end
47+
end
48+
49+
def add_comment
50+
if config.add_comment && !comment_present?
51+
body = {
52+
body: COMMENT_BODY % @payload["compare_url"]
53+
}.to_json
54+
55+
http_post(comments_url, body)
56+
end
57+
end
58+
59+
def comment_present?
60+
response = http_get(comments_url)
61+
comments = JSON.parse(response.body)
62+
63+
comments.any? { |comment| comment["body"] =~ BODY_REGEX }
64+
end
65+
66+
def setup_http
67+
http.headers["Content-Type"] = "application/json"
68+
http.headers["Authorization"] = "token #{config.oauth_token}"
69+
http.headers["User-Agent"] = "Code Climate"
70+
end
71+
72+
def status_url
73+
"#{BASE_URL}/repos/#{github_slug}/statuses/#{commit_sha}"
74+
end
75+
76+
def comments_url
77+
"#{BASE_URL}/repos/#{github_slug}/issues/#{number}/comments"
78+
end
79+
80+
def github_slug
81+
@payload.fetch("github_slug")
82+
end
83+
84+
def commit_sha
85+
@payload.fetch("commit_sha")
86+
end
87+
88+
def number
89+
@payload.fetch("number")
90+
end
91+
92+
end

pull_request_test.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env ruby
2+
#
3+
# Ad-hoc script for updating a pull request using our service.
4+
#
5+
# Usage:
6+
#
7+
# $ OAUTH_TOKEN="..." bundle exec ruby pull_request_test.rb
8+
#
9+
###
10+
require 'cc/services'
11+
CC::Service.load_services
12+
13+
class WithResponseLogging
14+
def initialize(invocation)
15+
@invocation = invocation
16+
end
17+
18+
def call
19+
@invocation.call.tap { |r| p r }
20+
end
21+
end
22+
23+
service = CC::Service::GitHubPullRequests.new({
24+
oauth_token: ENV.fetch("OAUTH_TOKEN"),
25+
update_status: true,
26+
add_comment: true,
27+
}, {
28+
name: "pull_request",
29+
# https://github.com/codeclimate/nillson/pull/33
30+
state: "success",
31+
github_slug: "codeclimate/nillson",
32+
number: 33,
33+
commit_sha: "986ec903b8420f4e8c8d696d8950f7bd0667ff0c"
34+
})
35+
36+
CC::Service::Invocation.new(service) do |i|
37+
i.wrap(WithResponseLogging)
38+
end

test/github_pull_requests_test.rb

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
require File.expand_path('../helper', __FILE__)
2+
3+
class TestGitHubPullRequests < CC::Service::TestCase
4+
def test_pull_request_status_pending
5+
expect_status_update("pbrisbin/foo", "abc123", {
6+
"state" => "pending",
7+
"description" => /is analyzing/,
8+
})
9+
10+
receive_pull_request({ update_status: true }, {
11+
github_slug: "pbrisbin/foo",
12+
commit_sha: "abc123",
13+
state: "pending",
14+
})
15+
end
16+
17+
def test_pull_request_status_success
18+
expect_status_update("pbrisbin/foo", "abc123", {
19+
"state" => "success",
20+
"description" => /has analyzed/,
21+
})
22+
23+
receive_pull_request({ update_status: true }, {
24+
github_slug: "pbrisbin/foo",
25+
commit_sha: "abc123",
26+
state: "success",
27+
})
28+
end
29+
30+
def test_pull_request_comment
31+
stub_existing_comments("pbrisbin/foo", 1, %w[Hey Yo])
32+
33+
expect_comment("pbrisbin/foo", 1, %r{href="http://example.com">analyzed})
34+
35+
receive_pull_request({ add_comment: true }, {
36+
github_slug: "pbrisbin/foo",
37+
number: 1,
38+
state: "success",
39+
compare_url: "http://example.com",
40+
})
41+
end
42+
43+
def test_pull_request_comment_already_present
44+
stub_existing_comments("pbrisbin/foo", 1, [
45+
'<b>Code Climate</b> has <a href="">analyzed this pull request</a>'
46+
])
47+
48+
# With no POST expectation, test will fail if request is made.
49+
50+
receive_pull_request({ add_comment: true }, {
51+
github_slug: "pbrisbin/foo",
52+
number: 1,
53+
state: "success",
54+
})
55+
end
56+
57+
private
58+
59+
def expect_status_update(repo, commit_sha, params)
60+
@stubs.post "repos/#{repo}/statuses/#{commit_sha}" do |env|
61+
assert_equal "token 123", env[:request_headers]["Authorization"]
62+
63+
body = JSON.parse(env[:body])
64+
65+
params.each do |k, v|
66+
assert v === body[k],
67+
"Unexpected value for #{k}. #{v.inspect} !== #{body[k].inspect}"
68+
end
69+
end
70+
end
71+
72+
def stub_existing_comments(repo, number, bodies)
73+
body = bodies.map { |b| { body: b } }.to_json
74+
75+
@stubs.get("repos/#{repo}/issues/#{number}/comments") { [200, {}, body] }
76+
end
77+
78+
def expect_comment(repo, number, content)
79+
@stubs.post "repos/#{repo}/issues/#{number}/comments" do |env|
80+
body = JSON.parse(env[:body])
81+
assert_equal "token 123", env[:request_headers]["Authorization"]
82+
assert content === body["body"],
83+
"Unexpected comment body. #{content.inspect} !== #{body["body"].inspect}"
84+
end
85+
end
86+
87+
def receive_pull_request(config, event_data)
88+
receive(
89+
CC::Service::GitHubPullRequests,
90+
{ oauth_token: "123" }.merge(config),
91+
{ name: "pull_request" }.merge(event_data)
92+
)
93+
end
94+
95+
end

0 commit comments

Comments
 (0)