From a372f5182a799e322fa619a93bad3417458d47a5 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 25 Jun 2014 16:28:26 -0400 Subject: [PATCH 1/3] Add GithubPullRequests service - Receives pull_request event - Requires a personal OAuth token - Updates status and/or adds a comment depending on configuration --- lib/cc/service.rb | 2 +- lib/cc/services/github_pull_requests.rb | 92 ++++++++++++++++++++++++ pull_request_test.rb | 38 ++++++++++ test/github_pull_requests_test.rb | 95 +++++++++++++++++++++++++ 4 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 lib/cc/services/github_pull_requests.rb create mode 100755 pull_request_test.rb create mode 100644 test/github_pull_requests_test.rb diff --git a/lib/cc/service.rb b/lib/cc/service.rb index cf2f98d..b5664e3 100644 --- a/lib/cc/service.rb +++ b/lib/cc/service.rb @@ -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 diff --git a/lib/cc/services/github_pull_requests.rb b/lib/cc/services/github_pull_requests.rb new file mode 100644 index 0000000..1992b3f --- /dev/null +++ b/lib/cc/services/github_pull_requests.rb @@ -0,0 +1,92 @@ +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{Code Climate has analyzed this pull request} + COMMENT_BODY = ' Code Climate has analyzed this pull request.' + + 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 diff --git a/pull_request_test.rb b/pull_request_test.rb new file mode 100755 index 0000000..8d0664b --- /dev/null +++ b/pull_request_test.rb @@ -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 diff --git a/test/github_pull_requests_test.rb b/test/github_pull_requests_test.rb new file mode 100644 index 0000000..5729210 --- /dev/null +++ b/test/github_pull_requests_test.rb @@ -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, [ + 'Code Climate has analyzed this pull request' + ]) + + # 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 From e583b97ce8afc1d06a6f6da821008bab62ab07b9 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 25 Jun 2014 16:30:46 -0400 Subject: [PATCH 2/3] Document Pull Request event --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a3d42d..fbb78c4 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,21 @@ 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. @@ -68,7 +83,6 @@ The following are not fully implemented yet. * :issue * :unit * :snapshot -* :pull\_request ## License From 93d3dc87e04728e9088092e00abb431f7b6df736 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 25 Jun 2014 17:20:44 -0400 Subject: [PATCH 3/3] Add a simple handler for the test event - Test that we can access the GH API with the configured token --- lib/cc/services/github_pull_requests.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/cc/services/github_pull_requests.rb b/lib/cc/services/github_pull_requests.rb index 1992b3f..395f82d 100644 --- a/lib/cc/services/github_pull_requests.rb +++ b/lib/cc/services/github_pull_requests.rb @@ -20,6 +20,17 @@ class Config < CC::Service::Config BODY_REGEX = %r{Code Climate has analyzed this pull request} COMMENT_BODY = ' Code Climate has analyzed this pull request.' + # 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