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

Commit dba23d9

Browse files
committed
Merge pull request #26 from codeclimate/pb-pull-request-service
Add GithubPullRequests service
2 parents 2a7f47a + 93d3dc8 commit dba23d9

File tree

5 files changed

+252
-2
lines changed

5 files changed

+252
-2
lines changed

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,28 @@ Event-specific attributes:
6161
}
6262
```
6363

64+
### Pull Request
65+
66+
Event name: `pull_request`
67+
68+
Event-specific attributes:
69+
70+
```javascript
71+
{
72+
"state": String, // "pending", or "success"
73+
"github_slug": String, // user/repo
74+
"number": String,
75+
"commit_sha": String,
76+
}
77+
```
78+
6479
## Other Events
6580

6681
The following are not fully implemented yet.
6782

6883
* :issue
6984
* :unit
7085
* :snapshot
71-
* :pull\_request
7286

7387
## License
7488

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: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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+
# Just make sure we can access GH using the configured token. Without
24+
# additional information (github-slug, PR number, etc) we can't test much
25+
# else.
26+
def receive_test
27+
setup_http
28+
29+
http_get("#{BASE_URL}")
30+
31+
nil
32+
end
33+
34+
def receive_pull_request
35+
setup_http
36+
37+
case @payload["state"]
38+
when "pending"
39+
update_status("pending", "Code Climate is analyzing this code.")
40+
when "success"
41+
add_comment
42+
update_status("success", "Code Climate has analyzed this pull request.")
43+
end
44+
end
45+
46+
private
47+
48+
def update_status(state, description)
49+
if config.update_status
50+
body = {
51+
state: state,
52+
description: description,
53+
target_url: @payload["details_url"],
54+
}.to_json
55+
56+
http_post(status_url, body)
57+
end
58+
end
59+
60+
def add_comment
61+
if config.add_comment && !comment_present?
62+
body = {
63+
body: COMMENT_BODY % @payload["compare_url"]
64+
}.to_json
65+
66+
http_post(comments_url, body)
67+
end
68+
end
69+
70+
def comment_present?
71+
response = http_get(comments_url)
72+
comments = JSON.parse(response.body)
73+
74+
comments.any? { |comment| comment["body"] =~ BODY_REGEX }
75+
end
76+
77+
def setup_http
78+
http.headers["Content-Type"] = "application/json"
79+
http.headers["Authorization"] = "token #{config.oauth_token}"
80+
http.headers["User-Agent"] = "Code Climate"
81+
end
82+
83+
def status_url
84+
"#{BASE_URL}/repos/#{github_slug}/statuses/#{commit_sha}"
85+
end
86+
87+
def comments_url
88+
"#{BASE_URL}/repos/#{github_slug}/issues/#{number}/comments"
89+
end
90+
91+
def github_slug
92+
@payload.fetch("github_slug")
93+
end
94+
95+
def commit_sha
96+
@payload.fetch("commit_sha")
97+
end
98+
99+
def number
100+
@payload.fetch("number")
101+
end
102+
103+
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)